并查集
这种数据结构用来表示集合信息,实现如确定某个集合含有哪些元素、判断某两个元素是否存在同一个集合中、求集合中元素的数量等问题。
也就是用一个树来表示集合关系。
图片内容摘自《王道》。
并查集一般有几个基本操作:
- 寻找结点的根,在过程中进行路径压缩
- 合并两个集合
- 判断是不是同一集合
我们一般用一个数组表示下标对应结点及其根节点的关系。
const int maxn = 5000;
int Tree[maxn];
void init(int n){ //初始化每个结点的根节点是自己
for(int i = 0; i <= n;++i){
Tree[i] = i;
}
}
//寻找根节点
int findRoot(int x){
if(Tree[x] != x)
Tree[x] = findRoot(Tree[x]); //路径压缩
return Tree[x];
}
//合并两个集合
void unite(int x,int y){
int fx = Tree[x];
int fy = Tree[y];
if(fx != fy){
Tree[fx] = fy;
}
return;
}
bool same(int x,int y){
return findRoot(x) == findRoot(y);
}
在此基础上,为了在合并结点时防止数的退化,我们还可以加入按秩合并的方法,也就是用一个数组保存树深,把较矮的树合并到较高的树上去。
void unite(int x,int y){
int fx = Tree[x];
int fy = Tree[y];
if(fx != fy){
if(deep[fx] < deep[fy]) //deep数组保存树深,合并矮树
Tree[fx] = fy;
else{
Tree[fy] = fx;
if(deep[fx] == deep[fy])
deep[fx]++;
}
}
return;
}
简单的并查集可以解决无向图的最小生成树Krusal算法,给节点排序后不断的将新节点合并进来,在合并的过程中计算权值和,直到所有结点都在同一集合。
同样的思想,可以解决至少加几条路才能使一个无向图强连通,计算有几个集合,道路条数等于集合数减一。
种类并查集
下面我们来聊聊种类并查集,以一个例题开始:
POJ 2942 寻找GAY虫
Description
Professor Hopper is researching the sexual behavior of a rare species of bugs. He assumes that they feature two different genders and that they only interact with bugs of the opposite gender. In his experiment, individual bugs and their interactions were easy to identify, because numbers were printed on their backs.
Problem
Given a list of bug interactions, decide whether the experiment supports his assumption of two genders with no homosexual bugs or if it contains some bug interactions that falsify it.
Input
Output
Sample Input
2 3 3 1 2 2 3 1 3 4 2 1 2 3 4
Sample Output
Scenario #1: Suspicious bugs found! Scenario #2: No suspicious bugs found!
Hint
题目大意就是给出虫子的依恋关系,一般来说应该是异性恋,但是有一种特殊的虫子是同性恋,让你通过给出的关系找找有米有GAY虫。
很明显我们可以定义出两个集合为两个性别,如果我们发现有依恋关系的两个虫子属于同一个集合,那么就发现了GAY虫。思路上是不难理解的。
在代码方面,我们把一个数组分成两个部分,前半部分存放雄性,后半部分存放雌性。也就是说我们会定义一个大小为
2
n
2n
2n的数组。
先给出代码:
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
const int maxn = 5000;
int par[maxn];
void init(int n){
for(int i=0;i<=n;i++)
par[i]=i;
}
int find(int x){
if(x!=par[x])
par[x]=find(par[x]);
return par[x];
}
void Union(int a,int b){
int x=find(a);
int y=find(b);
if(x!=y)
par[x]=y;
}
bool judge(int x,int y){//判断是否为同性,异性为真,同性为假
x=find(x);
y=find(y);
if(x!=y)
return true;
return false;
}
int main()
{
int t,n,m,cas=1;
scanf("%d",&t);
for(int i=1;i<=t;i++){
scanf("%d%d",&n,&m);
init(n*2);
bool l=1;
int x,y;
while(m--){
scanf("%d%d",&x,&y);
if(judge(x,y)||judge(x+n,y+n)){
Union(x,y+n);//把同性的加到一个集合里
Union(x+n,y);
}
else
l=0;
}
if(i!=1)
puts("");
printf("Scenario #%d:\n",cas++);
if(l)
printf("No suspicious bugs found!\n");
else
printf("Suspicious bugs found!\n");
}
return 0;
}
大家可能会对这一段代码产生迷惑:
先聊聊一个简单的推理:有虫子ABC,A love B ,能推理出A和B应当互为异性,现在又知道了B love C,能推理出B和C互为异性,还能推理出A 和 C为同性。
观察一下每个虫子的同性和异性集合,发现具有依恋关系的两个虫子AB,A的同性集是B的异性集,所以在有新的关系产生的时候,就要去更新各自的同异性集合。
当然,当我们发现这样的情况时:
毫无疑问,这三只虫子中一定有一个是GAY!不信的话,可以比较一下同异性集合XD
(以上图中应该都是无向边)
下面这道题相对难一些啦,但本质上也是种类并查集的问题,只不过这里会出现三个集合:同类,吃,被吃。
POJ 1182 食物链
Description
现有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
以下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
附上代码:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;int n,k;
const int MAXN = 100005;
int Tree[3*MAXN];
int findRoot(int x){
if(Tree[x] == x)return x;
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
void merge(int x,int y){
int a = findRoot(x);
int b = findRoot(y);
if(a != b){
Tree[a] = b;
}
}
bool same(int x,int y){
return findRoot(x) == findRoot(y);
}
int main(){
scanf("%d%d", &n, &k);
for(int i = 0; i < n * 3;++i) Tree[i] = i;
int ans = 0;
for(int i = 0; i < k;++i)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
int tag = a;
int x = b - 1;
int y = c - 1;
if(x < 0 || x >= n || y < 0 || y >= n)
{
ans++;
continue;
}
if(tag == 1){
if(same(x,y+n) || same(x ,y+ 2 * n)){
ans++;
}
else{
merge(x,y);
merge(x+n,y+n);
merge(x+2*n,y+2*n);
}
}
else{
if(same(x,y) || same(x,y+2*n)){
ans++;
}
else{
merge(x,y+n);
merge(x+n,y+2*n);
merge(x+2*n,y);
}
}
}
printf("%d\n",ans);
return 0;
}