[ZJOI2012]灾难

本文介绍了如何在食物网中计算生物的灾难值,即某生物灭绝后受影响的生物数量。通过构建支配树,利用拓扑排序和最近公共祖先(LCA)算法解决这一问题。文章以实例详细解释了建树过程,并提供了代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目大意

给定一张食物网,对于每一种生物,求其灾难值。灾难值的定义为

如果它突然灭绝,那么会跟着一起灭绝的生物的种数。

Solution

很多 d a l a o dalao dalao说是支配树。

呃?不知道支配树??

没事,我也不知道,但是这题看完之后自然就知道了。


如果给你的不是一个 D A G DAG DAG,而是一棵树,那么怎么做?

这不简单?每个节点的答案就是子树大小 − 1 -1 1,因为要减去自己。

那我们就把图变成一棵树。


我们以样例为例。

首先草是一切生物的食物来源,所以其等级最高,就会被建在我们新建的树的根节点。
所以我们是怎么找到草的呢?不知道。但是如果反向建边呢?在这里插入图片描述

很明显,就是入度为 0 0 0的点。

然后考虑下一个节点,比如说考虑节点 2 2 2,显然,在重构的树中,节点 2 2 2应该是 1 1 1的儿子。这是一个前驱的情况,那么比如说节点 4 4 4这样有 2 , 3 2,3 2,3两个前驱的呢?显然不可能是 2 , 3 2,3 2,3的儿子,根据建树的目的,可以想到, 4 4 4应该是 1 1 1的儿子,因为只有 1 1 1灭绝了,才有可能使 4 4 4灭绝,而 1 1 1是怎么找到的呢?在 4 4 4之前,显然 2 , 3 2,3 2,3已经作为 1 1 1的儿子建在树上了,而 1 1 1恰好就是 4 4 4的所有前驱的 L C A LCA LCA

所以建树时,对于每个节点 u u u,要得出 u u u的所有前驱,然后求出这些前驱的在重构的树中的 L C A LCA LCA,然后将 u u u建为 L C A LCA LCA的儿子。其合理性非常简单,在重构的树中,要求一个父节点灭绝之后,其所有的子节点都灭绝,为了使 u u u灭绝,必须使所有 u u u的前驱灭绝,为了使 u u u的所有前驱灭绝,只有让这些点在重构树上的 L C A LCA LCA灭绝,所以当 L C A LCA LCA灭绝了, u u u才会灭绝,所以 u u u要作为 L C A LCA LCA的直接儿子。

最后一点就是,为了使在处理 u u u时,所有的 u u u的前驱都被处理过了,就要用到 t o p o topo topo

而最后建出来的这棵树,就被称为支配树,上述就是对于 D A G DAG DAG的建支配树的方法。

你,学废了吗?


为了写代码方便,避免出现多个初始入度为 0 0 0的点,可以建立一个大起点(太阳?),从而转化为一个起点的情况。

Code

#include<bits/stdc++.h>
#define ll long long
#define inf 1<<30
using namespace std;
const int MAXN=1e6+10;
vector<int> e[MAXN],tr[MAXN],lk[MAXN];
//e存原图	tr存树图	lk存点的前驱
int in[MAXN],f[MAXN][20],dep[MAXN];
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	int dlt=dep[x]-dep[y];
	for(int i=0;i<20;i++)
		if(dlt&(1<<i))
			x=f[x][i];
	if(x==y) return x;
	for(int i=19;i>=0;i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}//倍增求LCA
int siz[MAXN];
void dfs(int x,int fa){
	siz[x]=1;
	for(int i=0;i<tr[x].size();i++){
		int s=tr[x][i];
		if(s==fa) continue;
		dfs(s,x);
		siz[x]+=siz[s];
	}
}
int main()
{
	int n,z;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		while(~scanf("%d",&z)&&z){
			e[z].push_back(i);//注意反向建边,别被题目误导
			in[i]++;
		}
	}
	int rot=0;
	dep[rot]=0;//大起点
	queue<int> q;
	for(int i=1;i<=n;i++)
		if(!in[i]){
			lk[i].push_back(rot);//将大起点放入i的前驱中
			q.push(i);
		}
	while(!q.empty()){
		int x=q.front();q.pop();
		int lca=lk[x][0];
		for(int i=1;i<lk[x].size();i++)
			lca=LCA(lca,lk[x][i]);//求出树中的lca
		dep[x]=dep[lca]+1;
		f[x][0]=lca;
		for(int i=1;i<20;i++)
			f[x][i]=f[f[x][i-1]][i-1];//倍增的预处理
		tr[lca].push_back(x);//将x建入lca的儿子
		for(int i=0;i<e[x].size();i++){
			int s=e[x][i];
			lk[s].push_back(x);//x是s的前驱之一
			if(--in[s]==0) q.push(s);
		}
	}
	dfs(rot,-1);//跑出每个点的子树大小
	for(int i=1;i<=n;i++)
		printf("%d\n",siz[i]-1);//然后减一就是答案。
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值