看题传送门:http://acm.hdu.edu.cn/showproblem.php?pid=2473
这题可以说是经典了,涉及了并查集的删除操作。由于并查集是树状的结构,导致不能直接简单删除一个一个节点而又不影响其他节点的连通性。前人给出的解决方法是使用虚拟节点,一种类似于金蝉脱壳的思路,直接增加一个新的集合。
具体的操作方面,可以开辟两个n长度的数组,也可以直接用一个2 * n + m的数组,我用的是第一种方法。使用id数组来代表每个元素现在所处的真实下标,对元素的所有操作,实际上是对id数组里的下标进行操作。当下标为 i 的元素被删除时,实际上可以看成先在原地建造一个“影分身”,然后在把本体移动到 n + 1 下标的位置,以后调用 id[i] 时,实际上操作的是 n + 1。
先上代码。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = 1200000;
int fa[MAXN], id[MAXN], Rank[MAXN], flag[MAXN];
void init(int n){
for(int i = 0;i < n;++i){
fa[i] = i;
id[i] = i;
Rank[i] = 0;
}
}
int Find(int x){
return fa[x] == x? x : fa[x] = Find(fa[x]);
}
void Union(int x, int y){
int faX = Find(x), faY = Find(y);
if(faX != faY){
if(Rank[faX] < Rank[faY]){
int temp = faY;
faY = faX;
faX = temp;
}
fa[faY] = faX;
if(Rank[faX] == Rank[faY])
Rank[faX]++;
}
}
int main(){
//freopen("input.txt", "r", stdin);
int n, m, cnt = 0;
while(~scanf("%d %d", &n, &m) && (n || m)){
int num = n;
getchar();
init(n);
for(int i = 0;i < m;++i){
char ch = getchar();
if(ch == 'M'){
int a, b;
scanf("%d %d", &a, &b);
getchar();
Union(id[a], id[b]);
}
else{
int a;
scanf("%d", &a);
getchar();
id[a] = num;
fa[num] = num;
Rank[Find(a)]--;
num++;
}
}
memset(flag, 0, sizeof(flag));
int ans=0, a;
for(int i=0; i<n; ++i){
a=Find(id[i]);
if(!flag[a]){
++ans;
flag[a]=1;
}
}
/*
int ans = 0;
for(int i = 0;i < n;++i){
if(Find(id[i]) == id[i])
ans++;
}*/
printf("Case #%d: %d\n", ++cnt, ans);
}
}
这个题WA了很多次……主要还是因为对并查集的结构没有完全理解。最后注释掉的那一段是WA的原因,如果用这种原始的方式寻找答案的话,看似没有任何问题,但是却又非常大的纰漏。这种方式其实漏掉了一种可能,即删除的是并查集的根节点,缺少实体根节点的并查集的Find( id[i] ) 肯定不再是 id[i] ,比如删除了根节点 1 节点,1 节点分身成了 5 节点,这时 Find( id[1] ) == 5,原来的那棵树的根节点如果再进行Find也会等于5,导致这种方法失效,原来的那棵树的根节点是 1 节点的分身,但Find(1)不再等于 1 了。
正确的寻找连通分量的方法是寻找每一个节点的祖先节点,然后用一个vis或flag数组统计是否曾经到达过,顺便统计数量就OK了。