题目链接:Click here~~
题意:
给 n 个独立的节点,定义一种具有传递性的关系,两种操作:
1、合并,即合并 u 和 v 各自所在的集合。
2、分离,将 u 从之前的集合中分离出来,并将其作为一个新的独立节点。
最后找出 n 个节点一共存在于多少个集合中。
解题思路:
从合并操作来看,很容易想到并查集,但是我们在学习并查集时,知道传统的并查集是不支持分离操作的。如何完成这一操作呢?
想办法调整策略,使得分离操作不悖于之前的合并操作,并且对之后的合并操作也不产生影响。
先回顾一下问题,然后我们考虑将一个已经存在于一个集合的节点 u 分离出来,会对并查集产生什么影响呢?
如果什么都不处理,无脑分离,直接将 u 的根指向自己的话,那些将根指向 u 的节点没有得到更新,而且我们也没有什么好的方法去快速更新它们。
网上的题解大都分为两种吧。
先讲解下方法一,也是我采用的方法:分离时,舍弃掉那些被分离的点,直接为并查集引入一个新的节点。
“舍弃” 的意思,是不再处理那个被分离的点与其他节点的关系。
举个例子,假如一共有 4 个节点 V = {1,2,3,4},分为两个集合 S1 = {1,2,3},S2={4}。
现在将节点 3 分离,我们保留这个节点 3,不去管它,而是引入一个新的节点 5 代替节点 3,此时,S1 = {1,2,3},S2 = {4},S3 = {5}。
之后所有关于节点 3 的操作全部去对于节点 5 进行。如果节点 5 又被删除的话,那么同样的方法再继续引入新的节点即可。
方法二:为每个节点引入一个虚拟点,作为它的父亲,由并查集合并操作的特点,它将一直是树中的叶子节点,而对于所有集合的根节点一直是虚拟点。分离时,直接将这个节点指向另一个新的独立的虚拟点即可。
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int N = 1e6 + 1e5 + 5;
namespace ufSet
{
int pre[N],map[N],top;
void init(int n){
memset(pre,-1,sizeof(pre));
for(int i=0;i<n;i++)
map[i] = i;
top = n;
}
int root(int u){
return pre[u] == -1 ? u : pre[u] = root(pre[u]);
}
bool gather(int u,int v){
int r1 = root(u);
int r2 = root(v);
if(r1 == r2)
return false;
else
pre[r1] = r2;
return true;
}
void apart(int u){
map[u] = top++;
}
}using namespace ufSet;
bool vis[N];
int main()
{
int n,q,ncase = 0;
while(~scanf("%d%d",&n,&q),n||q)
{
memset(vis,false,sizeof(vis));
init(n);
while(q--)
{
char op[4];
int u,v;
scanf("%s",op);
if(op[0] == 'M')
{
scanf("%d%d",&u,&v);
gather(map[u],map[v]);
}
else
{
scanf("%d",&u);
apart(u);
}
}
for(int i=0;i<n;i++)
vis[root(map[i])] = true;
int ans = 0;
for(int i=0;i<top;i++)
if(vis[i])
++ans;
printf("Case #%d: %d\n",++ncase,ans);
}
return 0;
}