并查集到带权并查集

并查集到带权并查集

合并-查找问题

在说并查集之前,我们先讲一下合并-查找问题

合并-查找问题。顾名思义,就是既有合并又有查找操作的问题

举个例子:

  1. 有一群人,他们之间有若干好友关系。
  2. 如果A是B好友的好友,或者好友的好友的好友等等,即通过若干好友可以认识,那么我们说A和B是间接好友。如果两个人有直接或者间接好友关系,那么我们就说他们在同一个朋友圈
  3. 随着时间的变化,这群人中有可能会有新的朋友关系,比如A和C变成了好友,那么C和B也是间接好友了
  4. 查找操作:这时候我们需要对当中某些人是否在同一朋友圈进行询问:B和C是否在一个朋友圈中?(是否是直接或间接欸好友?)

2和3是合并操作,4是查找操作

朴素算法

暴力直接的方式:每个人用一个编号来表示他所在的朋友圈(下图用颜色表示编号),如果有新认识的朋友,我们就合并朋友圈:把两人的朋友圈中所有人编号改成同一个

  1. A和B是好友,属于红色组,C和D是好友,属于蓝色组
  2. 询问两个人是否在同一个朋友圈,判断他们标记(颜色)是否相同
  3. 过了不久,A和D又成为了好友,我们把两个朋友圈中所有的人标记变成相同的颜色,这就完成了一次合并的操作

假设我们要合并A和D的朋友圈,需要找到所有和D在同一朋友圈里的人,并把标记改为A所在的朋友圈

//group[i]表示i所在朋友圈的颜色(编号)
//合并朋友圈B到A
for(int i =0; i<n; i++){
    if (group[i] == group[D]){
        group[i] = group[A]
    }
}

需求分析

这个时候我们就需要创建一种数据结构,能够高效的处理这三种操作,分别是

  • MakeSet(x),创建一个只有元素x的集合,且x不应出现在其他的集合中
  • Union(x, y),将元素x所在集合Sx和元素y所在的集合Sy合并,这里我们假定Sx不等于Sy
  • FindSet(x),查找元素x所在集合的代表

回到最开始的题:

  1. 有一群人,他们之间有若干好友关系。
  2. 如果A是B好友的好友,或者好友的好友的好友等等,即通过若干好友可以认识,那么我们说A和B是间接好友。如果两个人有直接或者间接好友关系,那么我们就说他们在同一个朋友圈
  3. 随着时间的变化,这群人中有可能会有新的朋友关系,比如A和C变成了好友,那么C和B也是间接好友了
  4. 查找操作:这时候我们需要对当中某些人是否在同一朋友圈进行询问:B和C是否在一个朋友圈中?(是否是直接或间接欸好友?)

假设这里有5个人,起初每个人都互相不认识,每个人都是一个集合,于是调用n次 MakeSet() 来创建n个集合

当有两个人互相认识的时候,那么我们用 Union() 来合并两个集合

询问x和y是否在同一集合,我们调用两次 Find() 查看x和y所在集合的根,通关判断根是否相同,从而判断他们是否在同一个集合内

我们看到Union(1,4),首先我们先找到1和4所在树的根,1的根就是1,而4的根就是3,这时候我们把这两颗树合并,并把1设置为3的父亲节点,这时候就完成了两颗树的合并

起初有5个人,编号为1-5,刚开始大家都互不认识,所以各自为一个节点的树,自己就是根结点

这时候如果1和2认识了,那么我们就把这两个节点所代表的树合并起来,由编号较小的1作为根

接着3和4又认识了,那么我们重复刚刚的过程,把3和4所代表树合并起来

现在1和4认识了,首先我们先找到1和4所在树的根,1的根就是1,而4的根就是3,两个根不同证明他们原本不在同一棵树上,我们就要把这两颗树合并,把1设置为3的父亲节点

在上述一系列操作中,初始化构建树就是MakeSet操作,树的合并就是Union操作,而找根的过程就是FindSet操作

这个数据结构就是并查集

并查集是什么

管理元素分组情况的数据结构,在并查集中,每个不相交的集合都用一颗有根树来表示,每个元素都是树上的一个节点

并查集可以高效地进行如下操作:

  • 查询元素a和元素b是否属于同一集合(组)
  • 合并元素a和元素b所在集合(组)

并查集的结构

树形结构实现的

