洛谷P2597 [ZJOI2012] 灾难

文章讨论了一种基于并查集和拓扑排序解决生物灭绝问题的算法。首先,通过反向思考,确定需要找到多个节点的最近公共祖先(LCA)。接着,构建灭绝树,利用拓扑排序确定节点加入的顺序,从而简化问题。最后,通过经典LCA算法在灭绝树中求解答案,并处理不联通图的情况。文章提供了具体的代码实现来演示整个过程。

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

 目录

题意

思路

        高潮一

        高潮二

        高潮三

        小坑点

代码


题意

        题目

        食物网中,灭绝一类生物,若其余生物没有了食物来源,则该生物一并死亡。将灭绝一类生物所导致的其他生物死亡种类数称为“灾难值”,求每类生物的灾难值。

        数据范围:1≤N≤65534

思路

        初看题目标签:“并查集”“拓扑排序”,你可能会一脸疑惑。不要着急,让我们慢慢来看。

        如果直接模拟某一类生物死亡,则时间复杂度为O(N^2),(……)就会爆炸。

        (小黑子树枝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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值