并查集最详细总结(入门到精通)

并查集

给定N个元素,它们之间存在某种关系,对于这种关系的元素可以归结为一个集合,当数量太大的时候,查询等操作可能需要特别长的时间,并查集正是解决这种问题。

初始化F[x]

  1. 全部赋值为-1,F[x]=-1说明x为根结点。但在合并的时候不能判断find(x)==find(y)两个根节点都可以都是-1。
  2. 全部赋值为x,F[x]=x说明每个元素的父节点都是自己本身,比较符合逻辑,在合并的时候可以判断find(x)==find(y)从而确定两个节点是否在一个集合里,进而决定是否要进行合并

两个基本操作(包含路径压缩)

1.查询
递归

int find(int x)      // 查询根节点   
{  
    if(par[x] == x)      // 寻找到根节点,返回根节点的值   
    return x;  
    else  
    return  find(par[x]);     // 如果该节点不为根节点,就递归求其根节点   等价于去掉par[x] 
} 

递归(路径压缩)

int find(int x) {     //路径压缩   递归方法 
  
    if (x != pre[x]) pre[x] = find(pre[x]);  
  
    return pre[x];  
  
} 

非递归(路径压缩)

int findset(int v)  
{  
    int t1,t2=v;  
    while(v!=pre[v])    v=pre[v];  
    while(t2!=pre[t2])  //路径压缩    利用非递归方法 
    {  
        t1=pre[t2];  
        pre[t2]=v;  
        t2=t1;  
    }  
    return v;  
} 

2.合并

void unions(int x,int y)  
{  
    int t1=find(x);  
    int t2=find(y);  
    if(t1!=t2)  pre[t1]=t2;  
} 

广义的并查集(基本功能)

将N个元素分为几颗树,每颗树都是有关系的一类。

经典例题1

1.HDU1213—How Many Tables
http://acm.hdu.edu.cn/showproblem.php?pid=1213
题目大意:给出N个人,并且描述N个人之见的关系,例如:A和B认识,B和C认识,则可以推出A和C认识,那么ABC则是一个朋友圈的人群,用一张桌子即可,最终确定最少需要多少张桌子?
并查集最详细总结(入门到精通)_第1张图片

  1. 定义一个大小为N的数组,然后将每个数组的值赋为F[x]=x。
  2. 然后对于某两个人有关系,只需要执行合并操作即可。
  3. 最后统计数组F[x]==x的个数,也就代表团伙的个数,意味着桌子的个数。
    参考代码如下:
#include   
#include   
#include   
#include   
#include   
#include   
#define MAX_N 5000   
using namespace std;  
int ans;  
int par[MAX_N];    // 父亲   
int Rank[MAX_N];    // 树的高度   
void init(int n)    // 初始化n个元素   
{  
    for(int i = 1;i <= n;i++)  
    {  
        par[i] = i;       // 初始化  将 n 个数的根都定位自己,形成 n 个数的森林   
        Rank[i] = 0;           //  初始化      每棵树的深度为 0   
    }  
                  
}  
int find(int x)      // 查询根节点   
{  
    if(par[x] == x)      // 寻找到根节点,返回根节点的值   
    return x;  
    else  
    return  find(par[x]);     // 如果该节点不为根节点,就递归求其根节点   等价于去掉par[x] 
}   
void unite(int x,int y)    // 合并 x 和 y 所属的集合 (哪个树高 则将 低树合并到高树上面,保持树的深度最优化,便于查找)   
{  
    x = find(x);  
    y = find(y);  
    if(x == y)            // 如果原本就属于同一个根,那说明在一个集合中,就不用合并了   
    return;  
    if(Rank[x] < Rank[y])    // 如果 y 的树的高度比 x 的树的高度高的   
    par[x] = y;              // 将 x 合并到 y 上面,以 y 为根     
    else  
    par[y] = x;          // 将 y 合并到 x 上面,以 x 为根   
    if(Rank[x] == Rank[y])  
    Rank[x]++;         // 如果要合并的两颗树的长度相等,则树的高度 +1    
      
} 
 
