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

一、普通并查集

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

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

原理及用法

以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 ,里面的题解和讨论区有更多讲解和入门题目链接

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

牛客网关押罪犯的题解代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn = 20002;
const int maxm = 100002;

struct edge {
    int a, b, c;
}e[maxm];

bool cmp(edge a, edge b) {
    return a.c > b.c;
}

int f[2 * maxn];
int Find(int x) {
    if (x == f[x])return x;
    return f[x] = Find(f[x]);
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++)
        scanf("%d%d%d", &e[i].a, &e[i].b, &e[i].c);
    sort(e, e + m, cmp);    //按仇恨值从大到小排序
    for (int i = 1; i <= 2 * n; i++)f[i] = i;    //初始化并查集

    int i;    //从大到小依次把每对罪犯安排到不同监狱
    for (i = 0; i < m; i++) {
        int a = Find(e[i].a), b = Find(e[i].b);
        if (a == b)break;    //两人的正点已在同一个集合,无法解决,最大冲突出现
        f[a] = Find(e[i].b + n);    //把a和b的反点(敌人)合并
        f[b] = Find(e[i].a + n);    //把b和a的反点(敌人)合并(每个点都有一个正点和反点)
    }
    if (i == m)printf("0");
    else printf("%d", e[i].c);
    return 0;
}
扩展域的并查集解法
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 200000 + 10;
const int gxs = 2;    //模2就只有0,1两个值,分别代表两个不同的集合
const int mod = 2;
int n, m;
int f[maxn], s[maxn];    //f记录父节点(前驱),s记录到虚拟root点的距离

void init() {
    for (int i = 0; i < maxn; i++) f[i] = i, s[i] = 0;
}
//查找
int finds(int x) {
    if (x == f[x]) return x;
    int t = f[x];
    f[x] = finds(f[x]);
    s[x] += s[t];    //s[x]原来是与t的相对距离,现在是与root的相对距离
    s[x] %= gxs;    //s值求余后代表所属监狱(二选一)
    return f[x];
}

//新建关系
void unions(int x, int y, int w) {
    int fx = finds(x), fy = finds(y);
    if (fx != fy) {
        f[fy] = fx;
        s[fy] = s[x] - s[y] + w + gxs;    //相对距离设置为w,解决这一对冲突
        s[fy] %= mod;        //求余直接赋予实际意义:所属的mod个集合的编号
    }
}

struct node {
    int a, b;
    ll val;
    bool operator < (const node &a)const {
        return val > a.val;
    }
};

vector<node> que;

int main() {
    cin >> n >> m;
    int a, b;
    ll v;
    for (int i = 0; i < m; i++) {
        cin >> a >> b >> v;
        que.push_back(node{ a,b,v });
    }
    sort(que.begin(), que.end());    //从大到小排序
    init();
    for (int i = 0; i < m; i++) {
        a = que[i].a;
        b = que[i].b;
        v = que[i].val;
        if (finds(a) == finds(b)) {    //在同一个集合就不能直接解决冲突
            if (s[a] == s[b]) {            //若s值相同就说明已经在同一个集合,冲突无法解决
                cout << v << endl;        //因为从大到小遍历,第一个解决不了的关系的val就是答案:最小化的最大冲突值
                return 0;
            }                            //否则说明解决之前的冲突后,当前冲突也被解决。
        }
        else {        //不在一个集合就可以通过设置s值解决冲突
            unions(a, b, 1);
        }
    }
    cout << 0 << endl;
    return 0;
}
加权并查集解法
原文地址:https://www.cnblogs.com/zsh-notes/p/12820467.html