【详解】并查集高级技巧:加权并查集、扩展域并查集

一、普通并查集

  可以理解为使用数组实现的树形结构,只保存了每个节点的父节点(前驱)。

  功能为:合并两个节点(及其所在集合) 、 查找节点所属集合的代表节点(可以理解为根节点)。

原理及用法

以6个元素为例(编号0到5):把0单独划分为一个集合;把1,2,3,4划分为一个集合;把5单独划分为一个集合。

  1. 初始化  init()

  n个元素的并查集,只需要一个容量为n的数组f[n],值全部初始化为自己即可:for(int i=0;i<n;i++) f[i]=i;

  2. 查找节点所属集合  Find(x)

  主要代码:Find(x):  if(x == f[x]) return x;

              return Find(f[x]);

  但若只是简单的这样做,会出现上图第三个圆中的情况,即查找某个节点时递归太多次。因此需要“路径压缩”,只需增加一步:

       Find(x):  if(x == f[x]) return x;

              return f[x] = Find(f[x]);

  3. 合并两个节点(及其所在集合)  Union(x, y)

    Union(x,y):  int fx=Find(x), fy=Find(y);

           if(fx != fy) f [fx] = fy;  // 此处换为f [fy] = fx也行,道理相同,意义和效果其实也一样。

  注意:一定是f [fx] = fy,而不是f [x] = y。只有把x和y的最终父节点(前驱)连接起来,所属的两个集合才算真正完全连通,整个逻辑也才能正确。

二、扩展域并查集

使用情景:

  n个点有m对关系,把n个节点放入两个集合里,要求每对存在关系的两个节点不能放在同一个集合。问能否成功完成?

思路:

  把每个节点扩展为两个节点(一正一反),若a与b不能在一起(在同一个集合),则把a的正节点与b的反节点放一起,把b的正节点与a的反节点放一起,这样就解决了a与b的冲突。若发现a的正节点与b的正节点已经在一起,那么说明之前的某对关系与(a,b)这对关系冲突,不可能同时满足,即不能成功完成整个操作。

具体实现:

  1. 初始化  init()

  n个点,每个点扩展为两个点(一正一反),则需要一个容量为2*n的数组f[n],值全部初始化为自己即可:for(int i=0;i<2*n;i++) f[i]=i;

  (注意初始编号,若编号为[1,n],则初始化应该为:for(int i=1;i<=2*n;i++) f[i]=i;)

      一个点x的正点编号为x,反点编号为x+n(这样每个点的反点都是+n,规范、可读性强、不重复、易于理解)。

  2.  Find(x)和Union(x, y)不需要修改,含义和实现不变。

  3. 解决问题的算法步骤

  1)初始化2*n个节点的初始父节点,即它本身。

  2)遍历m对关系,对每对(a,b),先找到a和b的父节点,若相等则说明(a,b)的关系与之前的关系有冲突,不能同时解决,则得到结果:不能完成整个操作。

      否则执行:Union(a, b+n), Union(b, a+n).  (这时已经Find过了,直接执行f [fx] = fy这一句就等效与Union(x, y) )

  3)若m对关系都成功解决,则得到结果:能够完成整个操作。

拓展:

  由于扩展域会直接是数值容量翻倍,所有一般只解决这种“二分”问题,只扩展为2倍即可。

  优点在于:结构简单,并查集的操作也不需要做改变,非常易于理解。  缺点显然就是:需要额外存储空间。

三、加权并查集

使用情景:

  N个节点有M对关系(M条边),每对关系(每条边)都有一个权值w,可以表示距离或划分成多个集合时的集合编号,问题依然是判断是否有冲突或者有多少条边是假的(冲突)等。

思路:

  给N个节点虚拟一个公共的根节点,增加一个数组s[n]记录每个节点到虚拟根节点的距离,把x,y直接的权值w看为(x,y)的相对距离。

  Union(x,y,w)时额外把x,y到虚拟根节点的距离(s值)的相对差值设置为w;Find(x)时,压缩路径的同时把当前s值加上之前父节点的s值,得到真实距离。

具体实现:

  1. 初始化  init()

  f[n]数组记录节点的父节点,s[n]数组记录节点到虚拟根节点的距离:  for(int i=0;i<n;i++) {  f[i]=i;  s[i]=0; }

  2.  Find(x)

      if(x==f[x])return x;

      int t  = f[x];

      f[x] = Find(f[x]);

      s[x] += s[t];

      // s[[x] %= mod;  若s[x]表示划分成mod个集合时的集合编号等情况时,则需要求余。

      return f[x];

  3. Union(x, y,w)

      int fx = Find(x), fy = Find(y);  //此时已经s[x]和s[y]都已经计算为真值。

      if(fx != fy) {

        f [fx] = fy;

        s [fx] = (s[x] - s[y] + w + mod) % mod;

      }

  4. 解决问题的算法步骤

    初始化后,遍历m对关系:若x,y的父节点不同,则Union(x,y,w);否则,若x与y的差值为w,则说明正确,继续遍历,不为w时说明出现冲突。

    当s[x]只是代表划分为mod个集合时的集合编号时,应该比较s[x]与s[y]的值是否相同,相同时说明出现冲突;不相同时说明之前已经解决了,正确可继续遍历。

拓展:加权并查集主要得赋予并理解s[x]值的意义,较难掌握且应用广泛

  牛客网例题:关押罪犯 https://ac.nowcoder.com/acm/problem/16591 ,里面的题解和讨论区有更多讲解和入门题目链接

  直接百度搜素“加权并查集”也可找到更多讲解和入门题目链接。

猜你喜欢

转载自www.cnblogs.com/zsh-notes/p/12820467.html