一:什么是并查集:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并以及查询问题,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定的顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
二:在实际应用中我们用一个数组来存储每个元素所在集合的上层元素是谁,仅有par[x]==x的元素被认为是根元素。每个集合中有且仅有一个根元素,这个根元素就是这个集合的象征,因此我们要查找某个元素a是否在这个集合中,只需要不断向上找,只要找到根元素roota,那么就能知道元素a属于roota所表示的集合。
三:关于并查集的操作:
(1)初始化:开始时有n个独立的点,且他们的根就是他们自己。
void init(int n)
{
for (int i = 1; i <= n; i++)
{
par[i] = i;
}
}
(2)寻找根节点时: 我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次find(x)都是O(n)的复杂度。为了避免这种情况,我们需对路径进行压缩,即当我们经过”递推”找到祖先节点后,”回溯”的时候顺便将它的子孙节点都直接指向祖先,这样以后再次find(x)时复杂度就变成O(1)了,如下图所示。可见,路径压缩方便了以后的查找。
代码如下:
int find(int x)
{
if (par[x] != x)x = find(par[x]);
return par[x];
}
(3)合并x与y的集合:这里我们就是要找到x与y的根节点,然后再将y集合的根节点赋值给x的对应关系数组中。
代码如下:
void unite(int x,int y){ //合并x与y所在集合
x=find(x);
y=find(y);
par[x]=y;
}
(4)判断是否在同一集合:只需要比较根节点即可
代码如下:
bool same(int x, int y)
{
return find(x) == find(y)
}
下面附上一个模板题:
C - 畅通工程 :
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。
Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
Sample Output
1
0
2
998
题目分析:
这道题是一个典型的并查集问题,假设每个连通的城市城市为一个集合,则我们只需要找到所拥有的集合个数再减去一就可以求出还需要修建的道路了。
AC代码如下:
#include<iostream>
#include<cstring>
using namespace std;
const int maxn = 1005;
int par[maxn];
void init(int n)
{
for (int i = 1; i <= n; i++)
{
par[i] = i;
}
}
int find(int x)
{
if (par[x] != x)par[x]= find(par[x]);
return par[x];
}
void unite(int x, int y)
{
x = find(x);
y = find(y);
par[x] = y;
}
bool same(int x, int y)
{
return find(x)==find(y);
}
int main()
{
int n, m;
while (cin >> n)
{
memset(par, 0, sizeof(par));
if (n == 0)break;
init(n);
cin >> m;
int x, y;
for (int i = 1; i <= m; i++)
{
cin >> x >> y;
unite(x, y);//把相互连通的两个城市合并为一个集合
}
int num = 0;
for (int i = 1; i <= n; i++)
{
if (par[i] == i)num++;//找到根节点则有一个集合
}
cout << num - 1 << endl;//集合数减一
}
return 0;
}
E - 食物链:
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
Input
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
Output
只有一个整数,表示假话的数目。
Sample Input
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
Sample Output
3
题目分析:
对于每只动物i创建三个元素A,B,C,并用这3xN个元素建立并查集。
有三种情况:x和y是同一类,x吃y,y吃x,则需在合并之前判断是否产生矛盾就好了。以元素x,x+n,x+2*n分别代表x-A,x-B,x-C。
AC代码如下:
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn = 5e4+5;
int par[3*maxn];
void init(int n)
{
for (int i = 1; i <= n; i++)
{
par[i] = i;
}
}
int find(int x)
{
if (par[x] != x)par[x] = find(par[x]);
return par[x];
}
void unite(int x, int y)
{
x = find(x);
y = find(y);
par[x] = y;
}
bool same(int x, int y)
{
return find(x) == find(y);
}
int main()
{
int n, k;
scanf("%d%d", &n, &k);
init(3 * n);
int ans = 0;
int a, x, y;
for (int i = 0; i < k; i++)
{
scanf("%d%d%d", &a, &x, &y);
if (x>n || y>n || (2 == a&&x == y)){
ans++;
continue;
}
if (a == 1)
{ //x与y为同类
if (same(x, y + n) || same(x, y + 2 * n))
{ //排除x吃y和y吃x的矛盾项的矛盾项
ans++;
}
else{
unite(x, y);
unite(x + n, y + n);
unite(x + 2 * n, y + 2 * n);
}
}
else if (a==2)
{ //x吃y
if (same(x, y) || same(x, y + 2 * n))
{ //排除x与y是同类,和y吃x
ans++;
}
else{//合并区间
unite(x, y + n);
unite(x + n, y + 2 * n);
unite(x + 2 * n, y);
}
}
}
cout << ans << endl;
return 0;
}