题目传送门:【BZOJ 2815】
题目描述:【BZOJ 2815 题面】
题目大意: 我们用一种叫做食物网的有向图来描述生物之间的关系:
一个食物网有 N 个点,代表 N 种生物,如果生物 x 可以吃生物 y,那么从 y
向 x 连一个有向边。
这个图没有环。
图中有一些点没有入边,这些点代表的生物都是生产者,可以通过光合作用来生存; 而有入边的点代表的都是消费者,它们必须通过吃其他生物来生存。如果某个消费者的所有食物都灭绝了,它会跟着灭绝。
一个图里面可能会有多个生产者。
我们定义一个生物在食物网中的“灾难值”为,如果它突然灭绝,那么会跟着一起灭绝的生物的种数。现在,我们给定了一个食物网,你要求出每个生物的灾难值。
题目分析:
这道题和我们校内的一道模拟题十分相似,只不过那道题是求从源点到目标点的“关键点”个数,这里是求一个点是多少个其他点的“关键点”。
分析题意。如果我们只考虑整个图是一棵树的情况,那么显然,对于任意一个点 ,它的灾难值就是它的子树大小-1(子树当然是要包括自己的)。但是这里,它并不是一棵树,而是一个 DAG(有向无环图)。
DAG 图和树形图有许多的共性,它们也有很多相似之处。到了这里,我们可能会想:应该怎么样才能去把 DAG 图的“食物网”关系给弄出来呢?或者,我们能不能把 DAG 图转化为一棵树,然后再去求它的子树大小,进而得到答案呢?
幸运的是,这样做是可行的。我们可以利用整个图的拓扑关系,通过拓扑排序的方式遍历整个图。在遍历整个图的时候,我们发现:如果有这么一个点 q,它可以由其他多个点 p 1 ,p 2 ,…… p n 遍历到,那么,它只会直接受到所有的这些点的 LCA 所影响(即:只有 LCA( p 1 ,p 2 ,…… p n ) 的灭绝,才会使得点 q 代表的生物灭绝)。
所以,在进行拓扑排序的时候,我们需要记录下将要遍历到的那个点的 father(即:这个 father 是它的最近“关键点”,当这个 father 灭绝的时候,它也会跟着灭绝);如果它已经被记录下了一个 father,那么我们就尝试去更新它,最后得到的就是它的直接 father。同时,当一个节点得到了直接 father 时,我们就对它的直接 father 和这个点连一条有向边。最后,当整个拓扑排序结束时,DAG 图就被转化成了一个树形图。
这样的树形图又被叫做“支配树”,它就是由一系列变换而得到的一棵新树。对于这道题,由于原图仅仅是一个 DAG,所以生成支配树的方式有所不同,我们只需要考虑原图的拓扑关系和点与点之间的关系即可。
对于新的树形图,我们再跑一遍 DFS,就可以求出每个点的子树大小。此时我们就把原问题转化成了树形图上的问题。
但是这里有一个细节:一个图里面可能会有多个生产者,这就意味着,原图进行变换之后,得到的不一定是一棵树,而是一片森林(多棵树)。对于这种情况,在原图中,我们新加入一个超级源点,然后把超级源点和最开始时所有入度为 0 的点相连,在拓扑排序之后,一定就能得到一棵树了。然后我们以超级源点为根,再来跑 DFS 即可得到正确答案。
下面附上代码:
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
- #include<queue>
- using namespace std;
- typedef long long LL;
- const int MX=70005;
- const int P=16;
- struct Edge{
- int to,next;
- }edge[MX*2],trees[MX*2];
- int n,m,head[MX],now=0;
- queue<int> q;
- int fa[MX][P+2],deg[MX],dep[MX],ans[MX],getlca(int,int);
- int head2[MX],now2=0,siz[MX];
- bool vis[MX],vis2[MX];
- inline void adde(int u,int v){
- edge[++now].to=v;
- edge[now].next=head[u];
- head[u]=now;
- }
- inline void adde2(int u,int v){ //在新得到的树上建图
- trees[++now2].to=v;
- trees[now2].next=head2[u];
- head2[u]=now2;
- }
- void bfs(int s){ //拓扑排序
- q.push(s);
- while (!q.empty()){
- int u=q.front();
- q.pop();
- for (int i=head[u];i;i=edge[i].next){
- int v=edge[i].to;
- if (!vis[v]) vis[v]=true,fa[v][0]=u;
- else {
- fa[v][0]=getlca(fa[v][0],u);
- }
- deg[v]–;
- if (!deg[v]){
- q.push(v);
- adde2(fa[v][0],v);
- dep[v]=dep[fa[v][0]]+1; //这里是fa[v][0],因为u不一定是转移到v的那个点
- for (int i=1;i<=P;i++) //fa[v][0]是支配树实际的转移点
- fa[v][i]=fa[fa[v][i-1]][i-1];
- }
- }
- }
- }
- void dfs(int u){ //在建好的新树上跑dfs求子树大小
- siz[u]=1;
- vis2[u]=true;
- for (int i=head2[u];i;i=trees[i].next){
- int v=trees[i].to;
- if (vis2[v]) continue;
- dfs(v);
- siz[u]+=siz[v];
- }
- }
- int getlca(int a,int b){ //倍增跳 LCA
- if (dep[a]<dep[b]) swap(a,b);
- int t=dep[a]-dep[b];
- for (int i=P;i>=0;i–){
- if (fa[a][i] && t&(1<<i))
- a=fa[a][i];
- }
- if (a==b) return a;
- for (int i=P;i>=0;i–){
- if (fa[a][i] && fa[b][i] && fa[a][i]!=fa[b][i])
- a=fa[a][i],b=fa[b][i];
- }
- return fa[a][0];
- }
- int main(){
- scanf(”%d”,&n);
- for (int i=1;i<=n;i++){
- while (1){
- scanf(”%d”,&m);
- if (m==0) break;
- adde(m+1,i+1); //把所有节点编号+1,之后将超级源点设为 1 号点
- deg[i+1]++; //防止编号溢出
- }
- }
- for (int i=P;i>=0;i–) fa[1][i]=0;
- dep[1]=1;
- q.push(1);
- for (int i=1;i<=n;i++)
- if (deg[i+1]==0) deg[i+1]++,adde(1,i+1);
- bfs(1);
- dfs(1);
- for (int i=1;i<=n;i++){
- printf(”%d\n”,siz[i+1]-1);
- }
- return 0;
- }