树形DP

给出一棵树,要求以最少代价(或最大收益)完成给定的操作。

基本操作

  • 树的遍历,用DFS从根节点开始进行记忆化搜索
  • 从树最深处开始往回进行DP,用子节点dp值来更新父节点dp值

复杂度分析:遍历每个节点,总复杂度为O(n)

例题

某大学有 n 个职员,编号为 1n

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

题目分析

根据DP的解题思路,定义状态:dp[i][0]表示不选择当前节点的最优解,dp[i][1]表示选择当前节点的最优解。

状态转移方程有两种情况(设u为父节点,v为子节点):

1.不选择当前节点,那么它的子节点可选可不选,取其中的最大值,即

dp[u][0] += max(dp[v][0], dp[v][1])

2.选择当前节点,那么它的子节点不可选,即

dp[u][1] += dp[v][0]

实现代码

#include <bits/stdc++.h>
using namespace std;
const int N = 6005;
int val[N], dp[N][2], father[N];
vector<int> G[N];
void addedge(int from, int to)
{
	G[from].push_back(to); // 用邻接表建树
	father[to] = from;	   // 父子关系
}
void dfs(int u)
{
	dp[u][0] = 0;	   // 赋初值:不参加宴会
	dp[u][1] = val[u]; // 赋初值:参加宴会
	for (int i = 0; i < G[u].size(); i++)
	{ // 遍历u的邻居v。逐一处理这个父结点的每个子结点
		int v = G[u][i];
		dfs(v);						// 深搜子结点
		dp[u][1] += dp[v][0];			        // 父结点选择,子结点不选
		dp[u][0] += max(dp[v][0], dp[v][1]);            // 父结点不选,子结点可选可不选
	}
}
int main()
{
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &val[i]); // 输入快乐指数
	for (int i = 1; i < n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		addedge(v, u);
	}
	int t = 1;
	while (father[t])
		t = father[t]; // 查找树的根结点
	dfs(t);			   // 从根结点开始,用dfs遍历整棵树
	printf("%d\n", max(dp[t][0], dp[t][1]));
	return 0;
}

模板

由例题,可以总结出来树形dp核心部分的模板:

void dfs(int u)
{
	dp[u][...] = ...; 			//初始化
	for(int i=0; i < edge[u].size(); i++)	//遍历处理u的子节点v
	{
		int v = edge[u][i];
        	dfs(v);				//深搜子结点
        	dp[u][...] = ...;		//状态转移方程
   	}
}

最大独立集

在树中选出尽量多的节点,使得任何两个节点均不相邻。

其实上面的第一道例题就是典型的求最大独立集题型,在这里就不加以赘述了。


最小点覆盖

在树中选出尽量少的节点,使得树上每一条边都至少有一端的节点被选中。

定义状态:dp[i][0]表示不选择当前节点的最优解,dp[i][1]表示选择当前节点的最优解。

状态转移方程有两种情况(设u为父节点,v为子节点):

1.不选择当前节点,那么它的子节点必须选,即

dp[u][0] += dp[v][1]

2.选择当前节点,那么它的子节点可选可不选,即

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

代码实现

与前者类似


最小支配集

在树中选出尽量少的节点,使得树上每个点要么被选、要么被它的相邻点支配(即该点有相邻点被选)

  • 定义状态:
    • 状态1:dp[i][0]表示选择当前节点
    • 状态2:dp[i][1]表示不选择当前节点,且无子节点被选(即父节点被选)
    • 状态3:dp[i][2]表示不选择当前节点,且其至少有一个子节点被选.

状态转移方程有两种情况(设u为父节点,v为子节点):

  1. 选择当前节点,那么它的子节点可选可不选,取其中的最小值,即

    dp[u][0] += min(dp[v][0], dp[v][1], dp[v][2])

  2. 不选当前节点,且父节点被选,则当前节点的子节点属于状态3,故

    dp[u][1] += dp[v][2]

  3. 不选当前节点,且其至少有一个子节点被选

    若点 u 没有子节点,dp[u][2] 应被初始化为INF
    若有,则其子节点可能是状态1或状态3。
    而点u至少有一个子节点被选中,所以至少有一个节点是状态1;
    则转移方程:

    if(u 没有子节点) dp[u][2] = INF;
    else			dp[v][2] += min(dp[v][0], dp[v][2]);
    
    遍历完后:dp[v][2] += inc;
    其中:	
    	if(dp[v][0] <= dp[v][2])	inc = 0; //如果有至少一个子节点被选择,则不用补差值
    	else			inc = min(inc, dp[v][0] - dp[v][2]); //如果没有,则选择两个状态下差值最小的子节点
    

代码实现