bool same(int x,int y)   // 判断 x 和 y 是否属于同一个集合内!   
{  
    return find(x) == find(y);     // 属于统一集合返回值为 真,不属于返回值为 假 !  (   既是  find(x) == find(y) 为真还是为假  )   
}  
int main()  
{  
    int n,m,t;  
    cin >> t;  
    while(t--)  
    {  
        scanf("%d%d",&n,&m);  
        memset(par,0,sizeof(par));     // 对父节点进行清除  
        ans = n;                       //  初始化根的数目  
        init(n);  
        for(int i = 0;i < m;i++)  
        {  
            int a,b;  
            scanf("%d%d",&a,&b);  
            if(same(a,b))        //  判断两个是否属于同一集合内  
            continue;  
            else      //   如果两个点原本不属于同一集合内,则将其合并  
            {  
                unite(a,b);  
                ans--;      // 每两个树合并,树的数量少 1  
            }  
              
        }
		cout << ans << endl;          // 最后还有 ans 棵树,则需要 ans 张桌子就行了  
    }  
    return 0;  
}  
#include   
#include   
#include   
#include   
#include   
#include   
#define MAX_N 5000   
using namespace std;  
int ans;  
int par[MAX_N];    // 父亲   
int Rank[MAX_N];    // 树的高度   
void init(int n)    // 初始化n个元素   
{  
    for(int i = 1;i <= n;i++)  
    {  
        par[i] = i;       // 初始化  将 n 个数的根都定位自己,形成 n 个数的森林   
        Rank[i] = 0;           //  初始化      每棵树的深度为 0   
    }  
                  
}  
int find(int x)      // 查询根节点   
{  
    if(par[x] == x)      // 寻找到根节点,返回根节点的值   
    return x;  
    else  
    return  find(par[x]);     // 如果该节点不为根节点,就递归求其根节点   等价于去掉par[x] 
}   
void unite(int x,int y)    // 合并 x 和 y 所属的集合 (哪个树高 则将 低树合并到高树上面,保持树的深度最优化,便于查找)   
{  
    x = find(x);  
    y = find(y);  
    if(x == y)            // 如果原本就属于同一个根,那说明在一个集合中,就不用合并了   
    return;  
    if(Rank[x] < Rank[y])    // 如果 y 的树的高度比 x 的树的高度高的   
    par[x] = y;              // 将 x 合并到 y 上面,以 y 为根     
    else  
    par[y] = x;          // 将 y 合并到 x 上面,以 x 为根   
    if(Rank[x] == Rank[y])  
    Rank[x]++;         // 如果要合并的两颗树的长度相等,则树的高度 +1    
      
} 
 
bool same(int x,int y)   // 判断 x 和 y 是否属于同一个集合内!   
{  
    return find(x) == find(y);     // 属于统一集合返回值为 真,不属于返回值为 假 !  (   既是  find(x) == find(y) 为真还是为假  )   
}  
int main()  
{  
    int n,m,t;  
    cin >> t;  
    while(t--)  
    {  
        scanf("%d%d",&n,&m);  
        memset(par,0,sizeof(par));     // 对父节点进行清除  
        ans = n;                       //  初始化根的数目  
        init(n);  
        for(int i = 0;i < m;i++)  
        {  
            int a,b;  
            scanf("%d%d",&a,&b);  
            if(same(a,b))        //  判断两个是否属于同一集合内  
            continue;  
            else      //   如果两个点原本不属于同一集合内,则将其合并  
            {  
                unite(a,b);  
            }  
              
        }
		int sum=0; 
        for(int i=1;i<=n;i++) 
        if(par[i]==i)sum++;
		cout << sum << endl;          // 最后还有 ans 棵树,则需要 ans 张桌子就行了  
    }  
    return 0;  
}  
经典例题2

1.HDU1232—畅通工程
http://acm.hdu.edu.cn/showproblem.php?pid=1232
题目大意:
给出N个城市以及若干条道路,保证所有城市联通的前提,最少需要再加几条道路?
题目分析:
这道题和上面那道Table其实是一回事,A和B之间you道路 与 A和B认识 是一回事,一个Table相当于一个城市最大联通图。
所以这道题就是求出一共有n个城市联通图,那么最少需要再加n-1条道路就可以。
(上面那道题,直接求出一共有n个Table)一个求n,一个求n-1,差别就是如此。
参考代码如下:

#include
using namespace std;
int f[1001];
int find(int i)
{
	if(f[i]==i)return i;
	return f[i]=find(f[i]);
}
int merge(int x,int y)
{
	x=find(x);
	y=find(y);
	f[x]=y;
}
bool same(int x,int y)
{
	return find(x)==find(y);
}
int main()
{
	int n,m;
	while(scanf("%d",&n)&&n!=0){
	scanf("%d",&m);
	for(int i=1;i<=n;i++)
	f[i]=i;
	int x,y;
	int sum=n-1;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		if(same(x,y))continue;
		else
		{
			merge(x,y);
			sum--;
		}
	}	
	cout<
经典例题3

RQMPJ家族
http://www.rqnoj.cn/problem/331
题目大意:给出一个家族若干对关系,问某两个人是否有关系;例如给出A和B有关系,B和C有关系,问A和C是否有关系?
题目分析:用并查集的基本操作将家族划分为若干个互相有关系的群体,直接用find函数查看find(A)==find(B)是否成立即可。
参考代码如下:

#include
using namespace std;
int pre[5001];
int getf(int i){
	if(pre[i]==i)
	return i;
	else
	{
		while(pre[i]!=i)
		{
			i=pre[i];
		}
		return i;
	}
	
}
void merge(int i,int j){
	int t1,t2;
	t1=getf(i);
	t2=getf(j);
	if(t1!=t2)
	{
		f[t2]=t1;
	}
	return;
}
int main() {
	int n,m,p,i,j,k;
	cin>>n>>m>>p;
	int t1,t2;
	
	for(i=1;i<5001;i++)
		pre[i]=i;
	
	for(i=0;i>t1>>t2;
		merge(t1,t2);
	}
	for(j=0;j>t1>>t2;
		if(getf(t1)==getf(t2))
		cout<<"Yes"<
经典例题4

POJ-The Suspects
http://poj.org/problem?id=1611
题目大意:n个学生分属m个团体,一个学生可以属于多个团体。一个学生疑似患病,则它所属的整个团体都疑似患病。已知0号学生疑似患病,以及每个团体都由哪些学生构成,求一共多少个学生疑似患病。
题目分析:该题有两种做法如下
第一种就是直接将给出的关系,依次调用合并操作完成,然后利用find函数遍历所有学生,结果与学生0一样的总数量就是题目所要求的结果。
第二种就是新加一个辅助数组sum,sum[0]=1,表示以0为根节点的群体的总数为1,每当合并的时候就进行相加操作,最后直接输出sum(find(0))即可。
参考关键代码如下

方法一
for(int j=1;j<=m;j++)
		{
		
			cin>>sum;
			cin>>x;
			for(int k=1;k>y;
				merge(x,y);//每输入n个元素就合并n-1次
			}
		}
		int num=0;
		int w=find(0);//找到0所在集合的根节点
		for(int i=0;i
		int merge(int x,int y)
		{
		x=find(x);
		y=find(y);
		if(x!=y){
		f[y]=x;
		sum1[x]+=sum1[y];	//将被合并的子树的数量加到另一颗树的数量上
		}
		}
		for(int i=0;i>sum;
			cin>>x;
			for(int k=1;k>y;
				merge(x,y);
			}
		}
		cout<

并查集的补集(辅助功能)

一般给定大小为N的数组表示N个元素,因为元素之间只有一种关系,那么问题来了,如果不止一种关系怎么办,比如A和B不仅有朋友的关系还可能有敌人的关系,或者A和B的关系有兄弟、父子关系,也就说明A是B的兄弟或者A是B的父亲或者B是A的父亲。
这时候就可以利用并查集的补集,定义数组大小为2N,那么f[x]表示元素x(朋友关系)所在集合对应的父节点,f[x+n]表示元素(敌人关系)所在集合对应的父节点。
如果A和B是朋友关系,则合并x和y即可。
如果A和B是敌对关系,则合并x和y+n以及y和x+n即可。

经典例题1

RQNOJ/NOIP2010关押罪犯
http://www.rqnoj.cn/problem/600
题目大意:N个人,给出M对关系值,关系为敌人,关系值为仇恨度,一共有两个监狱,敌人不能分到一个监狱,那么需要你调整分配方案,保证分到一共监狱的敌人仇恨值最小化。
题目分析:

  1. 首先将仇恨值排序,优先分配仇恨值高的放在不同的监狱(敌人关系)。
  2. 所以每遍历一对仇恨值的时候,都相当于做一次敌人关系的合并,即合并x和y+n以及y和x+n。
  3. 在合并的过程中,先判断一下二者是否是一个集合的,如果是的话说明二者是朋友,即在一个监狱,那么就不能再分到两个监狱,输出该仇恨值即可。
    参考代码如下
#include
#include
using namespace std;
//用并查集补集法 
struct node{
	int a,b,c;
	bool operator < (const node& it)const
	{
		return c>it.c;
	}
}pairs[100005];
int f[20005<<1];
int find(int x)
{
	if(x==f[x])return x;
	return f[x]=find(f[x]);
}
int main(){
	int n,m;//罪犯个数还有怨气值对数
	int i,j,k;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n*2;i++)
	f[i]=i;
	//n~2n的部分作为罪犯的影子, X与它的影子关押在不同的监狱里 
	for(i=1;i<=m;i++)
	{
		scanf("%d%d%d",&pairs[i].a,&pairs[i].b,&pairs[i].c);
	}
	sort(pairs+1,pairs+m+1);//注意排序的具体个数 
	//将怨气值从大到小排序 
	for(i=1;i<=m;i++)
	{
		int x=find(pairs[i].a), y=find(pairs[i].b);
		if(x == y)//如果二者已经在一个集合了,不能再拆开了,则输出对应怨气值就想 
		{
			printf("%d\n",pairs[i].c);
			return 0;
		}
		else//合并操作,合并两颗并查集树,将一个根节点连接到另一个对应影子所在树的根节点 
		{
			f[x]=find(pairs[i].b+n);f[y]=find(pairs[i].a+n);
			//将x关押在b的影子对应的牢房,将y关押在a的影子对应的牢房 
		}
	}
	printf("0\n");
} 

经典例题2

RQNOJ/PID577/团伙
http://www.rqnoj.cn/problem/577
题目大意:如果两个强盗遇上了,要么是朋友,要么是敌人,朋友的朋友还是朋友,敌人的敌人也是朋友。给出敌人以及朋友关系,输出最大可能的团伙数量。
题目分析:
该题和关押罪犯类似,敌人的敌人好比不能关押在与相反监狱相反的人,也就是本监狱的人,所以方法相同。只不过要求的内容有些差异,关押罪犯给出的数据会有冲突,求出最小的冲突;本题给出的数据不会冲突,求出最后的团伙个数(均为朋友关系)。
那么问题来了,我们既然开了2N的数组,那么遍历2N个元素,统计f[x]=x即根节点的个数不就行了吗?这样做其实是不对的,因为N+1~2N其实是不存在的,并没有对应某个元素,它只是辅助作用,例如x和y+n合并,说明x和y是敌对关系,所以避免冲突,我们将y+n合并到x上即可。最后只统计前N个元素有多少个根节点即可
参考代码如下

#include
#include
using namespace std;
int f[2005]; 
int find(int i)
{
	if(f[i]==i)return i;
	int x=i,y=i;//将传入值获取 
	while(f[i]!=i)i=f[i];//得到根节点 
	while(x!=f[x])
	{
		y=f[x];
		f[x]=i;
		x=y;
	}
	//路径压缩 
	return i;
} 
int main(){
	int n,m,i,j,k;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n*2;i++)
	f[i]=i;

	char c;
	int p,q;
	for(j=1;j<=m;j++)
	{	
		cin>>c>>p>>q;
		int x=find(p),y=find(q);
		if(c=='F')//如果是同伙,直接合并到一棵树就可以 
		{
			f[x]=y;
		}
		else//如果是仇人,就把仇人的仇人合并到自己这棵树上 
		{
			f[find(q+n)]=x;
			f[find(p+n)]=y;
		}

	}
	int sum=0;
	
	for(k=1;k<=n;k++){
	if(f[k]==k)sum++;	
	}

	
	printf("%d\n",sum);
	return 0;
} 

经典例题3

PID455 / [NOI2001]食物链
http://www.rqnoj.cn/problem/455
题目大意:动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。A吃B,B吃C,C吃A。
有人用两种说法对这N个动物所构成的食物链关系进行描述:

第一种说法是“1 X Y”,表示X和Y是同类。

第二种说法是“2 X Y”,表示X吃Y。
1) 当前的话与前面的某些真的话冲突,就是假话;

2) 当前的话中X或Y比N大,就是假话;