并查集支持的操作

  • MakeSet建立组(集合)

    //p[i]表示根为i的集合
    void MakeSet(){
        for(int i = 0; i < N; i++)
            p[i] = i;
  • Union合并,从一个组的根向另一个组的根连边,这样两棵树变成了一棵树,也就把两个集合合并为一个集合了

    void UnionA(int x, int y)
    {
      int xRoot = FindSet(x);
      int yRoot = FindSet(y);
      //将集合x所在合并到y所在集合上
      parent[xRoot] = yRoot;
    }

  • FindSet查询:如果两个节点的根相同,就可以知道它们属于同一集合

    //Find(x) return the root of x 查找所在集合的根
    //对父节点递归调用Find,直到找到根为止
    int FindSet(int x){
        if(x == parent[x])    //如果父节点是的根就是自身,返回这个节点
          return x;
        else             //否则递归调用查询父亲的根节点
              return FindSet(parent[x]);
    }

并查集的优化

在树形数据结构中,如果发生了退化的情况,复杂度就会变得很高,具体来讲,像下面这棵树,如果我们越加越深,那么每次调用Find可能会需要O(n)的时间,总的复杂度在最坏情况下就是O(nQ)了

解决方案:

1. 路径压缩

递归找到根节点的时候,把当前节点到根节点间所有节点的父节点都设置为根节点

例如我们现在要找元素9所在树的根节点,在找根节点的过程中使用路径压缩,也就是说9到根的路径上的节点9,6,3,1的父节点都设置成为根节点0,所以在 FindSet(9) 之后,树就变成了下面的样子,这就是路径压缩,具体来讲就是加一行代码

int FindSet(int x){
      if(x == parent[x])    //如果父节点是的根就是自身,返回这个节点
          return x;
      else{
          parent[x] = FindSet(parent[x]);//路径上节点的父节点都设置成为根节点0
          return FindSet(parent[x]);
          //或者直接一句 return p[x] = FindSet(p[x]);
      }             
  }

2. 按秩合并(启发式合并)

路径压缩时直接将节点的父亲修改成最终的祖先节点,在破坏原先的树结构的同时,在有些题目中也会损失信息

对于每棵树,记录这棵树的高度rank,合并时如果两棵树的高度不同,从高度小的向高度大的连边

void Union(int x, int y){
    int xRoot = FindSet(x);//找出双方的根
    int yRoot = FindSet(y);

    if(xRoot == yRoot) return;//同根则结束
    
    if(rank[xRoot] < rank[yRoot])//比较高度
        parent[xRoot] = yRoot;
    else if(rank[xRoot] > rank[yRoot]) 
        parent[yRoot] = xRoot;
    else{ //rank[xRoot] == rank[yRoot]
        parent[yRoot] = xRoot;
        rank[xRoot]++;
    }
}

代码模板

//initial the sets 这里有5个人,每个人都是一个集合
void MakeSet(){
    for(int i = 0; i < N; i++){
        parent[i] = i;
    }
}

//压缩路径
int FindSetA(int x){
    if(x == parent[x])
        return x;
    else
        return parent[x] = FindSet(parent[x]);
}
//启发式合并:不压缩路径,保持树结构
int FindSetB(int x){
    if(x == parent[x])
        return x;
    else
        return FindSetB(parent[x]);
}   

//普通合并
void UnionA(int x, int y)
{
    int xRoot = FindSet(x);
    int yRoot = FindSet(y);
    //将集合x所在合并到y所在集合上
    parent[xRoot] = yRoot;
}

//启发式合并:小树接在大树上
void UnionB(int x, int y){
    int xRoot = FindSet(x);//找出双方的根
    int yRoot = FindSet(y);
    if(xRoot == yRoot) return;//同根则结束
    
    if(rank[xRoot] < rank[yRoot]) 
        parent[xRoot] = yRoot;
    else if(rank[xRoot] > rank[yRoot]) 
        parent[yRoot] = xRoot;
    else{
        parent[yRoot] = xRoot;
        rank[xRoot]++;
    }
}

bool IsSameRoot(int x, int y){
    //printf("root of %d: %d\n", x, FindSet(x));
    //printf("root of %d: %d\n", y, FindSet(y));
    return FindSet(x) == FindSet(y);
}

例题part1

hdu1232 城镇交通

Problem Description
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?

Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说

3 3
1 2
1 2
2 1

这种输入也是合法的
当N为0时,输入结束,该用例不被处理。

Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。

Sample Input

4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0

Sample Output

1
0
2
998

问将所有独立的集合连接起来还需要几条路,那只要找到独立集合个数-1就是答案

这里做法很简单,用给出的数据建树,再遍历每一个节点,此节点为根节点则总集合数目++,rank都不需要了

#include <stdio.h>
const int MAX = 1000;
int parent[MAX];
//初始化集合
void MakeSet(int n){
    int i;
    for (i = 1; i <= n; i++)
        parent[i] = i;
}
//查找函数
int FindSet(int x){
    if (x == parent[x])
        return x;
    else
        return parent[x] = FindSet(parent[x]);
}
//合并函数
void Union(int a, int b){
    int aRoot, bRoot;
    aRoot = FindSet(a);
    bRoot = FindSet(b);
    if (aRoot != bRoot)
        parent[aRoot] = bRoot;
}
int main()
{
    int n, m, a, b;
    while (scanf("%d", &n) != EOF){
        if (!n) break;
        MakeSet(n);
        scanf("%d", &m);
        for (int i = 0; i < m; ++i){
            scanf("%d %d", &a, &b);
            Union(a, b);
        }
        //确定连通分量个数
        int sum = 0;
        for (int i = 1; i <= n; ++i)
            if (parent[i] == i)
                sum++;
        printf("%d\n", sum - 1);
    }
    return 0;
}

poj1611 感染的学生

一些学生被分组,0号可能感染病毒,跟他同一集合的也可能感染,那么给出几个分组,问可能感染的人数

Sample Input

100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0

Sample Output

4
1
1

开个数组sumInSet统计每个集合的人数,以0号作为根节点,sumInSet[0]就是和0在同个集合的人数,即为答案,做一下路径压缩即可,也不需要rank

#include <bits/stdc++.h>
using namespace std;

const int MAX = 30001;
int sumInSet[MAX];//每个集合人数
int parent[MAX];
int trank[MAX];//树高

int FindSet(int x) {
    if (x == parent[x])
        return x;
    else //带路径压缩
        return parent[x] = FindSet(parent[x]);
}
void Union(int x, int y) {
    int xRoot = FindSet(x);//找出双方的根
    int yRoot = FindSet(y);
    if (xRoot == yRoot) return;//同根则结束
    if (trank[xRoot] > trank[yRoot]) {//让rank比较高的作为父结点
        parent[yRoot] = xRoot;
        sumInSet[xRoot] += sumInSet[yRoot];
    }
    else {
        parent[xRoot] = yRoot;
        if (trank[xRoot] == trank[yRoot])
            trank[yRoot]++;
        sumInSet[yRoot] += sumInSet[xRoot];
    }
}
int main()
{
    int n, m;
    int k, x, y;
    while (scanf("%d%d", &n, &m)!=EOF) {
        if (n == 0 && m == 0) return 0;
        //init
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            sumInSet[i] = 1;
            trank[i] = 0;
        }
        for (int i = 0; i < m; i++) {
            scanf("%d%d", &k, &x);
            k--;
            while (k--) {
                scanf("%d", &y);
                Union(x, y);
            }
        }
        printf("%d\n", sumInSet[FindSet(0)]);
    }
    return 0;
}