void dfs(int u)
{
    dp[u][0] = 1;
    dp[u][1] = dp[u][2] = 0;
    bool s = false; //转移状态2时记录是否有子节点被选择
    int sum = 0, inc = INF;
    for (int i = 0; i < edge[u].size(); i ++)
    {
        int v = edge[u][i];
        dfs(v);
        dp[u][0] += min({dp[v][0], dp[v][1], dp[v][2]}); //转移状态1

        if (dp[v][0] <= dp[v][2])                     //转移状态2时,判断子节点
        {
            sum += dp[v][0];
            s = true;
        }
        else
        {
            sum += dp[v][2];
            inc = min(inc, dp[v][0] - dp[v][2]);
        }

        if (dp[v][2] != INF && dp[u][1] != INF)     //转移状态3
            dp[u][1] += dp[v][2];
        else dp[u][1] = INF;
    }

    if (inc == INF && !s)   //转移状态2
            dp[u][1] = INF; //没有子节点时初始化为INF
    else
    {
        dp[u][1] = sum;
        if (!s) dp[u][1] += inc;
    }
}

树上背包

树上的背包问题,简单来说就是背包问题与树形 DP 的结合。

例题

某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。

从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。

现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。

写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。

题目分析

我们设 dp(u,i,j) 表示以 u 号点为根的子树中,已经遍历了 u 号点的前 i 棵子树,选了 j 个用户的最大收益。

转移的过程结合了树形 DP 和 背包 DP 的特点,我们枚举 u 点的每个子结点 v,同时枚举以 v 为根的子树选了几个用户,将子树的结果合并到 u 上。

记点 x 的儿子个数为 sx,以 x 为根的子树所有的用户数量 siz_x ,可以写出下面的状态转移方程:

f(u,i,j)=maxv,kj,ksiz_vf(u,i1,jk)+f(v,sv,k)

注意上面状态转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。

dp 的第二维可以很轻松地用滚动数组的方式省略掉,注意这时需要倒序枚举 j 的值。

dp 循环实现:

for (int i = 0; i < edge[u].size(); i++)          // 把u的每个子树看作一个组
{
	...
	for (int j = sum[u]; j >= 0; j--)         // 把u下的用户总数总数看成背包容量
		for (int k = 1; k <= sum[v]; k++)	  // 用k遍历每个子树的每个用户
			dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[v][k] - w);
}

实现代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int N = 3e3 + 5;
struct node
{
	int v, w;
	node(int x = 0, int y = 0) : v(x), w(y) {}
};

vector<node> edge[N];
int n, m, val[N];
int sum[N]; 		//节点下的用户数量
int dp[N][N];
void dfs(int u)
{
	if (val[u])		//用户节点初始化
	{
		dp[u][1] = val[u];
		sum[u] = 1;
		return;
	}
	for (int i = 0; i < edge[u].size(); i++)
	{
		int v = edge[u][i].v, w = edge[u][i].w;
		dfs(v);
		sum[u] += sum[v];
		for (int j = sum[u]; j > 0; j--)
			for (int k = 1; k <= sum[v]; k++)
				dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[v][k] - w);
	}
	return;
}
int main()
{
	memset(dp, -0x3f3f3f3f, sizeof dp); //初始化一个极大负值,因为dp可能为负
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n - m; i++)
	{
		int k;
		scanf("%d", &k);
		while (k--)
		{
			int a, c;
			scanf("%d %d", &a, &c);
			edge[i].push_back(node(a, c));
		}
	}
	for (int i = 1; i <= m; i++)
		scanf("%d", &val[n - m + i]);
	for (int i = 1; i <= n; i++) 
		dp[i][0] = 0;     //选0个用户的花费是0
	dfs(1);
	for (int i = m; i >= 1; i--)
		if (dp[1][i] >= 0)
		{
			printf("%d", i);
			break;
		}
	return 0;
}

换根 DP

树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。

通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。

例题

给定一个 n 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

题目分析

不妨令 u 为当前结点,v 为当前结点的子结点。首先需要用 si 来表示以 i 为根的子树中的结点个数,并且有 su=1+sv。取一个节点 x 为根,以一次 DFS 来计算所有的 si,这次的 DFS 就是预处理,我们得到了以结点 x 为根时其子树中的结点总数。

取与 x 相邻的节点 y. 则当根从 x 换到 y 时:

所有在 y 的子树上的结点深度都减少了1,那么总深度和就减少了 sy

所有不在 y 的子树上的结点深度都增加了1,那么总深度和就增加了 nsy

设状态 fx 为以 x 为根时,所有结点的深度之和,则 fy=fxsy+nsy=fx+n2×sy

于是在第二次 DFS 遍历整棵树并状态转移 fy=fx+n2×sy,那么就能求出以每个结点为根时的深度和了。最后只需要遍历一次所有根结点深度和就可以求出答案。


题单

序号 题号 标题 题型 难度评级
1 P1352 没有上司 的舞会 树形DP/最大独立集
2 hdu 2196 Computer 树形DP ⭐⭐
3 P2015 二叉苹果树 树上背包 ⭐⭐
4 P1273 有线电视网 树上背包 ⭐⭐
5 P2014 选课 树上背包 ⭐⭐
6 P3047 Nearby Cows G 树形DP ⭐⭐⭐
7 P3698 小Q的棋盘 树形DP ⭐⭐
8 P2607 骑士 树形DP ⭐⭐⭐
9 P2016 战略游戏 最小点覆盖
10 P2899 Phone Network G 最小支配集 ⭐⭐
11 P2986 Great Cow Gathering G 换根dp ⭐⭐

参考资料

posted @   dddon  阅读(45)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示