树形动态规划题集

测试地址
部分题面可能复制有问题,图片没有复制,有疑惑见原题面。

1575:【例 1】二叉苹果树

问题描述
有一棵二叉苹果树,如果数字有分叉,一定是分两叉,即没有只有一个儿子的节点。这棵树共 N 个节点,标号 1 至 N,树根编号一定为 1。

我们用一根树枝两端连接的节点编号描述一根树枝的位置。一棵有四根树枝的苹果树,因为树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。

解题思路
相当于从一棵二叉树上去掉一些子树,使得剩下 m 条边,目标是使得这 m 条边的总权值最大。剩 m 条边就相当于剩 m + 1 个点,我们转换成点来做。
设 f[i ,j] 表示节点 i 还剩 j 个点时最大总权值,目标状态是 f[1 , m+1] 。我们可以利用记忆化搜索,令 f[ i , j ] = MIN{ f[ ls , k] + f[rs , j - k -1] + w[ i ] }。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 440;
int head[N],edge[M],nex[M],ver[M],tot = 1;
void addEdge(int x,int y,int z){
	ver[++tot] = y; edge[tot] = z;
	nex[tot] = head[x]; head[x] = tot;
}
int n,m,f[N][N];
int ls[N],rs[N],w[N];
void dfs(int x,int fa){
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i],z = edge[i];
		if(y == fa) continue;
		if(ls[x]) rs[x] = y;
		else ls[x] = y;
		w[y] = z;	//y 到其父亲的边权
		dfs(y,x);
	}
}
int DP(int i,int j){
	if(!j) return 0;
	if(!ls[i] && !rs[i]) return w[i];
	if(f[i][j] > 0) return f[i][j];
	for(int k = 0;k <= j-1;k++)
		f[i][j] = max(f[i][j],DP(ls[i],k) + DP(rs[i],j-k-1) + w[i]);
	return f[i][j];
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1,x,y,z;i <= n;i++){
		scanf("%d%d%d",&x,&y,&z);
		addEdge(x,y,z); addEdge(y,x,z);		
	}
	dfs(1,0);
	printf("%d\n",DP(1,m+1)); //1号子树保留m+1个节点时最大值
	return 0;
}
1576:【例 2】选课

问题描述
原题来自:CTSC 1997

大学实行学分制。每门课程都有一定的学分,学生只要选修了这门课并通过考核就能获得相应学分。学生最后的学分是他选修各门课的学分总和。

每个学生都要选择规定数量的课程。有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其他的一些课程基础上才能选修。例如《数据结构》必须在选修了《高级语言程序设计》后才能选修。我们称《高级语言程序设计》是《数据结构》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。为便于表述,每门课都有一个课号,课号依次为 1,2,3,⋯。

学生不可能学完大学开设的所有课程,因此必须在入学时选定自己要学的课程。每个学生可选课程的总数是给定的。请找出一种选课方案使得你能得到的学分最多,并满足先修课优先的原则。假定课程间不存在时间上的冲突。

解题思路
这题本质上是有树形依赖的分组背包问题。
首先用 0 号虚拟节点将森林合并为一棵树,注意 0 号节点不算一门课,没有学分也不消耗选课机会。
设 f[x , j] = 在以 x 为根的子树中选 j 门课能得到的最大学分。
那么显然 f[x , j] 就可以表示为“在以 x 为根的子树上选 j 门,在子树 y 上选 k 门”的最大值。即 f[x , j] = MAX{ f[x , j - k] + f[y , k] },因为用 x 的儿子对 f[x, j] 的更新无所谓先后顺序,所以上式可以成立。

除了 0 号节点以外, x 是必选的,所以回溯前要加上该门课的学分并消耗一次选课机会。
代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1100;
const int M = N<<2;
int head[N],ver[M],edge[M],nex[M],tot = 1;
int w[N];
void addEdge(int x,int y,int z){
	ver[++tot] = y; edge[tot] = z;
	nex[tot] = head[x]; head[x] = tot;
}
int n,m;
int f[N][N];
void dfs(int x){
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i], z = edge[i];
		dfs(y);
		for(int t = m;t >= 0;t--)	//在子树 x 上选 t 门
			for(int j = t;j >= 0;j--)//在子树 y 选 j 门
				f[x][t] = max(f[x][t],f[x][t-j] + f[y][j]);
	}
	if(x) for(int t = m;t > 0;t--)//如果不是 0 号节点,那么根节点是必选的
		f[x][t] = f[x][t-1] + w[x];
}
void solve(){
	dfs(0);
	cout << f[0][m] << endl;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1,x,z;i <= n;i++){
		scanf("%d%d",&x,&z); 
		addEdge(x,i,z); w[i] = z;
	}
	solve();
	return 0;
}
1577:【例 3】数字转换

问题描述
如果一个数 x 的约数和 y (不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。例如 4 可以变为 3,1 可以变为 7。限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。

解题思路
“约数和 y” 是指 x 的所有约数的累加的值是y。
从 1~n 挨个求出它们的约数和 y,然后由 y 向它们连线,最终构成一棵树。接下来就是求树上最长链问题,可以用两次 bfs 或者 dp 。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = N<<1;
int getF(int x){
	int res = 1, i;
	for(i = 2;i*i < x;i++) if(x%i == 0) res += i+x/i;
	if(i*i == x) res += i;
	return res;
}
int n,a[N];
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	if(x == 0 || y == 0) return;
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int leaf,mx = 0;
int vis[N];
void bfs(int x,int step){
	if(step > mx) leaf = x,mx = step;
	vis[x] = true;
	for(int i = head[x]; i;i = nex[i]){
		int y = ver[i];
		if(vis[y]) continue;
		bfs(y,step+1);
	}
}
int main(){
	scanf("%d",&n);
	for(int i = 1;i <= n;i++){
		int x = getF(i);
		if(x > i) continue;
		addEdge(x,i); addEdge(i,x);
	}
	bfs(1,0);
	memset(vis,false,sizeof vis);
	bfs(leaf,0);
	cout << mx << endl;
	return 0;
}
1578:【例 4】战略游戏

问题描述
Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的方法。现在他有个问题。

现在他有座古城堡,古城堡的路形成一棵树。他要在这棵树的节点上放置最少数目的士兵,使得这些士兵能够瞭望到所有的路。

注意:某个士兵在一个节点上时,与该节点相连的所有边都将能被瞭望到。

请你编一个程序,给定一棵树,帮 Bob 计算出他最少要放置的士兵数。

解题思路
这种题是“树的最大独立集”问题,也就是要求树上每一条边都至少有一个端点在集合内。这一类最值问题向来都是用动态规划解决的。
任何一个点的取舍都可以看作一个决策,而每个顶点为阶段,顶点取或不取为状态。设f[x , 0] = 第 x 个点不取时,以 x 为根的子树需要的最少士兵数目;f[ x, 1]表示取 x 点时,以 x 为根的子树需要的最少士兵数目。
如果 x 不取,那么它的所有子节点都一定要取;如果取 x,那么其字节点可取可不取。
所以状态转移方程为 f [ x , 0 ] = ∑ f [ y , 1 ] f[x , 0] = \sum f[y ,1] f[x,0]=f[y,1] f [ x , 1 ] = ∑ M I N ( f [ y , 0 ] , f [ y , 1 ] ) + 1 f[x , 1] = \sum MIN(f[y , 0] , f[y ,1] ) + 1 f[x,1]=MIN(f[y,0],f[y,1])+1

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 2000;
const int M = N<<1;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int n,f[N][4];
bool vis[N];
void dfs(int x){
	vis[x] = true; f[x][1] = 1;
	for(int i = head[x];~i;i = nex[i]){
		int y = ver[i]; 
		if(vis[y]) continue;
		dfs(y); 
		f[x][0] += f[y][1];
		f[x][1] += min(f[y][0],f[y][1]);
	}
}
int main(){
	scanf("%d",&n);
	memset(head,-1,sizeof head);
	for(int i = 1,x,y,k;i <= n;i++){
		scanf("%d%d",&x,&k);
		for(int j = 1;j <= k;j++){
			scanf("%d",&y); 
			addEdge(x,y); addEdge(y,x);
		}
	}
	dfs(1);
	printf("%d\n",min(f[1][0],f[1][1]));
	return 0;
}
1579: 【例 5】皇宫看守

问题描述
太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。

皇宫以午门为起点,直到后宫嫔妃们的寝宫,呈一棵树的形状,某些宫殿间可以互相望见。大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。

可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。

帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。

解题思路
这题和上一题不同,上一次是每条边必须有一个端点被选,而这题是要求每个点的所有边中至少有一个边被选。换成点来说,就是与其相连的所有点(或其本身)中至少有一个被选。
那么我们就不能再“要么选 x ,要么选 x 的所有儿子”了;对于当前位置 x,我们可以分别从:

  • x 的父亲被选了
  • x 的儿子有一个被选了
  • x 自己被选了

三种情况来计算。我们设 f[x ,0|1|2 ]分别对应上述三种情况。对于当前节点 x ,我们既要考虑它的父亲,还要考虑它的儿子们,所以我们转移的时候既要加上它爹也要计算它儿子和孙子。具体状态设计和方程见代码吧,其中 d 是用来挑选一个费用最小的儿子。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 2000;
const int M = N<<1;
int n,w[N];
int head[N],ver[M],nex[M], tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int f[N][10];
bool vis[N];
void dfs(int x){
	vis[x] = true; f[x][2] = w[x];
	int d = 0x3f3f3f3f;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i], z = w[x];
		if(vis[y]) continue;
		dfs(y);
		f[x][0] += min(f[y][1],f[y][2]);
		f[x][1] += min(f[y][1],f[y][2]); 
		f[x][2] += min(f[y][0],min(f[y][1],f[y][2]));
		d = min(d,f[y][2] - min(f[y][1],f[y][2]));
	}
	f[x][1] += d;
}
int main(){
	scanf("%d",&n);
	for(int i = 1,x,k,y;i <= n;i++){
		scanf("%d",&x); scanf("%d%d",&w[x],&k);
		for(int j = 1;j <= k;j++){
			scanf("%d",&y);
			addEdge(x,y); addEdge(y,x);
		} 
	}
	dfs(1);
	printf("%d\n",min(f[1][1],f[1][2]));
	return 0;
}
1580:加分二叉树

题意描述
原题来自:NOIP 2003

设一个 n 个节点的二叉树 tree 的中序遍历为 (1,2,3,⋯,n),其中数字 1,2,3,⋯,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di ,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:

记 subtree 的左子树加分为 l,右子树加分为 r,subtree 的根的分数为 a,则 subtree 的加分为:l×r+a

若某个子树为空,规定其加分为 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。

试求一棵符合中序遍历为 (1,2,3,⋯,n) 且加分最高的二叉树 tree。

要求输出:

1、tree 的最高加分;

2、tree 的前序遍历。

解题思路
假设 f[i , j] 为中序遍历中 i 到 j 节点构成的子树最大得分是多少。那么显然就可以用类似区间 DP 的方法来解决,让跨度从小到大,起点也是从小到大,然后枚举中间节点即可。

但是输出前序遍历却不能直接用简洁的递归输出,而是需要加一行特判来处理边界情况。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int n,path[N][N];
typedef long long ll;
ll f[N][N],a[N];
void print(int i,int j){
	if(i == j || i+1 == j){
		printf("%d ",i);
		if(i != j) printf("%d ",j);
		return;
	}
	printf("%d ",path[i][j]);
	print(i,path[i][j]-1); print(path[i][j]+1,j);
}
void solve(){
	for(int i = 1;i <= n;i++)
		for(int j = 1;j <= n;j++) f[i][j] = 1;
	for(int i = 1;i <= n;i++) path[i][i] = i,f[i][i] = a[i];
	for(int l = 1;l <= n;l++)
		for(int i = 1;i <= n-l;i++){
			int j = i+l;
			for(int k = i;k <= j;k++)
				if(f[i][j] < f[i][k-1]*f[k+1][j] + a[k]){
					f[i][j] = f[i][k-1]*f[k+1][j] + a[k];
					path[i][j] = k;
				}
		}
	printf("%lld\n",f[1][n]);
	print(1,n);
}
int main(){
	scanf("%d",&n);
	for(int i = 1;i <= n;i++) scanf("%lld",a+i);
	solve();
	return 0;
}
1581:旅游规划

题意描述
W 市的交通规划出现了重大问题,市政府下定决心在全市各大交通路口安排疏导员来疏导密集的车流。但由于人员不足,W 市市长决定只在最需要安排人员的路口安排人员。

具体来说,W 市的交通网络十分简单,由 n 个交叉路口和 n−1 条街道构成,交叉路口路口编号依次为 0,1,⋯,n−1 。任意一条街道连接两个交叉路口,且任意两个交叉路口间都存在一条路径互相连接。

经过长期调查,结果显示,如果一个交叉路口位于 W 市交通网最长路径上,那么这个路口必定拥挤不堪。所谓最长路径,定义为某条路径 p=(v1,v2,v3,⋯,vk),路径经过的路口各不相同,且城市中不存在长度大于 k 的路径,因此最长路径可能不唯一。因此 W 市市长想知道哪些路口位于城市交通网的最长路径上。

解题思路
这道题就是求最长链上的节点,而最长链可能不止一条,所以就成了找出所有最长链上的所有节点,并按照升序输出。
我们知道两次 bfs 可以找到最长链的长度以及起点和终点,但是两次 bfs 却无法找到所有的最长链起点和终点。
我们可以通过 3 次 bfs 来找到所有的最长链以及其终点。首先通过第一遍 bfs 找到某一个叶子节点,然后第二次 bfs 找到所有以该叶子为起点的最长链,第三次 bfs 以其上次找到的某一条最长链的终点为起点,找到剩余的所有最长链。

由于树形结构上每一个节点都只有一个父亲,所以用 par 数组存放节点的父亲,这样就可以从终点找到该链上的所有节点。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+10;
const int M = N<<1;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int n,par[N],leaf,mx = 0;
bool vis[N];
queue<int> q;
void bfs(int x,int step){
	if(mx < step){
		mx = step,leaf = x;
		while(q.size()) q.pop();
	}
	if(mx == step) q.push(x);
	vis[x] = true;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(vis[y]) continue;
		par[y] = x;
		bfs(y,step+1);
	}
}
int ans[M], cnt = 0;
void solve(){
	bfs(1,0);	//求最远叶子
	memset(vis,false,sizeof vis);
	bfs(leaf,0);	//从leaf出发的所有最远叶子
	memset(vis,false,sizeof vis);
	memset(par,0,sizeof par);
	bfs(leaf,0);	//再求一边最远叶子,防止对称遗漏

	while(q.size()){
		int x = q.front(); q.pop();
		ans[++cnt] = x;
		while(par[x]) x = ans[++cnt] = par[x];
	}
	sort(ans+1,ans+1+cnt);
	for(int i = 1;i <= cnt;i++)
		if(ans[i] != ans[i-1]) cout << ans[i]-1 << endl;
}
int main(){
	scanf("%d",&n);
	for(int i = 1,x,y;i < n;i++){
		scanf("%d%d",&x,&y); x++,y++;
		addEdge(x,y); addEdge(y,x);
	}
	solve();
	return 0;
}
1582:周年纪念晚会

题意描述
Ural 州立大学的校长正在筹备学校的 8080 周年纪念聚会。由于学校的职员有不同的职务级别,可以构成一棵以校长为根的人事关系树。每个资源都有一个唯一的整数编号,从 1 到 N 编号,且对应一个参加聚会所获得的欢乐度。为使每个职员都感到快乐,校长设法使每个职员和其直接上司不会同时参加聚会。

你的任务是设计一份参加聚会者的名单,使总欢乐度最高。
解题思路
这题就是“没有上司的舞会”那一题,解法就相当于求森林中每棵树的最大点独立集,我们设 f[x ,0] 表示 x 不去时最大快乐值,f[x , 1] 表示 x 去时最大快乐值。
显然 x 要是不去那么 y 去或不去都可以,x 如果去那么所有的 y 都不能去,所以状态转移方程就很好想了,详见代码。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 6100;
const int M = N<<1;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int hp[N], n, x, y;
int f[N][2],vis[N], ans = 0;
void dfs(int x){
	vis[x] = true;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(vis[y]) continue;
		dfs(y);
		f[x][1] = max(f[x][1],f[y][0]);
		f[x][0] += max(f[y][0],f[y][1]);
	}
	f[x][1] += hp[x];
}
int main(){
	scanf("%d",&n);
	for(int i = 1;i <= n;i++) scanf("%d",hp+i);
	while(scanf("%d%d",&x,&y) && (x || y)){
		addEdge(x,y); addEdge(y,x);
	}
	for(int i = 1;i <= n;i++)
		if(!vis[i]){
			dfs(i); ans += max(f[i][1],f[i][0]);
	} 
	cout << ans << endl;
	return 0;
}
1583:叶子的染色

题意描述
原题来自:CQOI 2009

给一棵有 m 个节点的无根树,你可以选择一个度数大于 1 的节点作为根,然后给一些节点(根、内部节点、叶子均可)着以黑色或白色。你的着色方案应保证根节点到各叶子节点的简单路径上都包含一个有色节点,哪怕是叶子本身。

对于每个叶子节点 u,定义 cu 为从根节点到 u 的简单路径上最后一个有色节点的颜色。给出每个 cu 的值,设计着色方案使得着色节点的个数尽量少。

解题思路
题意非常绕,理解起来要花时间,另外题面描述的还丢三落四。
大意就是说有一棵树,我们要求从树根到每一个叶子节点的路径上的最后一个节点的颜色是指定的,我们给出所有指定的颜色 c[leaf] ;显然如果一棵子树的所有叶子节点都指定黑色,那么只需要给根节点染一个黑色就可以了(当然也可以给每个叶子节点都染黑色,但是不是最优解),我们题意就是求类似这样的最优解。

那么我们可以设 f[x , 0] 表示 x 染黑色时的最优解,f[x , 1] 表示 x 染白色时,以 x 为根的子树最少需要染几次色。

如果 x 是叶子节点,且要求染黑色,那么显然以 x 为根的子树最少需要染一次色,f[ x , 0] = 1,f[x , 1] = INF。

如果 x 不是叶子节点,那么显然 f[x, 0] += max{ f[y , 0] - 1 , f[y , 1] },代表的实际含义是“如果 x 染黑色,那么就从 y 染黑色或 y 染白色中选一个最优解转移,显然如果 y 是黑色那么就可以少染一次(画图看)”

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 10500;
const int M = N<<1;
const int INF = 0x3f3f3f3f;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x];head[x] = tot;
}
int n,m,co[N],f[N][2];
bool vis[N];
void dfs(int x){
	vis[x] = true; f[x][0] = f[x][1] = 1;
	if(x <= m) f[x][co[x]] = 1,f[x][co[x]^1] = INF;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(vis[y]) continue;
		//cout << x << "-->" << y << endl;
		dfs(y);
		f[x][0] += min(f[y][0]-1,f[y][1]);
		f[x][1] += min(f[y][1]-1,f[y][0]);
	}
}
int main(){
	scanf("%d%d",&n,&m);
	memset(co,-1,sizeof co);
	for(int i = 1;i <= m;i++) scanf("%d",co+i);
	for(int i = 1,x,y;i < n;i++){
		scanf("%d%d",&x,&y);
		addEdge(x,y); addEdge(y,x);
	}
	dfs(m+1);
	cout << min(f[m+1][0],f[m+1][1]) << endl;
	return 0;
}
1584:骑士

题意描述
原题来自:ZJOI 2008

Z 国的骑士团是一个很有势力的组织,帮会中聚集了来自各地的精英。他们劫富济贫,惩恶扬善,受到了社会各界的赞扬。

可是,最近发生了一件很可怕的事情:邪恶的 Y 国发起了一场针对 Z 国的侵略战争。战火绵延五百里,在和平环境中安逸了数百年的 Z 国又怎能抵挡得住 Y 国的军队。于是人们把所有希望都寄托在了骑士团身上,就像期待有一个真龙天子的降生,带领正义打败邪恶。

骑士团是肯定具备打败邪恶势力的能力的,但是骑士们互相之间往往有一些矛盾。每个骑士有且仅有一个他自己最厌恶的骑士(当然不是他自己),他是绝对不会与最厌恶的人一同出征的。

战火绵延,人们生灵涂炭,组织起一个骑士军团加入战斗刻不容缓!国王交给你了一个艰巨的任务:从所有骑士中选出一个骑士军团,使得军内没有矛盾的两人,即不存在一个骑士与他最痛恨的人一同被选入骑士团的情况,并且使这支骑士军团最富有战斗力。

为描述战斗力,我们将骑士按照 1 至 N 编号,给每位骑士估计一个战斗力,一个军团的战斗力为所有骑士的战斗力之和。

解题思路
本来看起来和“没有上司的舞会”很类似,但是这题会有环,有环的情况下如果还用原来的解法就有可能出错,所以需要处理一下。

解决这类问题的方法一般有两种,一种是基环树 DP,另一种是两次树形 DP。这里采用的是俩次树形 DP,首先用一次 dfs 找到当前子树的环上的一条边 edge,然后对这两条边的顶点为根分别做一次 DP,最终答案为 max( f[x, 0] , f[ y, 0] ),如果当前子树没有环(有重边),则正常 DP 即可,最终累加答案并输出。
这题对时限要求有点紧,在 bzoj 是可以过的,但是在一本通上过不了,在本校的 oj 也过不了。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
const int M = N<<1;
int head[N],ver[M],nex[M],tot = 1;
inline void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
typedef long long ll;
ll a[N],f[N][2] , ans = 0,mx;
int n,vis[N],edge ;
int Stack[N],top = 0;
void DP(int x){
	vis[x] = true; Stack[++top] = x;
	f[x][1] = a[x]; f[x][0] = 0;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(vis[y]) continue;
		DP(y);
		f[x][0] += max(f[y][0],f[y][1]);
		f[x][1] += f[y][0];
	}
}
bool inStack[N];
void dfs(int x,int fa){	//找到环上一边
	inStack[x] = true;	//正在遍历
	if(edge) return;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(y == fa) continue;
		if(inStack[y]) edge = i;
		else dfs(y,x);
	}
	inStack[x] = false;
}
int getInt(){
	int res = 0;
	char c = getchar();
	bool neg = false;
	while(c != '-' && (c < '0' || c > '9')) c = getchar();
	if(c == '-') neg = true, c = getchar();
	while(c >= '0' && c <= '9') res = res*10 + c-'0',c = getchar();
	return neg?-res:res;
}
void print(ll x){
	if(x > 9) print(x/10);
	putchar(x%10+'0');
}
int main(){
	n = getInt();
	for(int i = 1,y;i <= n;i++){
		a[i] = getInt(); y = getInt();
		addEdge(i,y); addEdge(y,i);
	}
	for(int i = 1;i <= n;i++)
		if(!vis[i]){
			top = edge = 0; dfs(i,0);
			if(!edge){
				DP(i); ans += max(f[i][0],f[i][1]);
			}else{
				DP(ver[edge]); mx = f[ver[edge]][0];
				while(top) vis[Stack[top--]] = false;
				DP(ver[edge^1]); mx = max(f[ver[edge^1]][0],mx);
				ans += mx;
			}
		} 
	print(ans);putchar('\n');
	return 0;
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: C程序设计竞赛题集是由《算法竞赛入门经典》作者刘汝佳编写的一本算法竞赛题集。该书收录了大量的算法竞赛目,包括基础算法、数据结构、贪心算法、动态规划、字符串处理等多个方面的内容,这些目都是来自于各个大型编程比赛和实际编程项目中的经典案例。 C程序设计竞赛题集目难度从易到难逐渐递增,对于初学者来说,可以学习和掌握基本的编程技巧,并且逐渐提高自己的水平。对于已经有一定编程经验的人来说,这本书可以帮助他们解决实际的编程问,并且提高算法的实现能力。 除了提供各类算法实现的经典案例之外,C程序设计竞赛题集还包含了详细的解思路和详解,这对于独立思考和自己尝试解决问非常有帮助。此外,该书也提供了在线评测系统和部分习的官方解答,可以帮助读者更好的学习和提高。 总之,C程序设计竞赛题集是一本非常重要的编程题集,对于想要学习和提高算法竞赛能力的人来说,这本书是必不可少的参考书籍之一。 ### 回答2: "C程序设计竞赛题集" 是一本由中国著名程序员刘汝佳编写的程序设计竞赛题集。这本书分为基础篇、提高篇和数据结构篇三个部分。基础篇主要涉及编程基础和算法基础;提高篇增加复杂度较高的算法,并从多个角度分析对算法的优化;数据结构篇主要讨论了、图、字符串、数据结构设计等。 "C程序设计竞赛题集"不仅是一本程序员必备的参考书,也适合初学者在学习算法和数据结构时参考,可以提高他们解决复杂问的能力,虽然编程能力并不在大学排名考核之际,但计算机编程已经成为了各领域越来越重要的一项技能,能够为大众生活工作提供更加便捷的服务,参考这本书提高自己的编程能力就有利于开拓更多的事业机会,提高自己的市场价值。 总的来说, "C程序设计竞赛题集" 是一本内容较为全面的参考书,里面的目已经得到广泛认可和应用,并且也对算法和数据结构的学习有帮助。这本书适合程序员、计算机领域的工作者和对计算机有兴趣的初学者参考阅读。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

迷亭1213

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值