文章目录
并查集(Union-Find)
常用来解决动态连通性问题。
曾有外国网友在StackExchange上发起过投票,选出世界十大有趣算法。“Union-Find算法”以4票的微弱优势战胜排名第二的“KMP算法”,荣登榜首。
原投票网址:传送门
投票截止时间:2014年3月7日
基本思想:每个集合用一棵树表示。因为同一棵树上的每一个节点都有相同的根,这样就可以用根节点来标识每一棵树,即用代表元来标识每一个集合。我们可以将树存储在数组中。即S[i]=j。代表j为i的父节点。
并查集的基本应用:
- 确定元素属于哪个集合
- 判断两个元素是否属于同一集合
- 合并两元素
- 求一个无向图中连通分量的个数
- 判断增加一条边后是否会产生环(可用在kruskal算法中)
生活中的问题:
- 朋友圈
- 村中的族谱(大伯二婶三叔)
- 武林门派
- 路由网络
初始化的两种方法:
第一种:刚开始时每个集合中都只有一个元素,每个元素在树中都是根节点。故设S[i]=-1;
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
j | -1 | -1 | -1 | -1 | -1 |
第二种:对于每个节点i。他的组号也是i,即S[i]=i
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
j | 1 | 2 | 3 | 4 | 5 |
并集的三种方法:
这里有普通合并和两个启发函数可以选择:
第一种:普通合并(使用第1/2种初始化):无脑合并,两个元素谁合并到谁都行。
第二种:根据节点数量进行合并(使用第1种初始化):将节点少的树合并到节点数量多的树中.
第三种:根据树的高度进行合并(使用第2种初始化):将高度小的树合并到高度高的树中.
假设初始节点为1,2,3,4,5。合并顺序为:1和2、2和3、4和5
查集的两种方法:
第一种:(前提是用的第1种初始化和能使用第1种初始化方法的合并方法)(路径压缩)
J为负数时,代表i为根节点。并且j的绝对值代表这棵树中的节点个数。
J为正数时,代表j是i的父节点
查找x所在的集合
X=a[x] 直到a[x]小于0(等于-1)
则这个小于0(等于-1)的a[x]中的x就是目标集合,节点x就是这棵树的根节点
- 例:查找元素1所在的集合
- x=a[1]=2>0
- x=a[2]=3>0
- x=a[3]=-3 <0
- 即:3为目标集合,3为这棵树的根
根据合并顺序,结果为
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
j | 2 | 3 | -3(-1) | 5 | -2(-1) |
第二种:(前提是用的第2种初始化和能使用第2种初始化方法的合并方法)(非路径压缩)
J的值只能为正数,代表j是i的父节点(i=j时代表i是根节点)
查找x所在的集合
x=a[x] 直到a[x]等于x为止
则这个等于a[x]的x就是目标集合,节点x就是这棵树的根节点
- 例:查找元素1所在的集合
- a[1]=2!=1
- a[2]=3!=2,
- a[3]=3==3 故3为目标集合, 3为这棵树的根
根据合并顺序,结果为
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
j | 2 | 3 | 3 | 5 | 5 |
并、查的基本方法与优化
由上述可知,并与查的操作可以有以下4种方式:
源代码
基于上图四种方式,给出相应的源代码及输入和输出信息。
一、第1种初始化,第1种合并,第1种查询
#include <iostream>
#include <string.h>
#include <stdio.h>
#include<iomanip>
using namespace std;
int father[1002];
int m,n; //分别代表节点数和关系数
int count; //统计集合的个数
int Find(int x)
{
if(father[x]==-1)
return x;
return Find(father[x]); //或者用return father[x]=Find(father[x]);
//father[t]=Find(father[t]); //如果用这一句,则报错:Memory Limit Exceeded
}
void Union(int x,int y)
{
x=Find(x);
y=Find(y);
if(x!=y)
{
father[x]=y;
count--;
}
}
int main()
{
freopen("input3.txt", "r", stdin);
memset(father, -1, sizeof(father)); //都设置为-1
cin>>m>>n;
count=m; //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
for(int i=1;i<=n;i++)
{
int x,y;
cin>>x>>y;
Union(x,y);
}
//显示之间的关系
cout<<"树状关系如下:"<<endl;
for(int i=1;i<=m;i++)
cout<<i<<" --> "<<setw(2)<<father[i]<<endl;
cout<<endl<<"集合个数:"<<count<<endl;
for(int i=1;i<=m;i++)
{
int flag=0;
if(father[i]==-1)
{
flag=1;
cout<<"根节点"<<i<<"代表一个集合。其中有:";
for(int j=1;j<=m;j++)
{
if(Find(j)==i)
cout<<j<<" ";
}
}
if(flag==1)
cout<<endl;
}
}
二、第1种初始化,第2种合并,第1种查询
#include <iostream>
#include <string.h>
#include <stdio.h>
#include<iomanip>
using namespace std;
int father[1002];
int m,n; //分别代表节点数和关系数
int count; //统计集合的个数
int Find(int x)
{
if(father[x]<0)
return x;
return Find(father[x]); //或者用return father[x]=Find(father[x]);
//father[t]=Find(father[t]); //如果用这一句,则报错:Memory Limit Exceeded
}
void Union(int x,int y)
{
x=Find(x);
y=Find(y);
if(x==y)
{
return;
}
if(father[x]<father[y])
{
father[x]+=father[y];
father[y]=x;
count--;
}
else //默认两树中节点相同时,让x并到y上。当然,也可以默认y并到x上
{
father[y]+=father[x];
father[x]=y;
count--;
}
}
int main()
{
freopen("input3.txt", "r", stdin);
memset(father, -1, sizeof(father)); //都设置为-1
cin>>m>>n;
count=m; //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
for(int i=1;i<=n;i++)
{
int x,y;
cin>>x>>y;
Union(x,y);
}
//显示之间的关系
cout<<"树状关系如下:"<<endl;
for(int i=1;i<=m;i++)
cout<<i<<" --> "<<setw(2)<<father[i]<<endl;
cout<<endl<<"集合个数:"<<count<<endl;
for(int i=1;i<=m;i++)
{
int flag=0;
if(father[i]<0)
{
flag=1;
cout<<"根节点"<<i<<"代表一个集合。其中有:";
for(int j=1;j<=m;j++)
{
if(Find(j)==i)
cout<<j<<" ";
}
}
if(flag==1)
cout<<endl;
}
}
三、第2种初始化,第1种合并,第2种查询
#include <iostream>
#include <stdio.h>
#include<iomanip>
using namespace std;
int father[1002];
int m,n; //分别代表节点数和关系数
int count; //统计集合的个数
void Init()
{
for(int i=1;i<=m;i++)
{
father[i]=i;
}
}
int Find(int x)
{
if(x==father[x])
return x;
return Find(father[x]); //或者用return father[x]=Find(father[x]);
//father[t]=Find(father[t]); //如果用这一句,则报错:Memory Limit Exceeded
}
void Union(int x,int y)
{
x=Find(x);
y=Find(y);
if(x!=y)
{
father[x]=y;
count--;
}
}
int main()
{
freopen("input3.txt", "r", stdin);
cin>>m>>n;
Init();
count=m; //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
for(int i=1;i<=n;i++)
{
int x,y;
cin>>x>>y;
Union(x,y);
}
//显示之间的关系
cout<<"树状关系如下:"<<endl;
for(int i=1;i<=m;i++)
cout<<i<<" --> "<<setw(2)<<father[i]<<endl;
cout<<endl<<"集合个数:"<<count<<endl;
for(int i=1;i<=m;i++)
{
int flag=0;
if(i==father[i])
{
flag=1;
cout<<"根节点"<<i<<"代表一个集合。其中有:";
for(int j=1;j<=m;j++)
{
if(Find(j)==i)
cout<<j<<" ";
}
}
if(flag==1)
cout<<endl;
}
}
四、第2种初始化,第3种合并,第2种查询
#include <iostream>
#include <stdio.h>
#include<iomanip>
using namespace std;
int father[1002];
int height[1002];
int m,n; //分别代表节点数和关系数
int count; //统计集合的个数
void Init()
{
for(int i=1;i<=m;i++)
{
father[i]=i;
height[i] = 1;
}
}
int Find(int x)
{
if(x==father[x])
return x;
return Find(father[x]); //或者用return father[x]=Find(father[x]);
//father[t]=Find(father[t]); //如果用这一句,则报错:Memory Limit Exceeded
}
void Union(int x,int y)
{
x=Find(x);
y=Find(y);
if(x==y)
{
return;
}
if(height[x]==height[y])
{
height[x]++; //当两棵树一样高的时候,则增加其中一棵的高度
father[x]=y; //因为是让x所在的树高度自增的,所以此时x树要比y树高了,所以要把y并到x树上。当然,也可以让y树自增然后把x并到y树上
count--;
}
else if(height[x]<height[y])
{
father[x]=y;
count--;
}
else
{
father[y]=x;
count--;
}
}
int main()
{
freopen("input3.txt", "r", stdin);
cin>>m>>n;
Init();
count=m; //初始时每个节点都代表一个独立的集合。故集合个数等于节点的个数
for(int i=1;i<=n;i++)
{
int x,y;
cin>>x>>y;
Union(x,y);
}
//显示之间的关系
cout<<"树状关系如下:"<<endl;
for(int i=1;i<=m;i++)
cout<<i<<" --> "<<setw(2)<<father[i]<<endl;
cout<<endl<<"集合个数:"<<count<<endl;
for(int i=1;i<=m;i++)
{
int flag=0;
if(i==father[i])
{
flag=1;
cout<<"根节点"<<i<<"代表一个集合。其中有:";
for(int j=1;j<=m;j++)
{
if(Find(j)==i)
cout<<j<<" ";
}
}
if(flag==1)
cout<<endl;
}
}
输入:
输出: