Luogu6279 [USACO20OPEN]Favorite Colors G
Luogu6279 [USACO20OPEN]Favorite Colors G
并查集、启发式合并
这道题说实话挺阴间的。。。
首先,我们把对于一个节点的子节点合并(子节点数大于\(1\)的情况下才进行合并),对应的到达孙子节点的边也需要合并,如果是分层图的形式的话,这么做就直接做完了。
可是这道题奇葩的一点是有环,形成自己膜拜自己的情况(???)。
这样的情况手模一下,就是整颗子树会被合并入同一个集合内。
这里的代码妙不可言。
void dfs(int u)
{
if (e[u].size()<2)
return;
int rt=getf(e[u][0]);
for (int i=1;i<e[u].size();++i)
{
int v=getf(e[u][i]);
if (v==rt)
continue;
f[v]=rt;
if (e[rt].size()<e[v].size())
swap(e[rt],e[v]);
e[rt].insert(e[rt].end(),e[v].begin(),e[v].end());
}
e[u].clear();
e[u].push_back(rt);
dfs(rt);
}
我们每次与第一个子节点进行启发式合并。
当一个节点的儿子中含有自己时,如果\(rt \ne u\),就会扩张其他在子树内的点。
总有一个时刻\(rt=u\),这时注意我们的循环标准,也就是在合并过程中,\(e[u].size()\)会不断扩大,以致覆盖整棵子树,然后最终\(e[u].clear();e[u].push\_back(rt);\),子节点数降为\(1\),停止递归。
当然集合必须被合并到\(e[rt]\)尾部,因为\(rt=u\)时,我们需要遍历这些新的节点,而合并到尾部能够保证这些节点被遍历到。
这也是我们选择第一个子节点,而不是新建节点的理由,因为新建节点可能会导致\(e[u]\)整个集合被搬进新节点集合,从而导致死递归。
\(Code:\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#define N 200005
using namespace std;
int n,m,x,y,cnt;
int vis[N],f[N];
vector<int>e[N];
int getf(int x)
{
return (f[x]==x)?x:(f[x]=getf(f[x]));
}
void dfs(int u)
{
if (e[u].size()<2)
return;
int rt=getf(e[u][0]);
for (int i=1;i<e[u].size();++i)
{
int v=getf(e[u][i]);
if (v==rt)
continue;
f[v]=rt;
if (e[rt].size()<e[v].size())
swap(e[rt],e[v]);
e[rt].insert(e[rt].end(),e[v].begin(),e[v].end());
}
e[u].clear();
e[u].push_back(rt);
dfs(rt);
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;++i)
{
scanf("%d%d",&x,&y);
e[x].push_back(y);
}
for (int i=1;i<=n;++i)
f[i]=i;
for (int i=1;i<=n;++i)
dfs(i);
for (int i=1;i<=n;++i)
{
if (!vis[getf(i)])
vis[getf(i)]=++cnt;
printf("%d\n",vis[getf(i)]);
}
return 0;
}