带权并查集

普通的并查集仅仅记录的是集合的关系,这个关系无非是同属一个集合或者是不在一个集合

带权并查集不仅记录集合的关系,还记录着集合内元素的关系或者说是集合内元素连接线的权值

普通并查集本质是不带权值的图,而带权并查集则是带权的图

考虑到权值就会有以下问题:

  1. 每个节点都记录的是与根节点之间的权值,那么在Find的路径压缩过程中,权值也应该做相应的更新,因为在路径压缩之前,每个节点都是与其父节点链接着,那个Value自然也是与其父节点之间的权值
  2. 在两个并查集做合并的时候,权值也要做相应的更新,因为两个并查集的根节点不同

向量偏移法

对于集合里的任意两个元素x,y而言,它们之间必定存在着某种联系,因为并查集中的元素均是有联系的(这点是并查集的实质,要深刻理解),否则也不会被合并到当前集合中,那么我们就把这2个元素之间的关系量转化为一个偏移量

路径压缩

int FindSet(int x) {
    if (x == parent[x])
        return x;
    else {
        int t = parent[x]; //记录原父节点编号
        parent[x] = FindSet(parent[x]); //父节点变为根节点,此时value[x]=父节点到根节点的权值
        value[x] += value[t]; //当前节点的权值加上原本父节点的权值
         return parent[x]
    }
}

