本文参考了《挑战程序设计竞赛》和Jennica的github题解代码
模板
数组版:
int parent[MAX_N];
int rank[MAX_N];
void Init(int n){
for(int i = 0; i < n; ++i){
parent[i] = i;
rank[i] = 0;
}
}
int Find(int x){
if(parent[x] = x){
return x;
} else {
return Find(parent[x]);
}
}
void Union(int x, int y){
x = Find(x);
y = Find(y);
if(x == y)
return;
if(rank[x] < rank[y]){
parent[x] = y;
} else {
parent[y] = x;
if(rank[x] == rank[y]) rank[x]++;
}
}
结构体版:
struct Node
{
int rank;
int parent;
} node[MAX_N];
void Init(int n){
for(int i = 0; i < n; ++i){
node[i].parent = i;
node[i].rank = 0;
}
}
int Find(int x){
if(node[x].parent = x){
return x;
} else {
return Find(node[x].parent);
}
}
void Union(int x, int y){
x = Find(x);
y = Find(y);
if(x == y)
return;
if(node[x].rank < node[y].rank){
node[x].parent = y;
} else {
node[y].parent = x;
if(node[x].rank == node[y].rank) node[x].rank++;
}
}
例题
例1 POJ 1182(带权并查集/种类并查集)
解题思路:
这个题是非常经典的并查集问题(种类并查集)。并查集作用:查询a和b是否属于同一组;合并a和b所在的组。注意并查集无法进行分割操作。利用题目中动物之间的相对关系可以建立一个并查集。对每一个元素,把它对父节点的关系用数组rank[i]表示,即relation,作为权值。0:与父节点同类 1:吃父节点 2:被父节点吃。
路径压缩。为了使查找效率提高,我们需要使树的高度尽可能小,让它只有一层,即在查找的过程中,把树中一条路径上的所有点都连在根节点上,从而加快了查找速度,与此同时,还要更新与此节点有关的其他变量,比如此节点与父节点的权值(父节点变为根节点),该树的高度等等。对于此题来讲,就是要更新节点与父节点的关系,因为此节点的父节点已经变为根节点。那么这里如何推导出此节点与根节点的关系呢。假设此节点为x,其父节点为y,y的父节点为z。则:
rank[x] rank[y] x与z的关系权值
0 0 0=(i + j)%3
0 1 1=(i + j)%3
0 2 2=(i + j)%3
1 0 1=(i + j)%3
1 1 2=(i + j)%3
1 2 0=(i + j)%3
2 0 2=(i + j)%3
2 1 0=(i + j)%3
2 2 1=(i + j)%3
推导公式:rank[x] = (rank[x] + rank[y]) % 3; 即对于i节点,更新公式为:rank[i] = (rank[i] + rank[parent[i]]) % 3。不过还有更简便的方法:模拟向量的运算x->z = x->y + y->z,所以rank[x] = (rank[x] + rank[y])% 3。对3取模是为了保证结果为0,1,2。
最后是集合的合并操作。合并操作并不复杂,复杂的是更新集合间的关系(即集合根节点的关系)。这里有大神使用向量的形式来计算更新关系权值,假设需要合并x和y,查询得知x和y的根节点分别为:x_p,y_p,如果将x_p连接到y_p,那么rank[x_p]即为x_p与根节点y_p的关系。x_p->y_p = x_p->x + x->y + y->y_p = (-x->x_p + x->y + y->y_p)。所以更新公式为rank[x_p] = (-rank[x] + type - 1 + rank[y] + 3) % 3。(加上3是为了防止出现负数的情况;对3取模是为了保证结果的取值范围)。type即为输入的num。type为1,x与y同类,type为2,x吃y。
Solution:from 专注如一
#include <cstdio.h> #include <cstring> #include <algorithm> using namespace std; const int MAX_N = 50000 + 10; int parent[MAX_N], rank[MAX_N]; void Init(int n){ for(int i = 0; i <= n; ++i){ parent[i] = i; rank[i] = 0; } } int Find(int x){ if(parent[x] == x){ return x; } int y = Find(parent[x]); rank[x] = (ran[x] + ran[parent[x]]) % 3; return parent[x] = y; } int Union(int x, int y, int type){ x_p = Find(x); y_p = Find(y); if(x_p == y_p){ if((rank[x] - rank[y] + 3) % 3 == type - 1) return 0; return 1; } parent[x_p] = y_p; rank[x_p] = (-rank[x] + type - 1 + rank[y] + 3) % 3; return 0; } int main(){ int n, k, res = 0; int type, x, y; scanf("%d %d", &n, &k); Init(n); for(int i = 0; i < k; ++i){ scanf("%d %d %d", &type, &x, &y); if(x == y && type == 2) ++res; else if(x > n || y > n) ++res; else res += Union(type, x, y); } printf("%d\n", res); return 0; }
此题还有一种解法。对于每只动物i创建3个元素i->A,i->B,i->C,并用这3*N个元素建立并查集。i->x表示动物i属于种类x;并查集里的每一个组内表示组内所有元素代表的情况都同时发生或不发生。例如i->A和y->B属于一组,那么就说明如果i属于A那么j一定属于B,相同的如果j属于B那么i一定属于A。
n = 1: x和y属于一类,那么合并x->A和y->A,x->B和y->B,x->C和y->C。
n = 2: x吃y 那么合并x->A和y->B,x->B和y->C,x->C和y->A。
另外要注意的是在合并之前要检查合并操作是否会产生矛盾。
Solution 2
#include <cstdio.h> #include <cstring> #include <algorithm> using namespace std; const int MAX_N = 50000 * 3 + 5;
//元素x,x + n,x + 2 * n分别代表x->A,x->B,x->C int parent[MAX_N], rank[MAX_N]; void Init(int n){ for(int i = 0; i <= n; ++i){ parent[i] = i; rank[i] = 0; } } int Find(int x){ if(parent[x] == x){ return x; } return parent[x] = Find(parent[x]); } bool same(int x, int y){ return Find(x) == Find(y); } void Union(int x, int y, int type){ x = Find(x); y = Find(y); if(x == y){ return; } if(rank[x] < rank[y]){ parent[x] = y; } else { parent[y] = x; if(rank[y] == rank[x]) rank[x]++; } } int main(){ int n, k, res = 0; int type, x, y; scanf("%d %d", &n, &k); Init(n * 3); for(int i = 0; i < k; ++i){ scanf("%d %d %d", &type, &x, &y); if(x == y && type == 2 || (x > n || y > n)){ ++res; continue; } if(type == 1){ if(same(x, y + n) || same(x, y + 2 * n)){ res++; continue; } else { Union(x, y); Union(x + n, y + n); Union(x + 2 * n, y + 2 * n); } } else { if(same(x, y) || same(x, y + 2 * n)){ res++; continue; } else { Union(x, y + n); Union(x + n, y + 2 * n); Union(x + 2 * n, y); } } } printf("%d\n", res); return 0; }
例2 POJ 2236
解题思路:
典型的并查集问题,较上一题简单。思路就是开辟两个数组,一个用来保存父节点,一个用来保存状态:是否被修复。假设现在修复电脑p,那么p状态就更改为已修复。然后在所有电脑中遍历一遍,只寻找满足以下条件的电脑:1. 已修复电脑(假设为q) 2. p和q 距离小于最大距离 3. p和q根节点不同(即不属于同一棵树),那么就合并p和q所在的树。查询是否可以通讯就很简单了,如果p和q的根节点相同,那么就可以通讯,否则通讯失败。这里要注意并查集不是只有一棵树,而是一个森林,包含了很多树。
Solution :
#include <cstdio.h> #include <cstring> #include <algorithm> using namespace std; const int MAX_N = 1000 + 10; int x[MAX_N], y[MAX_N]; int parent[MAX_N]; bool fixed[MAX_N]; int n, d; bool close(int a, int b){ return (x[a] - x[b]) * (x[a] - x[b]) + (y[a] - y[b]) * (y[a] * y[b]) <= d * d; } void Init(int n){ for(int i = 1; i <= n; ++i){ parent[i] = i; fixed[i] = 0; } } int Find(int a){ if(parent[a] == a){ return a; } return parent[a] = Find(parent[a]); } void Union(int a, int b){ a = Find(a); b = Find(b); parent[a] = b; } int main(){ scanf("%d%d", &n, &d); Init(n); for(int i = 1; i <= n; ++i){ scanf("%d%d", &x[i], &y[i]); } char op[5]; int a, b; while(scanf("%s", op) != EOF){ if(op[0] == 'O'){ scanf("%d", &a); if(!fixed[a]){ fixed[a] = true; for(b = 1; b <= n; ++b){ if(!fixed[b]) continue; if(Find(a) == Find(b)) continue; if(close(a, b)){ Union(a, b); } } } } else { scanf("%d%d", &a, &b); if(Find(a) == Find(b)){ printf("SUCCESS\n"); } else { printf("FAIL\n"); } } } return 0; }
例3 POJ 1703(种类并查集)
解题思路:
poj1182的简化版,模板一套就可以了。
Solution
例4 AOJ 2170