并查集UFS(union-find set):
(一)并查集的定义与适用于:
(1)定义:
快速实现两个不相交的集合的合并,以及查找某个元素所在的结合的数据结构。
(2)适用于:
1.无向图的连通分量的个数;
2.Kruskal算法中求最小生成树;
3.关系传递,圈子个数;
(二)并查集的操作:
(1)initial(s):
将集合中的每一个元素都初始化为只有一个元素的子集合。
(2)find(s,x):
查找集合s中的单元素x所在的集合,并返回该集合的名字;
(3)merge(s,s1,s2):
把集合s中的不相交的集合s2,s1合并,存入到s1中。
(三)并查集的存储:
(1)使用森林的父指针数组表示法来存储并查集,每一个子集合作为一棵树,树中的每一个节点代表集合的一个元素。
(2)注意:
使用整数来表示节点的值,如果需要其他数据类型,可以用hashTable来进行映射。如:将string映射为int类型。
(四)并查集的优化:
(1)优化1:
typedef UFS{
int * group;
int subSet_count;//子集合的个数
int currentSize;//group的数组大小
}
/*
初始化:将s中的每一个元素都初始化为只有一个元素的集合。
*/
void initial(UFS & ufs,int size){
ufs.subSet_count=size;//初始化时子集合个数=数组大小
ufs.currentSize=size;
ufs.group=new int[size];
for(int i=0;i<size;i++)
ufs.group[i]=i;//每一个元素初始化为只有一个元素的集合。
}
/*
查找:查找元素ele所在的集合。返回集合的名字。
*/
int find(UFS & usf,int ele){
return ufs.group[ele];
}
/*
合并:将两个元素合并。如果两个元素在同一个集合,在不需要合并,否则合并。
*/
void union(UFS & usf,int elem1,int elem2 ){
int group1=usf.group[elem1];//获取elem1所在的集合名。
int group2=usf.group[elem2];//获取elem2所在的集合名。
if(group1==group2) return ;
for(int i=0;i<currentSize;i++){
if(ufs.group[i]==group1)
//将elem1所在的组加入到elem2所在的组
ufs.group[i]=group2;
}
ufs.subSet_count--;
}
分析:
find(x)的效率很高;
但是union(x1,x2)的效率就比较低了,因为每次union每次合并加入一个元素都需要遍历所有的节点。
(2)优化2:union优化之树结构
1.思想:
使用父指针表示法来优化union方法,group[p]的值就是p节点的父节点的序号。
如果p为树根,则group[p]=p,这里使用根节点来表示组的序号。
2.操作:
int find(UFS & ufs, int elem){
while(elem!=ufs.group[elem])
elem=ufs.group[elem];
//直到elem=ufs.group[elem],即根节点,即组的序号。
return elem;
}
/*
ufs中的group[i]就是求i的father父节点。
*/
void union(UFS & ufs, int elem2,int elem1){
int group1=find(ufs,elem1);
int group2=find(ufs,elem2);
if(group1==group2)
return;
ufs.group[group1]=group2;//group1的father为group2.
ufs.subSet_count--;
}
分析:
建树的过程中可能树是一条单链表,这样树的高度就很大,这样在find时,就很低效。为了控制树的高度,可以使用AVL树,根据AVL树的启发,可以在两棵树的 合并时,将小树作为大树的孩子。ufs.group[group1]=group2;不太合理,因为不知道哪棵树的高度大。
(3)优化3:union优化之树高度平衡
1.思想:
使用一个额外的数组size_subSet来记录每一个组的大小。
初始化时每一个组只有一个元素,所以大小为1.
for(int i=0;i<currentSize;i++)
size_subSet[i]=1;
注:size_subSet[i]记录的是序号为i的组的大小。
2.操作:
void union(UFS & ufs,int elem1,int elem2){
int group1=find(ufs,elem1);
int group2=find(ufs,elem2);
if(group1==group2) return ;
if(usf.size_subSet[group1]<usf.size_subSet[group2])
{
usf.group[group1]=group2;
usf.size_subSet[group2]+=usf.size_subSet[group1];
}
else{
usf.group[group2]=group1;
usf.size_subSet[group1]+=usf.size_subSet[group2];
}
usf.subSet_count--;
}
分析:树的高度尽可能的小,这样在find的时候才可以快速的查找。
或者:
/*通过随机法来优化,节约内存空间*/
int union(UFS & ufs,int elem1,int elem2){
int group1=find(ufs,elem1);
int group2=find(ufs,elem2);
if(group1==group2) return ;
if( rand() & 1){
ufs.group[group1]=group2;
}
else ufs.group[group2]=group1;
ufs.subSet_count--;
}
(4)优化4:find优化之一次走两步
int find(UFS & ufs, int p){
while(p!=ufs.group[p]){
ufs.group[p]=ufs.group[ufs.group[p]];
//将p的父节点设置为它的爷爷节点。
//即每次从下往上走两步最终到达根节点。
p=ufs.group[p];
}
return p;
}
(5)优化5:find优化之压缩路径
1.思想:
find是为了找集合的名称,即根节点的值。
对于某个节点到根节点的从下往上的路径,将路径中的每一个节点直接作为根节点的孩子。这样在查找根节点的值就很短了。
2.操作:
/*
将从i到根节点的路径上的每一个节点都作为根节点的直接孩子。
返回根 。
*/
int collapseFindPath(UFS & ufs, int i){
int root;
int father;
for(root=i;ufs.group[root]!=root;root=ufs.group[root]);
//找到根节点
while(i!=root){
father=ufs.group[i];//临时变量father保存i的父节点
ufs.group[i]=root;//i作为根节点的孩子。
i=father;
}
return root;
}
(6)综上:
find优化:查找优化可以一次走两步,或者压缩路径来快速从节点i定位到根节点,从而确定子集合的名字。
union优化:两个集合的合并,即两棵树的合并,让小树作为大树的孩子,保证树的高度的平衡性。
-----
(五)并查集的拓展:
(1)拓展1:添加节点间的属性关系
1)当前节点与父节点之间的关系(该关系具有传递性):
可以用一个额外的数组来表示。
如:添加一个par_relation数组, relation[i]表示的是i节点与其父节点之间的关系。
2)当前节点与根节点之间的关系:
可以用一个额外的数组来表示:
如:添加一个数组root_relation[i] 表示当前节点与i所在的根节点的关系。
(2)举例:
如:gender[i]的值为false或true,指的是节点i与其父节点的性别是否相同,
如果节点5的父节点为节点2,节点2的父节点为节点0,且gender[5]=true,gender[2]=false;gender[5]=true则表示节点5与节点2的性别不同,gender[2]=false,表示节点2与节点0的性别不同,所以得出节点5与节点0的性别不同。
可以通过gender[5]^gender[2]=true^false=false得到。
即当前节点的属性与其父节点的属性异或可以得到当前节点与其爷爷节点的属性关系。
(3)拓展2:动态规划:
----
(六)并查集的应用:
(0)并查集的应用:
1.判断一个无向图是否有环,
2.输出一个无向图的连通分量的个数,(即:关系的传递,圈子个数)
3.kruskal最小生成树的实现
4.动态规划DP:
(1)小明的生日,他邀请了很多的朋友,只有相互认识的人才会坐在一个桌子上,需要多少个桌子?如果A认识B,B认识C,意味着A,B,C相互认识,可以坐在一个桌子上。如:A认识B,B认识C,D认识E,所以A,B,C在一个桌子上,D,E在一个桌子上。
1.分析:
相当于若干次的union之后,还剩下多少棵树?
此中相当于关系传递之后,有多少个圈子?
(2)n个人,m对朋友关系,朋友关系对称且可以传递,求有几个朋友圈?
(3)并查集用于动态规划或贪心中的优化:
(4)种类相关的并查集操作:
题目中出现的元素分为一些种类,描述中会给出相关的描述信息,判断描述的正确性,即是否有悖于之前对这些元素种类的描述。一般可以增加一个kind属性来表示元素的种类。
范例:食物链;
有A,B,C三种动物A吃B,B吃C,C吃A,有两种描述:1.a,b是同类,2.a吃b。
判断有多少句假话?
分析:增加属性kind,kind[x]=0,表示与根同类,kind[x]=1,表示吃根,kind[x]=2表示被根吃。
#include <cstdio>
#include <cstring>
using namespace std;
#define MAX 50050
int par[MAX],rel[MAX];
void init(int n)
{
for(int i=1;i<=n;i++)
{
par[i]=i;
rel[i]=0;
}
}
int find(int x)
{
if(par[x]==x) return x;
int tmp=par[x];
par[x]=find(tmp);
rel[x]=(rel[tmp]+rel[x])%3;
return par[x];
}
void union_set(int x,int y,int px,int py,int d)
{
par[px]=py;
rel[px]=(rel[y]-rel[x]+2+d)%3;
}
int main()
{
int T,n,m,a,b,pa,pb,k,r;
scanf("%d%d",&n,&m);
{
init(n);
r=0;
for(int i=0;i<m;i++)
{
scanf("%d%d%d",&k,&a,&b);
if(a>n||b>n) { r++; continue;}
if(k==2&&a==b) { r++; continue;}
pa=find(a);
pb=find(b);
if(pa==pb)
{
if((rel[b]+k+2)%3!=rel[a]) r++;
}
else union_set(a,b,pa,pb,k);
}
printf("%d\n",r);
}
}
或者:
给出n个点m条边(无向边),寻找是否有奇环。可用bfs或者dfs黑白染色,用并查集则是顶点种类为kind[x]=0表示与根同色,kind[x]=1表示与根异色。
#include <cstdio>
#include <cstring>
using namespace std;
#define MAX 2050
int par[MAX],rel[MAX];
void init(int n)
{
for(int i=1;i<=n;i++)
{
par[i]=i;
rel[i]=0;
}
}
int find(int x)
{
if(par[x]==x) return x;
int tmp=par[x];
par[x]=find(tmp);
rel[x]^=rel[tmp];
return par[x];
}
void union_set(int x,int y,int px,int py)
{
par[py]=px;
rel[py]=(rel[y]==rel[x]);
}
int main()
{
int T,n,m,a,b,pa,pb,r;
scanf("%d",&T);
for(int t=1;t<=T;t++)
{
scanf("%d%d",&n,&m);
init(n);
r=0;
for(int i=0;i<m;i++)
{
scanf("%d%d",&a,&b);
if(!r)
{
pa=find(a);
pb=find(b);
if(pa==pb) r=(rel[a]==rel[b]);
else union_set(a,b,pa,pb);
}
}
printf("Scenario #%d:\n",t);
printf("%s bugs found!\n\n",r?"Suspicious":"No suspicious");
}
}
---
有n件物品,每件物品有两个属性,p和d,(price,day)表示在第d天之前将该物品卖出可以获得p的收益,假设一天至多能卖一件货物。问最多能获得的收益值。
分析:思想贪心。对货物按收益值进行排序,优先安排出售获益大的商品,对于每件商品可以把它安排在可以安排的最晚的那一天,即d之前的最晚的一天。如果直接暴力,则对于每件货物都需要从d天到第一天进行搜索,找出没有被安排的d之前的最晚的一天。时间复杂度O(nK),K是d的复杂度。考虑使用并查集优化,定义parent表示在d之前的可用时间(即题目中需要搜索出的时间)。对于每件货物设b=find(d),若b>0则该物品可安排,进行Union(b,b-1)的合并(b应该合并到b-1上),否则该货物无法安排。
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define MAX 10010
#define MP make_pair
int par[MAX];
//并查集中记录的是day,即parent[day]表示属性为day的商品最晚可以出售的日期
void init(int n)
//每个货物可以安排的最晚的一天就是自己的最后一天。
{
for(int i=0;i<=n;i++)
par[i]=i;
}
int find(int x)
{
if(x==par[x]) return x;
return par[x]=find(par[x]);
}
void union_set(int a,int b){ par[a]=b;}
vector<pair<int,int> > task;
//task为一个数组,数组中每个元素为一个键值对,第一个字段是price,第二个字段是day.
int main()
{
int n,a,b,mx;
while(scanf("%d",&n)!=EOF)
{
task.clear();
mx=0;//mx记录下商品中day的最大值。
for(int i=0;i<n;i++)
{
scanf("%d%d",&a,&b);
task.push_back(MP(a,b));
if(b>mx) mx=b;
}
sort(task.begin(),task.end());
//将数组task中各个商品按照price进行排序,从小到大。
init(mx);
int ans=0;//ans记录的是最终的最大的收益。
for(int i=n-1;i>=0;i--)
{
b=find(task[i].second);
//从数组中price最大的开始,price最大的在最后一天day出售。
if(b>0)
{
ans+=task[i].first;
union_set(b,b-1);
}
}
printf("%d\n",ans);
}
return 0;
}