并查集算法

目录

一 算法简介

算法定义:

算法应用:

算法场景:

 二 并查集的基本操作

1)初始化

2)合并

3)查找

4)统计

5)例题讲解: 

How Many Tables

三 合并的优化

四 查询的优化(路径压缩)

①递归实现

②非递归实现

五 带权并查集

1)带权值的路径压缩

2)带权值的合并

3)例题讲解 

How many answers are wrong

食物链

附录:


一 算法简介

算法定义:

并查集(Disjoint Set一种非常精巧而实用的数据结构将编号分别为1--n的n个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在的集合。

算法应用:

用于处理不相交集合的合并问题

经典应用有:

–连通子图
–最小生成树Kruskal算法
–最近公共祖先

算法场景:

一个城市中有n个人,他们属于不同的帮派;

已知这些人的关系,例如1号、2号是朋友,1号、3号也是朋友,那么他们都属于一个帮派;

问有多少帮派,每人属于哪个帮派。

并查集可以很简洁地表示这个关系

 二 并查集的基本操作

并查集的基本操作有初始化,合并,查找,统计。

1)初始化

 定义int s[]是以结点i为元素的并查集。开始时,还没有处理点与点之间的朋友关系,所以每个店属于独立的集合,直接以元素i的值表示它的集合s[i]。

初始化:令s[i]=i。

左图给出了元素与集合的值,右图画出了逻辑关系。右图圆圈为集,方块为元素。

2)合并

加入第一个朋友关系(1, 2)

在并查集s中,把结点1合并到结点2,也就是把结点1的集1改成结点2的集2

 

加入第二个朋友关系(1, 3)

查找结点1的集,是2,递归查找元素2的集是2

把元素2的集2合并到结点3的集3。此时,结点123都属于一个集。

加入第三个朋友关系(2, 4) 

3)查找

查找元素的集,是一个递归的过程,直到元素的值和它的集相等,就找到了根结点的集。

这棵搜索树,可能很,复杂度是O(n),变成了一个链表,出现了树的“退化”现象。

4)统计

如果s[i]=i,这是一个根节点,是他所在的集的代表,统计根节点的数量,就是集的数量。

5)例题讲解: 

How Many Tables

有n个人一起吃饭,有些人互相认识。认识的人想坐在一起,而不想跟陌生人坐。例如A认识BB认识C,那么ABC会坐在一张桌子上。

给出认识的人,问需要多少张桌子。

输入:第一行输入整数T,表示有t个测试,再每个测试中,第一行输入整数n和m,n为朋友人数,后面m行中每行输入两个整数a和b,表示a和b认识。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1050;
int s[N];
void init_set(){                        //初始化
    for(int i = 1; i <= N; i++)   s[i] = i;
}
int find_set(int x){                    //查找
    return x==s[x]? x:find_set(s[x]);
}
void merge_set(int x, int y){           //合并
    x = find_set(x);   y = find_set(y);
    if(x != y)    s[x] = s[y];          //把x合并到y上,y的根成为x的根
}
int main (){
    int t, n, m, x, y;    cin >> t;     //t个测试
    while(t--){
        cin >> n >> m;
        init_set();
        for(int i = 1; i <= m; i++){
            cin >> x >> y;
            merge_set(x, y);            //合并x和y
        }
        int ans = 0;
        for(int i = 1; i <= n; i++)     //统计有多少个集
            if(s[i] == i)   ans++;
        cout << ans <<endl;
    }
    return 0;
}

并查集的一个简单应用是连通性判断:有三种方法,BFS,DFS,并查集。

并查集的做法是统计连通的点和边,把联通的点放到同一个集中,检查完所有点,统计他们属于几个集合,就找到了图上有几个连通块。

三 合并的优化

上述查找find_set()、合并merge_set()的搜索深度是树的长度,复杂度都是O(n)。性能

目标:优化之后,复杂度 < O(logn)

 

操作:

•合并元素xy时,先搜到它们的根结点;
•合并这两个根结点:把一个根结点的集改成另一个根结点。
•这两个根结点的高度不同,把高度较小的集合并到较大的集上,能减少树的高度。