3) 当前的话表示X吃X,就是假话。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
题目分析:
该题与上面两道题的区别就是有三种关系,所以开数组的时候开3N就可以,对于假话23很好判断,假话2判断一下越界就可以,假话3判断一下是否相同就可以。
对于假话1(二者是同类),判断(find(x)==find(y+n)) || (find(x)==find(y+2
n))是否为真,如果为真的话,说明之前说的话已经定下了关系,此处则可以作为假话处理,如果不为真,说明还未定义之间的关系或者定义的关系已经是同类,那么再次合并一次即可。
对于假话2(前者吃后者),判断(find(x)==find(y)) || (find(x)==find(y+2*n))是否为真,同理,为真的话就是假话,不为真的话,说明未定义关系或者关系已经是前者吃后者,再次错位合并即可。
参考代码如下:

#include
#include
using namespace std;
int f[150010];//代表每个动物(最多5000)对应的三种类型 
//查找 
int find(int i)//路径压缩 
{
	if(i==f[i])return i;
	int x=i,y=i;
	while(i!=f[i])i=f[i];
	while(x!=f[x])
	{
		y=f[x];//获得上一节点 
		f[x]=i;//当前节点路径压缩完成 
		x=y;//将当前节点赋值为上一节点 
	}
	return i;
}
//合并 
void getTogether(int a,int b)
{
	int x=find(a), y=find(b);
	if(x!=y)f[x]=y;	
} 
int main()
{
	int n,k,flag,i,j;
	cin>>n>>k;
	for(i=1;i<=n*3;i++)
	f[i]=i;
	//并查集初始化(也可初始化为-1) 
	int sum=0;
	int x,y;
	for(j=1;j<=k;j++)
	{
		scanf("%d%d%d",&flag,&x,&y);
		if(x>n || y>n || (flag==2 && x==y))//第二条或者第三条可以直接判断 
		{
			sum++;
			continue;			
		}
		//以下为第一条 
		if(flag==1)//二者是一个等级 
		{	
			if(x==y) continue;
			if( (find(x)==find(y+n)) || (find(x)==find(y+2*n)) )//如果查出来说明之前说过,所以可以判断为错误 
				sum++;
			else//没说过,直接当作正确的 
			{
				//将三种类型分别合并起来 
				getTogether(x,y);
				getTogether(x+n,y+n);
				getTogether(x+2*n,y+2*n);
				
			}
		}
		else
		{	//如果发现是同类,或者是被吃关系,则说明错误 
			if( (find(x)==find(y)) || (find(x)==find(y+2*n)) )
				sum++;
			else//没说过,直接合并三种类型 
			{
				getTogether(x,y+n);
				getTogether(x+n,y+2*n);
				getTogether(x+2*n,y);
			}
		}
	}
	printf("%d\n",sum);
	return 0;	
} 

总结

并查集总的来说就是两个基本操作,合并和查询,路径压缩是关键点,这个数据结构用处非常广泛,个人觉得是其他数据结构的基础,建议与最小生成树和联通图、割边等相关知识一起学习,将上述七道题均自己手动敲出来则可以熟练掌握该数据结构。

你可能感兴趣的:(ACM算法)