2/2树形dp学习总结

树上dp就是在树上做dp(如说)

由于树本身具有子结构(子树)的性质,使得树上dp转移比较好推。

今天学习了几个模板问题

最大独立集

在一棵树上选点,每个点有对应的价值,选的点互相之间不能直接连接,求最大价值

状态定义:

\(dp[i,0]\) 为以i为根的子树中,不选i能获得的最大价值

\(dp[i,1]\) 为以i为根的子树中,选i能获得的最大价值

状态转移:

\[dp[u,0]=\sum_{v\in son(u)}\max(dp[v,0],dp[v,1]) \]

\[dp[u,1]=\sum_{v\in son(u)} dp[v,0]+a_u \]

AC code
#include<bits/stdc++.h>
using namespace std;
vector<int> g[6005];
bool v[6005];
int a[6005];
int dp[6005][2];
void dfs(int x){
	dp[x][1]=a[x];
	for(int i=0;i<g[x].size();i++){
		int y=g[x][i];
		dfs(y);
		dp[x][0]+=max(dp[y][0],dp[y][1]);
		dp[x][1]+=dp[y][0];
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int n;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		g[y].push_back(x);
		v[x]=1;
	}
	int r=0;
	for(int i=1;i<=n;i++){
		if(!v[i]){
			r=i;
			break;
		}
	}
	dfs(r);
	cout<<max(dp[r][1],dp[r][0]);
	return 0;
}

最小支配集

依旧选点,选到的点可以保护与该点距离为1的点,求最少选多少个点可以保护所有的点

状态定义:

\(dp_i\) 表示保护满以 \(i\) 为子树需要的最小点

\(dp_{i,0}\) 为选自己

\(dp_{i,1}\) 为不选,等儿子来保护

\(dp_{i,2}\) 为不选,等父亲来保护

状态转移:

\[dp[u,0]=\sum_{v\in son(x)} min(dp[v,0...2]) \]

\[dp[u,2]=\sum_{v\in son(x)} min(dp[v,0],dp[v,1]) \]

\[Sum=\sum_{v\in son(x)}min(dp[v,0],dp[v,1]) \]

如果存在 \(v\in son(x)\),使得 \(dp[v,0]<dp[v,1]\):

\[dp[u,1]=Sum \]

否则:

\[dp[u,1]=Sum+min(dp[v,0]-dp[v,1]) \]

贪心解

对于当前未被保护的深度最深的节点,显然直接选父亲更好(作用域更广)

但对于保安站岗这种有代价的题就不行,选父节点可能选到代价巨大的点。

AC code
#include<bits/stdc++.h>
using namespace std;
vector<int> g[10005];
int fa[10005];
bool v[10005],z[10005];
int ans;
void dfs(int x,int Fa){
	fa[x]=Fa;
	for(int i=0;i<g[x].size();i++){
		int nx=g[x][i];
		if(nx==Fa) continue;
		dfs(nx,x);
	}
	if(!z[x]&&!v[Fa]){
		v[Fa]=1;
		ans++;
		z[x]=z[Fa]=z[fa[Fa]]=1;
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int n;
	cin>>n;
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		g[x].push_back(y);
		g[y].push_back(x);
	}
	dfs(1,0);
	cout<<ans;
	return 0;
}

最小点覆盖

还是选点,选到的点可以保护连接的边,问至少选多少个点可以保护所有的边

状态定义:

\(dp[i,0]\) 为以i为根的子树中,不选i需要的最少点

\(dp[i,1]\) 为以i为根的子树中,选i需要的最少点

(像最大独立集)

状态转移

\[dp[u,0]=\sum_{v\in son(x)}dp[u,1] \]

\[dp[u,1]=1+\sum_{v\in son(x)}min(dp[u,0],dp[u,1]) \]

(也像最大独立集,可以理解为最大独立集变式)

666看了 Gallai 恒等式才知道,最小点覆盖+最大独立集=所有点。有点意思

代码实现
void dfs(int u, int fa) {
	dp[u][1] = 1;
	for (auto v : g[u]) {
		if (v == fa) continue;
		dfs(v, u);
		dp[u][0] += dp[v][1], dp[u][1] += min(dp[v][0], dp[v][1]);
	}
}

最大匹配

从树中选出最多的边,使得任意两条边没有公共点(终于不选点了TwT)

状态定义

虽然是选边,但我们还是根据点来设计(好写)

对于每一个节点,我们只需要考虑选或不选它与子节点相连的边。

\(dp[i,0]\) 表示在i为根的子树中,节点i不与其子节点进行匹配时的最大匹配数。

\(dp[i,1]\) 表示在i为根的子树中,节点i与其子节点进行匹配时的最大匹配数。

状态转移

如果不选,u的子节点可能已经与其他节点匹配了,也可能没匹配,有

\[dp[u,0]=\sum_{v\in son(x)}max(dp[v,0],dp[v,1]) \]

如果要选,u匹配的子节点c不可能匹配过,其他的子节点仍可以自由选择,有

\[dp[u,1]=\max_{c\in son(x)}\{1+dp[c,0]+\sum_{v\in son(x),v!=c}max(dp[v,0],dp[v,1])\} \]

优化:

\(dp[u,0]\) 的值求 \(dp[u,1]\) 的值,再减去多算的 c 贡献的值即可

\[dp[u,1]=\max_{c\in son(x)}\{dp[u,0]-max(dp[c,0],dp[c,1])+dp[c,0]+1\} \]

代码实现
void dfs(int u, int fa) {
	for (auto v : g[u]) {
		if (v == fa) continue;
		dfs(v, u);
		dp[u][0] += max(dp[v][0], dp[v][1]);
	}
	for (auto v : g[u]) {
		if (v == fa) continue;
		dp[u][1] = max(dp[u][1], dp[u][0] - max(dp[v][0], dp[v][1]) + dp[v][0] + 1);
	}
}

写完了!好累QwQ

posted @ 2026-02-02 19:36  Frums  阅读(4)  评论(0)    收藏  举报