并查集
并查集:合并,查找,集合。
每个集合都有至少一个元素,其中有一个代表该集合,以下简称老大,能通过查找的方法能找到集合的老大,集合只能两两合并,合并后老大会变成一个。
用一个数组par来应用并查集,一开始所有元素都是各自独立的,每个元素自成一个集合,再通过合并进行操作。
把集合看做帮派,代表元素看做老大,其他元素看做成员。
int par[11]; void init()///初始化:此前先定义全局数组int par[11]; { for(int i=0;i<11;i++) par[i]=i; } int find1(int x) { if(x==par[x]) return x; else return par[x]=find1(par[x]); } void unite(int x,int y) { int xx=find1(x); int yy=find1(y);///x老大的下标是xx;y老大的下标是yy if(xx!=yy)///老大的下标和内容永远一样 par[xx]=yy;///认yy做老大,把x的老大xx的内容改成yy,改后下标和内容不同,xx不再是老大了。 ///par[yy]=xx;//认xx做老大 }
比如par数组
下标 0 1 2 3 4 5 6 7 8 9 10
内容 0 1 2 2 4 5 3 7 6 9 10
明显数组下标和内容不一样的有3 6 8;其实他们早已被合并了,先看如何查找老大。
查找3的老大:
find1(3)
{
return par[3]=find1(par[3]=2)
{
return 2;
}
}
查找6的老大:
find1(6)
{
return par[6]=find1(par[6]=3)
{
return par[3]=find1(par[3]=2);
{return 2;}
}
}
查找8的老大:
find1(8)
{
return par[8]=find1(par[8]=6);
{
return par[6]=find1(par[6]=3);
{
return par[3]=find1(par[3]=2);
{return 2;}
}
}
}
数组下标就是成员,数组内容就是成员的老大,8的老大是6,6的老大是3,3的老大是2,2的老大是2,依次查找,所以2,3,6,8属于一个集合,2是这个集合的老大。
并查集只能两两合并。
接下来实际运用一下。
城市交通线
Time Limit: 2000/1000ms (Java/Others)
Problem Description:
A国有n座城市,编号为1~n,这n个城市之间没有任何交通线路,所以不同城市的居民之间不能互通,所以A国的人打算为这n个城市建立k条交通线
(两座城市之间可以有两条及以上的线路),交通线只能贯穿两座城市p, q(存在p=q的情况,不用考虑但会占用一条交通线), 建立交通线后两
座城市的居民可以互通。 例如:建立1, 4后再建立4, 2,则1和2是互通的(1—>4—>2)。建立完k条交通线后,接着有m次询问,问居住在两个城市a, b的居民能否互通。
Input:
输入数据有多组,每一组的第一行包含两个整数n (0<n<=5000), k (0<k<=10000) , 接下来有k行,每行包含两个整数p,q (0<p,q<=n),
代表城市p,q之间建立一条交通线。再接着一个整数m (0<m<=n) 代表有m次询问,接下来有m行,每行包含两个整数a, b(0<a,b<=n) 询问
城市a,b之间的居民能否互通 (同一个城市的居民能互通)。
Output:
两座城市的居民可以互通则输出“YES”,否则输出“NO”,每个输出占一行。
Sample Input:
6 5 1 4 4 2 2 4 5 6 4 1 4 1 2 1 3 1 6 4 4
Sample Output:
YES NO NO YES
题目来源:http://acm.gdufe.edu.cn/Problem/read/id/1505
代码:
#include<bits/stdc++.h> using namespace std; int par[5050]; void init() { for(int i=0;i<5050;i++) par[i]=i; } int find1(int x) { if(x==par[x]) return x; else return par[x]=find1(par[x]); } void unite(int x,int y) { int xx=find1(x); int yy=find1(y); if(xx!=yy) par[xx]=yy; } int main() { int n,k,q; while(cin>>n>>k)///城市个数,交通线数 { init(); int x,y; while(k--) { cin>>x>>y; unite(x,y); } cin>>q; ///询问个数 int a,b; while(q--) { cin>>a>>b; if(find1(a)==find1(b))///老大相同就是可以互通的 cout<<"YES\n"; else cout<<"NO\n"; } } return 0; }
How Many Tables
One important rule for this problem is that if I tell you A knows B, and B knows C, that means A, B, C know each other, so they can stay in one table.
For example: If I tell you A knows B, B knows C, and D knows E, so A, B, C can stay in one table, and D, E have to stay in the other one. So Ignatius needs 2 tables at least.
InputThe input starts with an integer T(1<=T<=25) which indicate the number of test cases. Then T test cases follow. Each test case starts with two integers N and M(1<=N,M<=1000). N indicates the number of friends, the friends are marked from 1 to N. Then M lines follow. Each line consists of two integers A and B(A!=B), that means friend A and friend B know each other. There will be a blank line between two cases.
OutputFor each test case, just output how many tables Ignatius needs at least. Do NOT print any blanks.
Sample Input
2 5 3 1 2 2 3 4 5 5 1 2 5Sample Output
2 4
题意:生日聚会上,认识的人坐一起。如果A和B认识,坐一张桌子上,如果B又和C认识,可以认为A和C也认识,坐一起。求最后有多少张桌子?
思路:这也是一道裸题,通过并查集将这些人联系在一起,每张桌子看做一个集合,总会有一个老大,查找老大数量就是桌子数量。
代码:
#include<stdio.h> #include<algorithm> #include<iostream> using namespace std; int par[5050]; void init() { for(int i=0;i<5050;i++) par[i]=i; } int find1(int x) { if(x==par[x]) return x; else return par[x]=find1(par[x]); } int unite(int x,int y) { int xx=find1(x); int yy=find1(y);//x老大的下标是xx;y老大的下标是yy if(xx!=yy)//老大的下标和内容永远一样 par[xx]=yy;//认yy做老大 return 0; } int main()//裸题 { int t,n,k,sum,flag; cin>>t; while(t--){ while(cin>>n>>k) { sum=0; init(); int x,y; while(k--) { cin>>x>>y; unite(x,y); } for(int i=1;i<=n;i++) if(par[i]==i)///老大下标和内容一样 sum++; cout<<sum<<"\n"; } } return 0; }
再看一道变相并查集的题目。(题目不好复制粘贴,直言题意了)
题意:每次输入两个数字,用一条边把他们联系起来,最后求环的个数,比如:
像这样的图,有2个环,(15,5,9,11)和(7,10,16);
解题思路:每合并两个顶点,他们的边数加1,最后,环中都是两条边的顶点符合要求。
代码:
#include<iostream> #include<stdio.h> using namespace std; int par[1000100]; int bian[1000100]; int temp[1000100]; void init(int x) { for(int i=1;i<=x;i++) {par[i]=i;bian[i]=0;}///一开始边数都是0 } int find1(int x) { if(x==par[x]) return x; else return par[x]=find1(par[x]); } int unite(int x,int y) { int xx=find1(x); int yy=find1(y); if(xx!=yy) par[xx]=yy; return 0; } int main() { int n,k,boss; while(cin>>n>>k)///点数,关系数 { boss=0; init(n); while(k--) { int a,b; cin>>a>>b; bian[a]++; bian[b]++; unite(a,b); } for(int i=1;i<=n;i++) temp[i]=par[i];///复制一个合并后数组的副本,等下修改数据 for(int i=1;i<=n;i++) if(bian[i]!=2)///如果这个节点边数不为2 temp[find1(i)]=-1; ///把这个节点的老大废掉,改成-1,改动是在副本里改动,查找在原数组里查找, ///如果是在原数组里改动,有些成员的老大被改成-1了,不好继续查找 for(int i=1;i<=n;i++) if(temp[i]==i)///老大还没废掉的说明它的所有成员符合要求,只有两条边 boss++; cout<<boss<<endl; } return 0; }