树上DP笔记

客博的__80gnaij11__ / 2023-08-21 / 原文

树是一个由 \(n\) 个节点,\(n-1\) 条边组成的联通图,图上没有环,其每一条边都是割边。

在树上设计动态规划算法时,一般以节点从深到浅的顺序作为 DP 的阶段。大多数时候,采用递归的方式实现树形动态规划。

对于每一个节点 \(x\),先对它的每一个子节点进行 DP,回溯时从子节点向 \(x\) 进行状态转移。

例题 \(1\) P1352

以节点编号作为 DP 状态的第 \(1\) 维,一名职员是否愿意参加,只与他的直接上司是否参加有关。我们定义 \(f[x,0]\) 表示从以 \(x\) 为根的子树中邀请一部分职员参会,且 \(x\) 不参加舞会时,快乐总和的最大值,\(f[x,1]\) 则代表 \(x\) 参加舞会时快乐总和的最大值。

那么状态转移方程为:

\[f[x,0]=\displaystyle\sum_{s ∈ son(x)}max(f[s,0],f[s,1]); \]

\[f[x,1]=h[x]+\displaystyle\sum_{s ∈ son(x)}f[s,0]; \]

如果是叶子结点,那么:

\[f[x,0]=0,f[x,1]=h[x] \]

设根节点为 \(root\),那么答案为 \(max(f[root,0],f[root.1])\)

#include<bits/stdc++.h>
using namespace std;
int n,r[100009],f[100009][2];
vector<int> to[100009];
void dp(int x,int fa){
	for(int i=0;i<to[x].size();i++){
		if(to[x][i]==fa)  continue;
		dp(to[x][i],x);
	}
	for(int i=0;i<to[x].size();i++){
		f[x][0]+=max(f[to[x][i]][0],f[to[x][i]][1]);
		f[x][1]+=f[to[x][i]][0];
	}
	f[x][1]+=r[x];
	return ;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)  cin>>r[i];
	for(int i=1;i<=n-1;i++){
		int x,y;
		cin>>x>>y;
		to[x].push_back(y);
		to[y].push_back(x);
	}
	dp(1,0);
	cout<<max(f[1][0],f[1][1]);
	return 0;
}

例题 \(2\) P2014

我们定义 \(f[x,t]\) 为以 \(x\) 为根的子树中选修 \(t\) 门课能获得的最大学分。\(son(x)\)\(x\) 的子节点集合,个数 \(p=|son(x)|\)

如果 \(x\) 学习完毕,则可以学习 \(x\) 的子节点,对于每个子节点 \(y_i∈son(x)\),在以 \(y_i\) 为根的子树中选择 \(C_i\) 门课,则在满足 \(\displaystyle \sum_{y∈son(x)}=t-1\) 的情况下获得尽可能多的学分。

\(t=0\) 时,\(f[x,t]= 0\)

\(t>0\) 时,状态转移方程如下:

\[f[x,y]=\displaystyle \max_{\sum^p_{i=1} c_i=t-1}\bigg \{ \displaystyle\sum^p_{i=1} f[y_i,c_i] \bigg\}+score[x] \]

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n,m,s[1009],f[1009][1009];
vector<int> to[1009];
void dp(int x){
	f[x][0]=0;
	for(int i=0;i<to[x].size();i++){
		int y=to[x][i];
		dp(y);
		for(int t=m;t>=0;t--){
			for(int j=0;j<=t;j++)  f[x][t]=max(f[x][t],f[x][t-j]+f[y][j]);
		}
	}
	if(x!=0){
		for(int t=m;t>0;t--)  f[x][t]=f[x][t-1]+s[x];
	}
	return ;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int ki,si;
		cin>>ki>>si;
		to[ki].push_back(i);
		s[i]=si;
	}
	dp(0);
	cout<<f[0][m]; 
	return 0;
}



例题 \(3\) P4395

题目和气垫车有什么关系

看到题目的第一反应可能是贪心 \(12\) 染色,但可以被菊花图 hack。

进过一系列玄学计算可以算出取值上界大约是 \(\log n +1\),在本题中是 \(15\),保险一点取 \(20\)。令 \(f[x,t]\) 表示节点 \(x\)\(t\) 时的最小权值和。

如果 \(x\) 为叶节点,\(f[x,t]=t\)

否则

\[f[x,t]=\displaystyle \sum_{s ∈ son(x)}\displaystyle\min_{k \neq t}\{ f[s,k]\}+t \]

答案为 \(\min\{ f[1,t] \}\)

#include<bits/stdc++.h>
using namespace std;
int n,f[10009][29];
vector<int> to[10009];
void dp(int x,int fa){
	for(int i=1;i<=20;i++)  f[x][i]=i;
	for(int i=0;i<to[x].size();i++){
		if(to[x][i]==fa)  continue;
		dp(to[x][i],x);
		for(int j=1;j<=20;j++){
			int minn=0x3f3f3f3f;
			for(int k=1;k<=20;k++){
				if(j==k)  continue;
				minn=min(minn,f[to[x][i]][k]);
			}
			f[x][j]+=minn;
		}
	}
	return ;
}
int main(){
	cin>>n;
	for(int i=1;i<=n-1;i++){
		int x,y;
		cin>>x>>y;
		to[x].push_back(y);
		to[y].push_back(x);
	}
	dp(1,0);
	int ans=0x3f3f3f3f;
	for(int i=1;i<=20;i++)  ans=min(ans,f[1][i]);
	cout<<ans;
	return 0;
}