并查集删点操作需要借助虚点,实际上也就是一个映射罢了。
通过index[i]表示i点经过映射后的编号,规定访问一个节点时只能通过它的编号index[i]来访问,初始时将各点的index[i]置为i,表示还未进行删点操作时都映射到本身,在删除点i时,将点i映射到一个虚节点上,即index[i] = m(m为虚节点编号),同时father[index[i]] = index[i] (表示删除点i后i点独立),之后所有涉及到点i的操作都用index[i]来代替,即m。
这样的好处在于find()函数不需要修改,且一定能够保证被删点所在连通块依然连通。
例如下图中,删除了2号点,实际上并没有真的删除它,而是把2号点映射到了5号点,之后每次使用2号点都用5号点来代替,而原来的2号节点不可能再被访问到,变成了垃圾节点。
以Junk-Mail Filter HDU - 2473为例,放一个模板:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
//为了删点(例如i点)后不破坏连通性,fa[i]不能改变,因为它是一座桥梁
//index[i]表示将i映射到index[i],index[i]是虚节点下标,每次merge和find都要先把i通过index映射一下
//fa数组多开了一倍,为可能出现的虚节点开辟空间
//这样的好处是merge和find函数都不用改变,只是调用的时候传映射过的值就可以了
int n, m, num;
int fa[1100005], index[100005];
bool vis[1100005];
void init()
{
for(int i = 0; i <= n-1; i++)
fa[i] = index[i] = i;//需要对index也初始化,表示一开始都映射到本身
num = 0;//表示开了几个虚节点
}
int find(int x)
{
if(fa[x] == x)
return x;
return fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
int fx = find(x), fy = find(y);
if(fx != fy)
fa[fx] = fy;
}
void del(int x)//本质上是改变i点的映射
{
fa[n+num] = n+num;//自己独立出来
index[x] = n+num;
num++;
}
signed main()
{
int t = 0;
while(~scanf("%d%d", &n ,&m))
{
t++;
if(n == 0 && m == 0)
break;
init();
char op[2];
int a, b;
for(int i = 1; i <= m; i++)
{
scanf("%s", op);
if(op[0] == 'M')
{
scanf("%d%d", &a, &b);
merge(index[a], index[b]);
}
else
{
scanf("%d", &a);
del(a);//这里不可以再取index了,因为index是对原始的n个数映射
}
}
int ans = 0;
memset(vis, false, sizeof vis);
for(int i = 0; i <= n-1; i++)//只能这么求,统计有多少个不同的根节点
{
if(!vis[find(index[i])])
ans++, vis[find(index[i])] = true;
}
//求连通块数,假如只看fa[x]==x,1->2->3,分别删1,2,3,会有问题
// for(int i = 0; i <= n-1; i++)//也不能这样求,比如1->2,删根节点2,会有错误
// if(fa[index[i]] == index[i])
// ans++;
printf("Case #%d: %d\n", t, ans);
}
return 0;
}