并查集.ppt
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
并查集及其应用
引例: 引例: 犯罪团伙 问题描述】 【问题描述】 警察抓到了n 警察抓到了n个罪犯,警察根据经验知道他们属于不同的犯 罪团伙,却不能判断有多少个团伙,但通过警察的审讯,知 道其中的一些罪犯之间相互认识,已知同一犯罪团伙的成员 之间直接或间接认识。有可能一个犯罪团伙只有一个人。请 你根据已知罪犯之间的关系,确定犯罪团伙的数量。已知罪 犯的编号从1 犯的编号从1至n。 【输入】 输入】 第一行:n <10000,罪犯数量), 第一行:n(<10000,罪犯数量), 第二行:m <100000,关系数量) 第二行:m(<100000,关系数量) 以下若干行:每行两个数:I 以下若干行:每行两个数:I 和j,中间一个空格隔开,表示 罪犯i和罪犯j 罪犯i和罪犯j相互认识。 【输出】 输出】 一个整数,犯罪团伙的数量。
并查集的基本概念及操作 一、基本概念 在一些应用问题中,有一类像引例中“犯罪团伙 在一些应用问题中,有一类像引例中“犯罪团伙”的问题, 犯罪团伙”的问题, 一种有效的解决得方法是:需要将n 一种有效的解决得方法是:需要将n个不同的元素划分成一 组不相交的集合。开始时,每个元素自己独自构成一个单元 素集合,然后按照一定的顺序或问题给定的条件和要求将有 相同属性的集合合并,最后统计集合的个数往往就是问题的 解。在这个过程中要反复的用到查询某个元素属于哪个集合 的运算;两个集合合并的运算。适合描述这类问题的抽象数 据结构称为并查集(合并与查找)。 并查集的数学模型是若干个不相交的动态集合的集合: S={A, S={A,B,C,D,……} 它支持以下两种操作: 1)、union(A,B):将A和B合并,合并后的集合名字为A或B。 )、union(A,B):将A 合并,合并后的集合名字为A 2)、find(x):查找元素x所在的集合,并返回该集合的 )、find( ):查找元素x 名字(一般返回根结点)。 在实际操作中,集合的名字一般都是用集合中的某一个代表 元素表示,不再另设置变量,这样处理比较方便。
犯罪团伙的参考程序: 犯罪团伙的参考程序: //a[i]:结点i的父亲结点:if a[i]=0: //a[i]:结点i的父亲结点:if a[i]=0:i为树根 else 指 向父亲结点。路径压缩后,都指向树根。 var a:array[1..100]of integer; i,j,k,m,n,t,x,y,p,q:integer; function find(i:integer):integer; //非递归算法, //非递归算法, 找i的根结点 var j,k,t:integer; begin j:=i; while a[j]<>0 do j:=a[j]; find:=j; end;
O(∑ i ) = O( n )
2 i =1
n
优化:当找到i所在的树根j后,把i的父亲指针指向j 优化:当找到i所在的树根j后,把i的父亲指针指向j,即 a[i]=j。这样可以减小以后的查找次数,这个优化称为路径 a[i]=j。这样可以减小以后的查找次数,这个优化称为路径 压缩。把一条路径的所有子结点都看作根的儿子,或者说 压缩。把一条路径的所有子结点都看作根的儿子,或者说 所有子结点的父亲指针都指向根。
【样例输入】 样例输入】 11 8 12 45 34 13 56 7 10 5 10 89 【样例输出】 样例输出】 3 样例说明:共三个集团。
【分析】 分析】 上述问题的模型就是求无向图的连通分量。 算法一:采用图的深度优先搜索算法,时间和空 间无法忍受。 算法二:有效算法 (1)开始把n个人看成n个独立集合(团伙)。 )开始把n个人看成n (2)每读入两个有联系(同一团伙)的人i和j, )每读入两个有联系(同一团伙)的人i 查找i 所在的集合f(i)和f(j),如果f(i)和f(j)是同一 查找i和j所在的集合f(i)和f(j),如果f(i)和f(j)是同一 个集合,不作处理;如果f(i)和f(j)属于不同的集合, 个集合,不作处理;如果f(i)和f(j)属于不同的集合, 则合并集合f(i)和f(j)为一个集合。(每个集合可以 则合并集合f(i)和f(j)为一个集合。(每个集合可以 找一个代表元素) (3)最后统计集合的个数即可得到问题的解
2、合并操作。 合并操作。 为了把两棵树p 为了把两棵树p和q(p和q必须是树根)合并起来,可以将 q的父亲指向p即可,即:a[q]:=p。也可以可以将p的父亲 的父亲指向p即可,即:a[q]:=p。也可以可以将p 指向q即可,即:a[p]:=q。 指向q即可,即:a[p]:=q。 优化:让深度小的树成为深度大的树的孩子,可以减少树 的高度,节省查找时间。但是由于在查找时有路径压缩的 操作,已经很有效了,一般不需要上述的优化。 一般情况下并查集算法的时间复杂度 O(mα(n))≈4m。 详细的时间分析可参看《算法导论》312页
并查集的一个重要的应用是确定给定集合 上的等价关系的个数。等价关系是一个具 有自反、对称和传递三个性质的关系。如 果“≡”是集合S 果“≡”是集合S上的一个等价关系,那么 对于集合S中的任意元素x 对于集合S中的任意元素x、y、z,有如下 的性质: 1)、x≡x(自反性) )、x≡x(自反性) 2)、如果x≡y,则y≡x(对称性) )、如果x≡y,则y≡x(对称性) 3)、如果x≡y,y≡z,则x≡z(传递性) )、如果x≡y,y≡z,则x≡z(传递性)
二、并查集的树结构实现 采用树结构实现并查集的基本思想是:每个集合用一棵树 来表示。树的结点用于存储集合中的元素。树中的每个结 点存放一个指向父亲的指针。在实际使用中往往用根结点 的元素代表该树所表示的集合。 如引例中:图的连通子图的数量 1)设:a[i]:为结点i的父亲指针。如果a[i]=0,则i是树的 )设:a[i]:为结点i的父亲指针。如果a[i]=0,则i 根,否则a[i]指向结点i的根。开始初始化时:a[i]=0,所 根,否则a[i]指向结点i的根。开始初始化时:a[i]=0,所 有结点都是独立的点,看成独立的一棵树根。 2)每读入两个结点x,y,如果x和y属于同一棵树,不做 )每读入两个结点x ,如果x 处理,如果属于不同的两棵树,则合并两棵树,具体操作 是:把y所在树的根看作x所在树的根的孩子。修改数组a 是:把y所在树的根看作x所在树的根的孩子。修改数组a。 3)最后统计树的数量,即a[i]为0的结点的数量。 )最后统计树的数量,即a[i]为
【输入:】 输入: 第一行:k k<=26,变量的个数,规定使用小写英 第一行:k(k<=26,变量的个数,规定使用小写英 文字母中的前k个字母作为变量,如k=5,则变量 文字母中的前k个字母作为变量,如k=5,则变量 a,b,c,d,e)。 a,b,c,d,e)。 第二行:k 第二行:k个正整数,中间用一个空格隔开,依次 代表k 代表k个变量的长度。 第三行:等式左边的表达式。 第四行:等式右边的表达式。 输出: 【输出:】 等式中出现的变量共有多少组解。
算法分析: 1、查找操作:find(i)过 查找操作:find(i)过 程。 查找元素i 查找元素i所在的集合的 过程很简单,只需要顺着 结点i 结点i到根结点的路径找 到i所在树的根即可。执 行Find(0), Find(1), …, Find(nFind(n-1), 若被搜索的元 素为 i,完成 Find(i) 操作 需要时间为O(i),完成 需要时间为O(i),完成 n 次搜索需要的总时间将达 到:
典型例题 14时限: 例14-1 二进制方程 时限:1s 【问题描述:】 问题描述:】 一个形如: X1X2…Xn=Y1Y2..Ym 的等式称为二进制方程。 在二进制方程的两边:Xi和 在二进制方程的两边:Xi和Yj (1<=i<=n;1<=j<=m)是二进制数字 1<=i<=n;1<=j<=m)是二进制数字 (0、1)或者一个变量(小写字母)。每个变量都是一个有固定长度 的二进制代码,他可以在等式中取代变量的位置,称这个长度为变量 的长度。为了解一个二进制方程,需要给其中的变量赋予适当的二进 制代码,使得我们用他们替代等式中的相应的变量后(等式的两边都 变成二进制代码),这个等式成立。 编程任务: 编程任务: 对于每一个给出的方程,计算一共有多少组解。已知变量最多有26个 对于每一个给出的方程,计算一共有多少组解。已知变量最多有26个 (26个英文小写字母),且等式的每一端的数字和变量的长度之和不 26个英文小写字母),且等式的每一端的数字和变量的长度之和不 超过10000。 超过10000。
说明:10目前还没有被查找,所以目前还没被压缩,如果后面 有查找10的查找操作,则又被压缩。
路径压缩的非递归算法: 从结点到根走两遍:第一遍找根;第二遍是将路 径上的所有结点的父亲都设为根。 function find(i:integer):integer; //非递归算法查 //非递归算法查 找i的根,并对路径进行压缩 var j,k,t:integer; begin j:=i; while a[j]<>0 do j:=a[j]; //查找根 //查找根 find:=j; k:=i; //从i开始对子树结点进行路径压缩 //从 while a[k]<>0 do begin t:=k;k:=a[k];a[t]:=j;end;
例如: 1、最常见的关系运算等号“=”在实数集合R上是一个等价 、最常见的关系运算等号“=”在实数集合R 关系。对于实数中的任意x 关系。对于实数中的任意x、y、z。一定满足下列关系: 1)、x=x )、x=x 2)、如果x=y,则y=x )、如果x=y,则y=x 3)、如果x=y,y=z,则x=z )、如果x=y,y=z,则x=z 2、引例中的“连通”也是一个等价关系。对于图中的任 意3个顶点:A,B,C。有: 个顶点:A,B,C。有: 1)、A连通A )、A连通A 2)、如果A连通B,则B连通A )、如果A连通B,则B连通A 3)、如果A连通B,B连通C,则A连通C )、如果A连通B 连通C,则A连通C 一个连通子图就是一个等价关系(连通),等价关系的个 数就是连通子图的个数。一个等价关系对应一个集合。
路径压缩的递归算法: 如果采用递归算法,一遍即可完成。是边查找边压缩,查 找和压缩同时进行。 function find(i:integer):integer; //递归寻找结点i的树根,并对结点i //递归寻找结点i的树根,并对结点i所在的子树进行路径压 缩,返回调整后的i 缩,返回调整后的i的父指针(根) begin if a[i]=0 then exit(i); //若i为根,返回本身结点序号 //若 if a[a[i]]=0 then exit(a[i]); 若i的父结点为根,则返回父 结点 find:=find(a[i]); //递归找根结点 //递归找根结点 a[i]:=find; //路径压缩,直接指向根 //路径压缩 路径压缩, end;
上述算法建成的树:
Fra Baidu bibliotek
上述中共有3棵树,即有3 上述中共有3棵树,即有3个子图(集团) 3 1 8 11 //三棵树的根} //三棵树的根} 1 1 1 1 1 1 1 8 8 1 11 //每个点所在树的根:find(i)的 //每个点所在树的根:find(i)的 值 0 1 1 3 4 1 1 0 8 7 0 //每个结点的父亲结点标志:a[i] //每个结点的父亲结点标志:a[i] 值
begin fillchar(a,sizeof(a),0); //默认根标志是0,开始全是树根 //默认根标志是0 readln(n,k); for i:=1 to k do begin readln(x,y); p:=find(x); q:=find(y); if p<>q then a[q]:=p; //合并操作 //合并操作 end; t:=0; //树根记数 //树根记数 for i:=1 to n do if a[i]=0 then inc(t); //记录树根结点 //记录树根结点 writeln(t); end.
引例: 引例: 犯罪团伙 问题描述】 【问题描述】 警察抓到了n 警察抓到了n个罪犯,警察根据经验知道他们属于不同的犯 罪团伙,却不能判断有多少个团伙,但通过警察的审讯,知 道其中的一些罪犯之间相互认识,已知同一犯罪团伙的成员 之间直接或间接认识。有可能一个犯罪团伙只有一个人。请 你根据已知罪犯之间的关系,确定犯罪团伙的数量。已知罪 犯的编号从1 犯的编号从1至n。 【输入】 输入】 第一行:n <10000,罪犯数量), 第一行:n(<10000,罪犯数量), 第二行:m <100000,关系数量) 第二行:m(<100000,关系数量) 以下若干行:每行两个数:I 以下若干行:每行两个数:I 和j,中间一个空格隔开,表示 罪犯i和罪犯j 罪犯i和罪犯j相互认识。 【输出】 输出】 一个整数,犯罪团伙的数量。
并查集的基本概念及操作 一、基本概念 在一些应用问题中,有一类像引例中“犯罪团伙 在一些应用问题中,有一类像引例中“犯罪团伙”的问题, 犯罪团伙”的问题, 一种有效的解决得方法是:需要将n 一种有效的解决得方法是:需要将n个不同的元素划分成一 组不相交的集合。开始时,每个元素自己独自构成一个单元 素集合,然后按照一定的顺序或问题给定的条件和要求将有 相同属性的集合合并,最后统计集合的个数往往就是问题的 解。在这个过程中要反复的用到查询某个元素属于哪个集合 的运算;两个集合合并的运算。适合描述这类问题的抽象数 据结构称为并查集(合并与查找)。 并查集的数学模型是若干个不相交的动态集合的集合: S={A, S={A,B,C,D,……} 它支持以下两种操作: 1)、union(A,B):将A和B合并,合并后的集合名字为A或B。 )、union(A,B):将A 合并,合并后的集合名字为A 2)、find(x):查找元素x所在的集合,并返回该集合的 )、find( ):查找元素x 名字(一般返回根结点)。 在实际操作中,集合的名字一般都是用集合中的某一个代表 元素表示,不再另设置变量,这样处理比较方便。
犯罪团伙的参考程序: 犯罪团伙的参考程序: //a[i]:结点i的父亲结点:if a[i]=0: //a[i]:结点i的父亲结点:if a[i]=0:i为树根 else 指 向父亲结点。路径压缩后,都指向树根。 var a:array[1..100]of integer; i,j,k,m,n,t,x,y,p,q:integer; function find(i:integer):integer; //非递归算法, //非递归算法, 找i的根结点 var j,k,t:integer; begin j:=i; while a[j]<>0 do j:=a[j]; find:=j; end;
O(∑ i ) = O( n )
2 i =1
n
优化:当找到i所在的树根j后,把i的父亲指针指向j 优化:当找到i所在的树根j后,把i的父亲指针指向j,即 a[i]=j。这样可以减小以后的查找次数,这个优化称为路径 a[i]=j。这样可以减小以后的查找次数,这个优化称为路径 压缩。把一条路径的所有子结点都看作根的儿子,或者说 压缩。把一条路径的所有子结点都看作根的儿子,或者说 所有子结点的父亲指针都指向根。
【样例输入】 样例输入】 11 8 12 45 34 13 56 7 10 5 10 89 【样例输出】 样例输出】 3 样例说明:共三个集团。
【分析】 分析】 上述问题的模型就是求无向图的连通分量。 算法一:采用图的深度优先搜索算法,时间和空 间无法忍受。 算法二:有效算法 (1)开始把n个人看成n个独立集合(团伙)。 )开始把n个人看成n (2)每读入两个有联系(同一团伙)的人i和j, )每读入两个有联系(同一团伙)的人i 查找i 所在的集合f(i)和f(j),如果f(i)和f(j)是同一 查找i和j所在的集合f(i)和f(j),如果f(i)和f(j)是同一 个集合,不作处理;如果f(i)和f(j)属于不同的集合, 个集合,不作处理;如果f(i)和f(j)属于不同的集合, 则合并集合f(i)和f(j)为一个集合。(每个集合可以 则合并集合f(i)和f(j)为一个集合。(每个集合可以 找一个代表元素) (3)最后统计集合的个数即可得到问题的解
2、合并操作。 合并操作。 为了把两棵树p 为了把两棵树p和q(p和q必须是树根)合并起来,可以将 q的父亲指向p即可,即:a[q]:=p。也可以可以将p的父亲 的父亲指向p即可,即:a[q]:=p。也可以可以将p 指向q即可,即:a[p]:=q。 指向q即可,即:a[p]:=q。 优化:让深度小的树成为深度大的树的孩子,可以减少树 的高度,节省查找时间。但是由于在查找时有路径压缩的 操作,已经很有效了,一般不需要上述的优化。 一般情况下并查集算法的时间复杂度 O(mα(n))≈4m。 详细的时间分析可参看《算法导论》312页
并查集的一个重要的应用是确定给定集合 上的等价关系的个数。等价关系是一个具 有自反、对称和传递三个性质的关系。如 果“≡”是集合S 果“≡”是集合S上的一个等价关系,那么 对于集合S中的任意元素x 对于集合S中的任意元素x、y、z,有如下 的性质: 1)、x≡x(自反性) )、x≡x(自反性) 2)、如果x≡y,则y≡x(对称性) )、如果x≡y,则y≡x(对称性) 3)、如果x≡y,y≡z,则x≡z(传递性) )、如果x≡y,y≡z,则x≡z(传递性)
二、并查集的树结构实现 采用树结构实现并查集的基本思想是:每个集合用一棵树 来表示。树的结点用于存储集合中的元素。树中的每个结 点存放一个指向父亲的指针。在实际使用中往往用根结点 的元素代表该树所表示的集合。 如引例中:图的连通子图的数量 1)设:a[i]:为结点i的父亲指针。如果a[i]=0,则i是树的 )设:a[i]:为结点i的父亲指针。如果a[i]=0,则i 根,否则a[i]指向结点i的根。开始初始化时:a[i]=0,所 根,否则a[i]指向结点i的根。开始初始化时:a[i]=0,所 有结点都是独立的点,看成独立的一棵树根。 2)每读入两个结点x,y,如果x和y属于同一棵树,不做 )每读入两个结点x ,如果x 处理,如果属于不同的两棵树,则合并两棵树,具体操作 是:把y所在树的根看作x所在树的根的孩子。修改数组a 是:把y所在树的根看作x所在树的根的孩子。修改数组a。 3)最后统计树的数量,即a[i]为0的结点的数量。 )最后统计树的数量,即a[i]为
【输入:】 输入: 第一行:k k<=26,变量的个数,规定使用小写英 第一行:k(k<=26,变量的个数,规定使用小写英 文字母中的前k个字母作为变量,如k=5,则变量 文字母中的前k个字母作为变量,如k=5,则变量 a,b,c,d,e)。 a,b,c,d,e)。 第二行:k 第二行:k个正整数,中间用一个空格隔开,依次 代表k 代表k个变量的长度。 第三行:等式左边的表达式。 第四行:等式右边的表达式。 输出: 【输出:】 等式中出现的变量共有多少组解。
算法分析: 1、查找操作:find(i)过 查找操作:find(i)过 程。 查找元素i 查找元素i所在的集合的 过程很简单,只需要顺着 结点i 结点i到根结点的路径找 到i所在树的根即可。执 行Find(0), Find(1), …, Find(nFind(n-1), 若被搜索的元 素为 i,完成 Find(i) 操作 需要时间为O(i),完成 需要时间为O(i),完成 n 次搜索需要的总时间将达 到:
典型例题 14时限: 例14-1 二进制方程 时限:1s 【问题描述:】 问题描述:】 一个形如: X1X2…Xn=Y1Y2..Ym 的等式称为二进制方程。 在二进制方程的两边:Xi和 在二进制方程的两边:Xi和Yj (1<=i<=n;1<=j<=m)是二进制数字 1<=i<=n;1<=j<=m)是二进制数字 (0、1)或者一个变量(小写字母)。每个变量都是一个有固定长度 的二进制代码,他可以在等式中取代变量的位置,称这个长度为变量 的长度。为了解一个二进制方程,需要给其中的变量赋予适当的二进 制代码,使得我们用他们替代等式中的相应的变量后(等式的两边都 变成二进制代码),这个等式成立。 编程任务: 编程任务: 对于每一个给出的方程,计算一共有多少组解。已知变量最多有26个 对于每一个给出的方程,计算一共有多少组解。已知变量最多有26个 (26个英文小写字母),且等式的每一端的数字和变量的长度之和不 26个英文小写字母),且等式的每一端的数字和变量的长度之和不 超过10000。 超过10000。
说明:10目前还没有被查找,所以目前还没被压缩,如果后面 有查找10的查找操作,则又被压缩。
路径压缩的非递归算法: 从结点到根走两遍:第一遍找根;第二遍是将路 径上的所有结点的父亲都设为根。 function find(i:integer):integer; //非递归算法查 //非递归算法查 找i的根,并对路径进行压缩 var j,k,t:integer; begin j:=i; while a[j]<>0 do j:=a[j]; //查找根 //查找根 find:=j; k:=i; //从i开始对子树结点进行路径压缩 //从 while a[k]<>0 do begin t:=k;k:=a[k];a[t]:=j;end;
例如: 1、最常见的关系运算等号“=”在实数集合R上是一个等价 、最常见的关系运算等号“=”在实数集合R 关系。对于实数中的任意x 关系。对于实数中的任意x、y、z。一定满足下列关系: 1)、x=x )、x=x 2)、如果x=y,则y=x )、如果x=y,则y=x 3)、如果x=y,y=z,则x=z )、如果x=y,y=z,则x=z 2、引例中的“连通”也是一个等价关系。对于图中的任 意3个顶点:A,B,C。有: 个顶点:A,B,C。有: 1)、A连通A )、A连通A 2)、如果A连通B,则B连通A )、如果A连通B,则B连通A 3)、如果A连通B,B连通C,则A连通C )、如果A连通B 连通C,则A连通C 一个连通子图就是一个等价关系(连通),等价关系的个 数就是连通子图的个数。一个等价关系对应一个集合。
路径压缩的递归算法: 如果采用递归算法,一遍即可完成。是边查找边压缩,查 找和压缩同时进行。 function find(i:integer):integer; //递归寻找结点i的树根,并对结点i //递归寻找结点i的树根,并对结点i所在的子树进行路径压 缩,返回调整后的i 缩,返回调整后的i的父指针(根) begin if a[i]=0 then exit(i); //若i为根,返回本身结点序号 //若 if a[a[i]]=0 then exit(a[i]); 若i的父结点为根,则返回父 结点 find:=find(a[i]); //递归找根结点 //递归找根结点 a[i]:=find; //路径压缩,直接指向根 //路径压缩 路径压缩, end;
上述算法建成的树:
Fra Baidu bibliotek
上述中共有3棵树,即有3 上述中共有3棵树,即有3个子图(集团) 3 1 8 11 //三棵树的根} //三棵树的根} 1 1 1 1 1 1 1 8 8 1 11 //每个点所在树的根:find(i)的 //每个点所在树的根:find(i)的 值 0 1 1 3 4 1 1 0 8 7 0 //每个结点的父亲结点标志:a[i] //每个结点的父亲结点标志:a[i] 值
begin fillchar(a,sizeof(a),0); //默认根标志是0,开始全是树根 //默认根标志是0 readln(n,k); for i:=1 to k do begin readln(x,y); p:=find(x); q:=find(y); if p<>q then a[q]:=p; //合并操作 //合并操作 end; t:=0; //树根记数 //树根记数 for i:=1 to n do if a[i]=0 then inc(t); //记录树根结点 //记录树根结点 writeln(t); end.