下列代码中,在初始化时用height[i]定义元素i的高度。

int height[N];
void init_set(){
for(int i = 1; i <= N; i++){
    s[i] = i;
    height[i]=0;                     //树的高度
}
}
void merge_set(int x, int y){            //优化合并操作
x = find_set(x);
y = find_set(y);
if (height[x] == height[y]) {
    height[x] = height[x] + 1;       //合并,树的高度加一
    s[y] = x;       
}
else{                                //把矮树并到高树上,高树的高度保持不变
    if (height[x] < height[y])  s[x] = y;
    else                        s[y] = x;
    }
}

 提示:一般不需要合并的优化,因为在做了路径压缩之后,附带着优化了合并。

四 查询的优化(路径压缩)

①递归实现

在上面的查询函数中,查询元素i所属的集需要搜索路径找到根节点,返回的结果是根节点。这条搜索路径可能很长。

优化:沿路径返回时,顺便i所属的集改成根结点下次再搜,复杂度是O(1)

路径压缩:整个搜索路径上的元素,在递归过程中,从元素i到根结点的所有元素,它们所属的集都被改为根结点。

int find_set(int x){
    if(x != s[x]) 
	  s[x] = find_set(s[x]); //路径压缩
    return s[x];
}
路径压缩 不仅 优化了下次查询, 而且 也优化了合并,因为合并时也用到了查询。

②非递归实现

上面用递归实现,如果数据规模太大,有可能爆栈,下面用非递归代码实现,虽然几乎用不到。

int find_set(int x){
    int r = x;
    while ( s[r] != r ) r=s[r]; //找到根结点
    int i = x, j;
    while(i != r){   
         j = s[i]; //用临时变量j记录
         s[i]= r ; //把路径上元素的集改为根结点
         i = j;
    }
    return r;
}

五 带权并查集

上面讲了并查集的基本应用--处理集合问题,点之间只有简单的归属关系,而没有权值。

如果各位联想到树这种数据结构,会发现并查集实际上是在维护若干课树。并查集的合并和查询优化,实际上是在改变树的形状,把原来的细长的,操作低效的大量小树,改成粗短的,操作高效的少量大树。如果在原来的小树上,点之间有权值,那么经过并查集的优化变成大树后,这些权值的操作也变得高效了。

定义一个权值数组d[],把节点i到父节点的权值记为d[i]

1)带权值的路径压缩

下面的图,是加上权值之后的路径压缩。原来的权值d[],经过压缩之后,更新为d[]’,例如d[1]’=d[1]+d[2]+d[3]。

需要注意的是,这个例子中,权值是相加的关系,比较简单;在具体的题目的中,可能有相乘、异或等等符合题意的操作。

 相应地,在这个权值相加的例子中,把路径压缩的代码改为:

int find_set(int x){
    if(x != s[x]) {
         int t = s[x]; //记录父结点
         s[x] = find_set(s[x]); //路径压缩。递归最后返回的是根结点
         d[x] += d[t]; //权值更新为x到根节点的权值
     }
     return s[x];
}

注意代码中的细节。原来的d[x]是点x到它的父结点的权值,经过路径压缩后,x直接指向根节点,d[x]也更新为x到根结点的权值。这是通过递归实现的。

代码中,先用t记录x的原父结点;在递归过程中,最后返回的是根节点;最后将当前节点的权值加上原父结点的权值(注意:经过递归,此时父结点也直接指向根节点,父结点的权值也已经更新为父结点直接到根结点的权值了),就得到当前节点到根节点的权值。

2)带权值的合并

在合并操作中,把点x与到点y合并,就是把x的根结点fx合并到y的根结点fy。在fx和fy之间增加权值,这个权值要符合题目的要求。

3)例题讲解 

How many answers are wrong

 给出区间[a, b],区间之和为v。输入m组数据,每输入一组,判断此组条件是否与前面冲突,最后输出与前面冲突的数据的个数。比如先给出[1, 5]区间和为100,再给出区间[1, 2]的和为200,肯定有冲突。

