目录
题意
食物网中,灭绝一类生物,若其余生物没有了食物来源,则该生物一并死亡。将灭绝一类生物所导致的其他生物死亡种类数称为“灾难值”,求每类生物的灾难值。
数据范围:1≤N≤65534
思路
初看题目标签:“并查集”“拓扑排序”,你可能会一脸疑惑。不要着急,让我们慢慢来看。
如果直接模拟某一类生物死亡,则时间复杂度为,(……)就会爆炸。
(小黑子树枝666)
于是我们要反向思考——若一个生物要灭绝,那么要满足怎么样的条件呢?显然,是当它的食物全部灭绝的时候。如果它的食物只有一种,那还好说。但是你看看那这题目怎么可能会这么善良(看样例即可),其中4就有两种食物。
高潮一
所以,要想让X灭绝,必须让X的食物全部灭绝。要想让X的食物全部灭绝,又只能人为灭掉一类生物,所以,就要让他们的公共祖先灭绝!!!
所以,问题就转化为了如何求多个节点的最近公共祖先LCA。(怎么样,是不是和题目标签呼应上了?)
可是,这不一定是一棵树,也许这些食物也有多个食物呢?难道没完没了?
高潮二
所以,我们迎来了全篇的第二个高潮:构造一棵灭绝树。既然树简单,那就把它变成树!我们定义,若x灭绝后,y也会灭绝,则在灭绝树中x是y的祖先(不一定是父亲)。此时,在原来的图中,必然满足x是y的所有食物的公共祖先。特别的,若x是y的食物的LCA,则在灭绝树中x是y的父亲。所以,我们只需要找到原图中y的食物的LCA,就能构造出整棵树。而对于每个节点,以其为根的子树的大小-1就是答案(想不明白多看两遍)
(说了半天,你还是没说怎么在一张有向无环图中求LCA啊!)
高潮三
这不,第三个高潮来了!如何在图中求LCA呢?这就要借助topsort(拓扑排序)和我们刚刚构造的灭绝树了。倘若你感觉不错,就能敏锐地察觉到:这张图好像topsort啊!是的,我们需要topsort来确定节点进灭绝树的顺序。为什么?假设a吃了b,则从a到b建立一条有向边。如一个节点x已经没有前驱了,则说明x的食物都已经加入灭绝树了。这时候x才可以加入灭绝树。这恰恰是topsort的思想(怎么样,又呼应上了)。既然x的食物都在灭绝树中了,灭绝树又是一棵标准的数,那么求LCA就如探囊取物,易如反掌了。
小坑点
最后,有个注意点:这张图可能不联通,所以需要一个超级源点将原本所有入度为0的点都收作儿子。这样就能避免混乱。
代码
#include <iostream>
#include <stdio.h>
#include <vector>
#include <queue>
using namespace std;
const int N=7e4;
int n;
int cnt[N];//cnt[i]表示节点i的父亲(食物)的数量
vector<int>side[N];//指向儿子(即吃i的生物)
vector<int>fath[N];//表示父亲(即i的食物)
vector<int>s2[N];//这是灭绝树的边
//用vector存边,单纯就是懒
int fa[N][15],depth[N];//用来维护LCA
int ans[N];//最后答案,全面解析已经说过了
queue<int>q;//topsort的queue
inline int lca(int x,int y){//经典LCA
if(x==y) return x;
if(depth[x]<depth[y]) swap(x,y);
for(int j=14;j>=0;j--){
if(depth[fa[x][j]]>=depth[y]) x=fa[x][j];
}
if(x==y) return x;
for(int j=14;j>=0;j--){
if(fa[x][j]!=fa[y][j]){
x=fa[x][j];
y=fa[y][j];
}
}
return fa[x][0];
}
inline void dfs(int x){//求子树的大小
ans[x]=1;
for(int i=0,len=s2[x].size();i<len;i++){
dfs(s2[x][i]);
ans[x]+=ans[s2[x][i]];
}
}
signed main(){
scanf("%d",&n);
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
if(!x) side[0].push_back(i),fath[i].push_back(0)/*i的父亲包括0*/,cnt[i]++;//0的儿子包括i
else while(x) side[x].push_back(i),fath[i].push_back(x)/*i的父亲包括x*/,cnt[i]++,scanf("%d",&x);//x的儿子包括i
}
//开始topsort
q.push(0);
while(q.size()){
int now=q.front();
q.pop();
for(int i=0,len=side[now].size();i<len;i++){
int to=side[now][i];
cnt[to]--;
if(cnt[to]==0){//将该点加入灭绝树
int father=fath[to][0];
for(int i=1,len=fath[to].size();i<len;i++)//求其食物在灭绝树中的LCA
father=lca(father,fath[to][i]);
s2[father].push_back(to);//直接成为LCA之子,既维护树的性质,又将其将其加入了树中
depth[to]=depth[father]+1;//记录深度和祖先
fa[to][0]=father;
for(int j=1;j<15;j++)
fa[to][j]=fa[fa[to][j-1]][j-1];
q.push(to);//不要忘记将其加入topsort的queue中
}
}
}
dfs(0);
for(int i=1;i<=n;i++){//输出答案
printf("%d\n",ans[i]-1);
}
printf("\n");
return 0;
}