因为在路径压缩后父节点直接变为根节点,此时父节点的权值已经是父节点到根节点的权值了,将当前节点的权值加上原本父节点的权值,就得到当前节点到根节点的权值

合并

已知x,y根节点分别为xRoot,yRoot,如果有了x、y之间的关系,合并如果不考虑权值直接修改parent就行了,但是现在是带权并查集,必须得求出xRoot与yRoot这条边的权值是多少,很显然x到yRoot两条路径的权值之和应该相同,就不难得出上面代码所表达的更新式(但是需要注意并不是每个问题都是这样更新的,有时候可能会做取模之类的操作,这一点在之后的例题中可以体现)

    int xRoot = FindSet(x);
    int yRoot = FindSet(y);
    if (xRoot != yRoot)
    {
        parent[xRoot] = yRoot;
        value[xRoot] = -value[x] + value[y] + s;
    }

例题part2

hdu3308

给出区间[1,n],下面有m组数据,l r v表示[l,r]区间和为v,每输入一组数据,判断此组条件是否与前面冲突,输出冲突的数据的个数

用一个value[]数组保存从某点到其根节点距离

  1. roota != rootb时,

    合并操作将roota并入rootb,roota~>rootb = b~>rootb - b~>roota

    然后我们可以知道 b~>roota = a~>roota - a~>b

    所以最后可以推出 roota ~>rootb = b~>rootb + a~>b - a~>roota

    而roota的根节点是rootb,所以 roota~>rootb = value[roota]

    然后依次推出得到 value[roota] = -value[a]+value[b]+v (a~>b=v)

  2. roota==rootb

    a~>b = a~>root - b~>root然后得到表达式 v = value[a]-value[b] (一定要记住这里的sum都是相对于根节点的,sum的更新在路径压缩的时候更新了)

#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 200010;
int parent[N];
int value[N];  ///记录当前结点到根结点的距离

int FindSet(int x) {
    if (x == parent[x])
        return x;
    else {
        int t = parent[x]; //记录原父节点编号
        parent[x] = FindSet(parent[x]); //父节点变为根节点,此时value[x]=父节点到根节点的权值
        value[x] += value[t]; //当前节点的权值加上原本父节点的权值
        return parent[x]
            // value[x] += value[FindSet(parent[x])];
            // return parent[x] = FindSet(parent[x]);
    }
}