本题是本节讲解的带权值并查集的直接应用。如果能想到可以把序列建模为并查集,就能直接套用模板了。

 代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn =200010;
int s[maxn]; //集合
int d[maxn]; //权值:记录当前结点到根结点的距离
int ans;

void init_set(){ //初始化
   for(int i = 0; i <= maxn; i++)
   { s[i] = i; d[i] = 0; }
}
int find_set(int x){ //带权值的路径压缩
    if(x != s[x]) {
         int t = s[x]; //记录父结点
         s[x] = find_set(s[x]); //路径压缩。递归最后返回的是根结点
         d[x] += d[t]; //权值更新为x到根节点的权值
     }
    return s[x];
}

void merge_set(int a, int b,int v){ //合并
    int roota = find_set(a), rootb = find_set(b);
    if(roota == rootb){
       if(d[a] - d[b] != v)
          ans++;
    }
    else{
       s[roota] = rootb; //合并
       d[roota] = d[b]- d[a] + v;
    }
}

int main(){
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF){
        init_set();
        ans = 0;
        while(m--){
            int a,b,v;
            scanf("%d%d%d",&a,&b,&v);
            a--;
            merge_set(a, b, v);
        }
        printf("%d\n",ans);
    }
    return 0;
}

食物链

题目描述

动物王国中有三类动物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),输出假话的总数。

题目解析

这一题中的权值比较有趣,它不是上一题中相加的关系。把权值d[]记录为两个动物在食物链上的相对关系。下面用d(A->B)表示A、B的关系,d(A->B) = 0表示同类,d(A->B) = 1表示A吃B,d(A->B) = 2表示A被B吃。

这一题难点在权值的更新。考虑三个问题:

 (i)路径压缩时,如何更新权值。

  若d(A->B) =1,d(B->C) = 1,求d(A->c)。因为A吃B,B吃C,那么C应该吃A,得d(A->C)=2;

  若d(A->B) =2,d(B->C) =2,求d(A->c)。因为B吃A,C吃B,那么A应该吃C,得d(A->C)=1;

  若d(A->B) = 0,d(B->C) =1,求d(A->c)。因为A、B同类,B吃C,那么A应该吃C,得d(A->C)=1;

  找规律知:d(A->C) = (d(A->B) + d(B->C) ) % 3,因此关系值的更新是累加再模3。

 (ii)合并时,如何更新权值。本题的权值更新是取模操作,内容见下面的代码。

 (iii)如何判断矛盾。如果已知A与根节点的关系,B与根节点的关系,如何求A、B之间的关系?内容见下面的代码。

 代码:

#include <iostream>
#include <stdio.h>
using namespace std;
const int maxn = 50005;

int s[maxn]; //集合
int d[maxn]; // 0:同类;1:吃;2:被吃
int ans;

void init_set(){ //初始化
     for(int i = 0; i <= maxn; i++)
     { s[i] = i; d[i] = 0; }
}
int find_set(int x){ //带权值的路径压缩
    if(x != s[x]) {
         int t = s[x]; //记录父结点
         s[x] = find_set(s[x]); //路径压缩。递归最后返回的是根结点
         d[x] = (d[x] + d[t]) % 3; //权值更新为x到根节点的权值
     }
    return s[x];
}
void merge_set(int x, int y, int relation){ //合并
     int rootx = find_set(x);
     int rooty = find_set(y);
    if (rootx == rooty){
      if ((relation - 1) != ((d[x] - d[y] + 3) % 3)) //判断矛盾
        ans++;
    }
    else {
      s[rootx] = rooty; //合并
      d[rootx] = (d[y] - d[x] + relation - 1) % 3; //更新权值
    }
}

int main(){
  int n, k; cin >> n >> k;
    init_set();
  ans = 0;
  while (k--){
    int relation, x, y;
    scanf("%d%d%d",&relation,&x,&y);
    if ( x > n || y > n || (relation == 2 && x == y ) )
      ans++;
    else
            merge_set(x,y,relation);
  }
  cout << ans;
  return 0;
}

附录:

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值