题目链接: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; }