树形DP问题

树形DP

是在树上进行的动态规划,所以一般采用递归的方式计算状态的转移

https://www.luogu.com.cn/problem/P1352

一道经典的树形DP问题引入

int n;
vector<int> e[6010];
int a[6010];
bool st[6010];
int dp[6010][2];

void dfs(int x){
	dp[x][1]=a[x];
	for (auto v:e[x]){
		dfs(v);
		dp[x][1]+=dp[v][0];
		dp[x][0]+=max(dp[v][1],dp[v][0]);
	}
}

void solve(){
	cin>>n;
	for (int i=1;i<=n;i++) cin>>a[i];
	for (int i=1;i<n;i++){
		int l,k;
		cin>>l>>k;
		e[k].push_back(l);
		st[l]=1;
	}
	for (int i=1;i<=n;i++){
		if (st[i]==0){
			dfs(i);
			cout<<max(dp[i][0],dp[i][1]);
			break;
		}
	}
}

我们定义 \(dp_{i,0}\) 表示不选 \(i\) 节点的的最大指数,\(dp_{i,1}\) 表示选择 \(i\) 节点的最大指数

根据题目可以得出规则:如果选一个节点,那么他的子节点不能选;如果不选一个节点,那么他的子节点可选可不选

所以写出状态转移方程:其中 \(j\) 表示 \(i\) 的子节点

\[dp_{i,1}=\sum_j dp_{j,0}\\ dp_{i,0}=\sum_j {\rm{max}}(dp_{j,0},dp_{j,1}) \]

对于 \(i\) 节点不选择的状态,从他的子节点的两个状态中选取最佳的状态进行转移

在 DFS 过程中,\(dp\) 状态通常为当前节点的最优解,上面的题目中,我们在后序遍历回溯时,总是上传子节点的最优解给父节点进行状态转移,最终到达根节点为全局最优解


与边有关的树形DP

https://www.luogu.com.cn/problem/P2015

struct edge{
	int v,w;
};

int n,q;
vector<edge> e[110];
int dp[110][110];
int a[110];

void dfs(int x,int p){
	dp[x][0]=a[x];
	for (auto it:e[x]){
		if (it.v==p) continue;
		a[it.v]=it.w;
		dfs(it.v,x);
		for (int i=q;i>=1;i--){
			for (int j=0;j<=i-1;j++){
				dp[x][i]=max(dp[x][i],dp[x][i-j-1]+dp[it.v][j]);
			}
		}
	}
}

void solve(){
	cin>>n>>q;
	for (int i=1;i<n;i++){
		int u,v,w;
		cin>>u>>v>>w;
		e[u].push_back({v,w});
		e[v].push_back({u,w});	
	}
	dfs(1,-1);
	cout<<dp[1][q];
}

给出的边连接树上两个点,边权为这个边上的苹果数量,可以看做这个边连接的儿子拥有的苹果数量

所以在 DFS 过程中应该记录当前儿子节点的苹果数

a[it.v]=it.w;

定义 \(dp_{i,j}\) 表示节点 \(i\)\(j\) 个树枝的最大苹果数,初始化时 \(dp_{i,0}=a_i\) 即每个节点如果只选自己,那么苹果数就为这个树枝上的苹果数

考虑状态的转移,其中 \(x,v\) 分别表示父节点和他的一个子节点

\[dp_{x,i}={\rm{max}}(dp_{x,i},dp_{x,i-j-1}+dp_{v,j}) \]

形象化的说,父节点有 \(i\) 个树枝的最大苹果数由他的子节点 \(v\)\(j\) 个树枝的状态转移

因为连接子树需要额外的一条边,所以如果选取一个有 \(j\) 条边的子节点的状态,那么父节点的边数需要多减一即 \(i-j-1\)


树上背包问题

https://www.luogu.com.cn/problem/P2014

int n,m;
int a[310];
vector<int> e[310];
int dp[310][310];

void dfs(int x){
	for (int i=1;i<=m;i++) dp[x][i]=a[x];
	for (auto v:e[x]){
		dfs(v);
		for (int i=m;i>=1;i--){
			for (int j=0;j<=i-(x!=0);j++){
				dp[x][i]=max(dp[x][i],dp[x][i-j]+dp[v][j]);
			}
		}
	}
}

void solve(){
	cin>>n>>m;
	for (int i=1;i<=n;i++){
		int k;
		cin>>k;
		e[k].push_back(i);
		cin>>a[i];
	}
	dfs(0);
	cout<<dp[0][m];
}

定义 \(dp_{i,j}\) 表示第 \(i\) 个节点容量为 \(j\) 时能得到的最大学分,因为每门课所占体积都为 \(1\) ,所以这里的 \(j\) 也可以表示选 \(j\) 门课

对于每个节点,都可以看作是一个容量为 \(j\) ,在他子节点中选取物品的背包问题

形如

for (auto v:e[x]){
	for (int i=m;i>=1;i--){
		
	}
}

第一层找子节点的循环可以类比 01背包的第一层循环,即在前 \(u\) 个物品中选

第二层的枚举背包容量同理,注意这里的 \(dp\) 状态利用滚动数组的方式优化掉了第二维

for (int j=0;j<=i-(x!=0);j++)//x!=0表明不是虚拟根节点,需要预留选父节点容量1

第三层循环的目的是在节点容量为 \(i\) 下,枚举决策数,例如容量为 \(x\) 的背包,我们可以在第一层枚举的前 \(u\) 个子节点中选取很多个不同的组合

状态的转移可以被描述为:

对于 \(i\) 节点容量为 \(j\) 的状态,由他的子节点的从 \([1,j]\) 的容量组合进行转移

dp[x][i]=max(dp[x][i],dp[x][i-j]+dp[v][j]);

代码中父节点的容量被定义为 \(i\)对于从 \([1,m]\) 的容量,我们对每个容量的状态都再多循环一层决策

即在容量为 \(i\) 下,将 \(i\) 拆分为哪两个组合最优:是原有状态还是将 \(i\) 拆分为 \(j,i-j\) 这两个容量

其中 \(j\) 容量表示子节点组合出的总体积为 \(j\) 的最优方案,\(i-j\) 容量为其他子树的贡献

类似于 01背包的 dp[j]=max(dp[j],dp[j-w[i]]+v[i])

posted @ 2025-04-20 16:25  才瓯  阅读(71)  评论(0)    收藏  举报