int main()
{
    int n, m;
    while (scanf("%d%d", &n, &m) != EOF) {
        for (int i = 0; i <= n; i++) {
            parent[i] = i;
            value[i] = 0;
        }
        int ans = 0;
        while (m--) {
            int a, b, v;
            scanf("%d%d%d", &a, &b, &v);
            a--;
            int roota = FindSet(a);
            int rootb = FindSet(b);
            if (roota == rootb) {
                if (value[a] - value[b] != v) ans++;  ///精华部分1
            }
            else {
                parent[roota] = rootb;
                value[roota] = -value[a] + value[b] + v; ///精华部分2
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

POJ2492

每次给出两个昆虫的关系(异性关系),然后发现这些条件中是否有悖论

第一组数据

1 2
2 3
1 3

1和2是异性,2和3是异性,然后说1和3是异性就显然不对了

用r[x]存储的是x与其根节点rx的关系,0代表同性1代表异性

这道题与上一道题唯一的不同是权值不是累加的关系而是相当于二进制的个位,也就是累加结果取%2。

#include <cstdio>

const int maxn = 2000 + 10;

int parent[maxn];
int value[maxn];  //与根节点的关系,如果值为1则为异性如果为0则为同性

int FindSet(int x) {
    int t = parent[x];
    if (parent[x] != x) {
        parent[x] = FindSet(parent[x]);
        value[x] = (value[x] + value[t]) % 2;  // 更新关系
    }
    return parent[x];
}

int main() 
{
    int flag;
    int kase = 0;
    int T;
    int x, y;
    int n, m;
    scanf("%d", &T);
    while (T--) {
        flag = 1;
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++) {
            parent[i] = i;
            value[i] = 0;
        }
        while (m--) {
            scanf("%d%d", &x, &y);
            if (!flag) continue;
            int rx = FindSet(x);
            int ry = FindSet(y);
            if (rx == ry) {
                if ((value[x] - value[y]) % 2 == 0) {
                    flag = 0;
                }
            }
            else {
                parent[rx] = ry;
                value[rx] = (value[x] - value[y] + 1) % 2;
            }
        }
        printf("Scenario #%d:\n", ++kase);
        if (flag)
            printf("No suspicious bugs found!\n\n");
        else
            printf("Suspicious bugs found!\n\n");
    }
    return 0;
}

我们再把上一道题升级一下,从两个种类拓展为三个种类,由于三个种类的关系依旧是一个所以依然可以套带权并查集模版,有几个种类就取几模,这里是%3

poj1182 食物链

Description

动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。

Input

第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。

Output

只有一个整数,表示假话的数目。

Sample Input

100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5

Sample Output

3

这道题难度会稍微上升,上一题看不太懂也没关系,我这里展开来细讲

向量偏移法:对于集合里的任意两个元素x,y而言,它们之间必定存在着某种联系,因为并查集中的元素均是有联系的(这点是并查集的实质,要深刻理解),否则也不会被合并到当前集合中,那么我们就把这2个元素之间的关系量转化为一个偏移量

我们先用数值表示三种基本关系:

  • 两者同类 = 0
  • 吃父节点 = 1
  • 被父节点吃 = 2

我们需要处理两种关系转化

1.在同一棵树上,根节点相同

A-r-B表示A和B之间的关系是r,比如A-1-B代表A吃B。

现在,若已知A~>B = r1,B~>C = r2,求A~>C

如下图,ABC的关系就像向量,A~>B,B~>C的关系已知并且可以量化

A~>B B~>C A~>C
0 0 0
0 1 1
0 2 2
1 0 1
1 1 2
1 2 0
2 0 2
2 1 0
2 2 1

于是我们不难发现,A~>C=(r1+r2)%3(这里模3是保证偏移量取值始终在[0,2]间)

这个就是在FindSet(int x)函数中用到的更新x与parent[x]的关系

2.两棵树合并

合并X的根节点和Y的根节点,同时修改各自的rank

现有a-r-b,Pa和Pb分别是a和b的根节点,我们已知的是

  • a~>Pa = rank[a]
  • b~>Pb = rank[b]
  • a~>b = r

显然我们可以得到

  • Pa~>a = (3-rank[a])%3
  • Pb~>b = (3-rank[b])%3

假如合并后a为新的树的根节点,那么原先Pa树上的节点不需变化,Pb树则要改变,因为rank[i]值为该节点i和树根的关系。这里只改变rank(Pb)即可,因为在进行 FindSet() 操作时可相应改变Pb树的所有节点的值rank值

于是问题变成了Pb~>Pa = ?

Pa~>Pb = Pb~>b + b~>a + a~>Pa = 3-rank[b] + (3-r)%3 + rank[a]

#include <iostream>
const int MAX = 50005;

int parent[MAX];
int rank[MAX];

void MakeSet(int x){
    parent[x] = x;
    rank[x] = 0;
}

//查找x的集合,回溯时压缩路径,并修改x与father[x]的关系
int FindSet(int x)
{
    if (x == parent[x])
        return x;
    else {
        //更新x与father[X]的关系
        rank[x] = (rank[x] + rank[parent[x]]) % 3;
        return parent[x] = FindSet(parent[x]);
    }
}

//合并x,y所在的集合
void Union(int x, int y, int d)
{
    int xRoot = FindSet(x);
    int yRoot = FindSet(y);
    //将集合x所在合并到y所在集合上
    parent[xRoot] = yRoot;
    //更新x的根与x的父节点的关系
    rank[xRoot] = (rank[y] - rank[x] + 3 + d) % 3;
}

int main()
{
    int ans = 0;
    int n, k, x, y, d, xRoot, yRoot;
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i)
        MakeSet(i);
    while (k--) {
        scanf("%d%d%d", &d, &x, &y);
        //如果x或y比n大,或x吃x,是假话
        if (x > n || y > n || (d == 2 && x == y)) {
            ans++;
        }
        else
        {
            xRoot = FindSet(x);
            yRoot = FindSet(y);
            //如果x,f的父节点相同 ,那么可以判断给出的关系是否正确的
            if (xRoot == yRoot) {
                if ((rank[x] - rank[y] + 3) % 3 != d - 1)
                    ans++;
            }
            else {
                //否则合并x,y
                Union(x, y, d - 1);
            }
        }
    }
    printf("%d\n", ans);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/zhxmdefj/p/11117791.html