给定N个元素,它们之间存在某种关系,对于这种关系的元素可以归结为一个集合,当数量太大的时候,查询等操作可能需要特别长的时间,并查集正是解决这种问题。
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.HDU1213—How Many Tables
http://acm.hdu.edu.cn/showproblem.php?pid=1213
题目大意:给出N个人,并且描述N个人之见的关系,例如:A和B认识,B和C认识,则可以推出A和C认识,那么ABC则是一个朋友圈的人群,用一张桌子即可,最终确定最少需要多少张桌子?
#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;
}
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<
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"<
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即可。
RQNOJ/NOIP2010关押罪犯
http://www.rqnoj.cn/problem/600
题目大意:N个人,给出M对关系值,关系为敌人,关系值为仇恨度,一共有两个监狱,敌人不能分到一个监狱,那么需要你调整分配方案,保证分到一共监狱的敌人仇恨值最小化。
题目分析:
#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");
}
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;
}
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+2n))是否为真,如果为真的话,说明之前说的话已经定下了关系,此处则可以作为假话处理,如果不为真,说明还未定义之间的关系或者定义的关系已经是同类,那么再次合并一次即可。
对于假话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;
}
并查集总的来说就是两个基本操作,合并和查询,路径压缩是关键点,这个数据结构用处非常广泛,个人觉得是其他数据结构的基础,建议与最小生成树和联通图、割边等相关知识一起学习,将上述七道题均自己手动敲出来则可以熟练掌握该数据结构。