专栏文章

二叉苹果树

个人记录参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@miostgrx
此快照首次捕获于
2025/12/03 00:34
3 个月前
此快照最后确认于
2025/12/03 00:34
3 个月前
查看原文

二叉苹果树

题目链接

凌晨0:31

分析

给你一棵树,只能保留 qq 个树枝(边),最大的苹果数量
因为是在树上,又有容量(树枝个数)和最大价值,不难想到树上背包

树上背包

所以设 dp[i][j][k]dp[i][j][k] 为 以 ii 为根的子树中,前 jj 个子树,选 kk 个树枝,最大的苹果数
由背包问题的优化,可以换为滚动数组,将 jj 一维压缩,再倒序遍历 jj
那么从节点 uu 一层一层地往下枚举,枚举每个子树根 vv ,可得:
dp[i][j]=max{dp[i][jk1]+dp[v][k]+w{u,v}}(0jq,0k<j)dp[i][j]=max\{dp[i][j-k-1]+dp[v][k]+w\{u,v\}\} (0 \leq j \leq q , 0 \leq k < j)
其中 jk1j-k-1 是除 vv 子树外树枝数
那么实现便是
DFS代码CPP
void dfs(int x,int fa){//x:以x为根的子树  fa:当前节点的父亲(不重复) 
	for(auto i:g[x]){//遍历子树 
		if(i.first==fa){//遍历到父亲,就下一个(跳过) 
			continue; 
		}
		dfs(i.first,x);//统计子树信息 
		for(int j=q;j>=1;j--){//枚举容量(树枝数) 原为 dp[i][j][k] 以i为子树,前j个枝,选k个,苹果数最大,压维后需逆序 
			for(int k=0;k<j;k++){//枚举当前树枝容量 
				dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[i.first][k]+i.second);//更新,注意是j-k-1不是j-k,因为 x->i 连边也应加上 
			}
		}
	}
	return;
}
时间复杂度 O(nq2)O(nq^2) ,对于这道题的 1Q<N1001 \leq Q < N \leq 100 来说已经足够了

优化

但还可以再优化一点
因为 jj 是当前子树的树枝个数,但 jqj \leq q ,所以只用枚举到当前子树 min{siz[i],q}min\{siz[i],q\} 即可,而同理,kk 也只需枚举 k<min{siz[v],j}k < min\{siz[v],j\}
跑一遍预处理 sizsiz 数组
预处理CPP
void solve(int x,int fa){
	for(auto i:g[x]){//遍历子树 
		if(i.first==fa){//遍历到父亲,就下一个(跳过) 
			continue; 
		}
		solve(i.first,x);//统计子树信息 
		siz[x]+=siz[i.first]+1;//树枝数=字数树枝数+1 
	}
	return;
}
再判断 jmin{siz[i],q},k<min{siz[v]+1,j}j \leq min\{siz[i],q\} , k < min\{siz[v]+1,j\}
注意这里 siz[v]siz[v] 要加1,因为是 << ,但是是可以选 siz[v]siz[v] 个的

AC Code

请勿copyCPP
//二叉苹果树
//树形dp 
#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
int n,q;
vector<pair<int,int>>g[310];//链式前向星亦可 
int dp[110][110];//dp[i][j]表示在以i为根的子树中,留下j个树枝,最多能留住的苹果数量 
int siz[110];
void solve(int x,int fa){
	for(auto i:g[x]){//遍历子树 
		if(i.first==fa){//遍历到父亲,就下一个(跳过) 
			continue; 
		}
		solve(i.first,x);//统计子树信息 
		siz[x]+=siz[i.first]+1;//树枝数=字数树枝数+1 
	}
	return;
}
void dfs(int x,int fa){//x:以x为根的子树  fa:当前节点的父亲(不重复) 
	for(auto i:g[x]){//遍历子树 
		if(i.first==fa){//遍历到父亲,就下一个(跳过) 
			continue; 
		}
		dfs(i.first,x);//统计子树信息 
		for(int j=min(siz[x],q);j>=1;j--){//枚举容量(树枝数) 原为 dp[i][j][k] 以i为子树,前j个枝,选k个,苹果数最大,压维后需逆序 
			for(int k=0;k<min(siz[i.first]+1,j);k++){//枚举当前树枝容量 (<!) 
				dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[i.first][k]+i.second);//更新,注意是j-k-1不是j-k,因为 x->i 连边也应加上 
			}
		}
	}
	return;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<n;i++){
		int a,b,c;
		cin>>a>>b>>c;
		g[a].push_back({b,c});
		g[b].push_back({a,c});//存树(图) 
	}
	solve(1,0);
	dfs(1,0);//从1开始遍历 
	cout<<dp[1][q];//在1节点,留q个树枝,最多能留住的苹果数 
	return 0;
} 

总结

这是一道很模板的树上背包,但由于是树枝(边)上有价值,所以要细心改动一些,在细节上也要小心
很像选课这道题,但除了边上有价值以外,这道题一定是一棵二叉树!!!所以只有两条边,没有选课麻烦,但这个通解总能行
一般来说,树形dp都是先统计子树的答案,再汇总到根来计算,如 siz,sumsiz,sum 等数组的统计,约等于记忆化,但更多是dp,从叶子往上递推(一般)
几个经典模型:
  • 树上背包
  • 最小覆盖集(点)
  • 最大独立集
  • 最小支配集(边)

树形dp题

评论

0 条评论,欢迎与作者交流。

正在加载评论...