树形DP
定义
树形动态规划(Tree Dynamic Programming,简称树形DP)是一种在树形结构上应用动态规划算法的技术。它利用树的递归结构,通过定义状态和状态转移方程,来求解与树相关的最优化问题,如树上的最长路径、最小路径覆盖、最大独立集等。与传统的线性动态规划相比,树形DP更侧重于利用树的子结构特性,递归地解决问题,从叶子节点到根节点或反之进行状态的累积和更新。
运用情况
- 树的最长路径问题:给定一个树,找出一条路径,使得该路径上所有边的权值之和最大。
- 数字转换问题:在不超过 n 的正整数范围内进行数字变换,求不断进行数字变换且不出现重复数字的最多变换步数。
- 树的中心问题:给定一棵树,找到一个点,使得该点到树中其他结点的最远距离最近。
- 没有上司的舞会问题:在一棵以校长为根的树中,每个职员有一个快乐指数,且没有职员愿意和直接上司一起参会,求邀请一部分职员参会使得所有参会职员的快乐指数总和最大。
- 路径问题:如求解树中两个节点间的最大距离、树的直径等。
- 子树问题:找出具有特定性质的子树,如最大权独立集、最小割集等。
- 计数问题:统计满足特定条件的子树数量,如完美子树的数量、具有特定性质的路径数量等。
- 优化问题:在树上分配资源,使得某个指标最大化或最小化,如最小化涂色成本、最大化收集的资源等。
注意事项
- 树形 DP 的 for 循环能优化就优化,比如取
j=min(size(x),m)
,k<=min(size(x),m)
之类的,否则很容易 TLE。 - 要考虑清楚不合法状态是否会对答案产生影响,如果有就要
memset(dp,-1,sizeof(dp))
和初始化,树形 DP 中跳过dp(x)(j)=-1
和dp(x)(k-j)=-1
之类情况。 - 状态定义:正确定义状态是关键,通常包括节点的选择状态(是否包含当前节点)和其他与问题相关的附加信息。
- 状态转移:明确状态之间的依赖关系,设计合理的状态转移方程,通常从子节点到父节点进行信息传递。
- 边界条件:处理好边界情况,如树的叶子节点或只有一个节点的子树。
- 记忆化搜索:由于树形DP往往涉及大量的重复计算,使用记忆化搜索可以避免重复计算,提高效率。
- 递归/迭代实现:根据具体情况选择合适的实现方式,递归通常更直观,迭代则可能在空间复杂度上有优势。
解题思路
- 确定状态表示:需要根据具体问题确定状态表示,通常是以节点为基本单位,记录每个节点的相关信息。
- 定义状态转移方程:根据问题的要求,确定状态之间的转移关系,即如何从一个状态转移到另一个状态。
- 确定边界条件:明确问题的边界情况,例如根节点的状态或者叶子节点的状态。
- 进行递归计算:根据状态转移方程,从根节点开始递归地计算每个节点的状态。
- 求解最优解:根据计算得到的状态值,求解问题的最优解。
- 分析问题:首先明确问题的求解目标,识别出问题中的"最优子结构",即问题的解可以由其子问题的解组合得出。
- 定义状态:基于问题特点,定义状态表示每个节点或子树在求解过程中的信息,通常包括是否选择该节点、子树的某些属性等。
- 设计状态转移方程:根据问题性质,确定状态如何从子树转移到父节点,即如何通过子节点的信息计算出父节点的信息。
- 初始化:确定基础情况,如叶子节点的初始状态。
- 实现:使用深度优先搜索(DFS)或广度优先搜索(BFS)遍历树,并根据状态转移方程计算每个状态的值,通常使用记忆化或递推实现。
- 回溯获取答案:根据最终状态,回溯获得问题的最优解。
AcWing 323. 战略游戏
题目描述
活动 - AcWingAcWing 323. 战略游戏 - AcWing活动 - AcWing
运行代码
cpp
#include <iostream>
#include <cstring>
using namespace std;
const int N = 2000, M = 4000;
int h[N], e[M], ne[M], idx;
int n;
int f[N][2];
int st[N];
void add(int a, int b)
{
e[idx] = b; ne[idx] = h[a]; h[a] = idx ++;
}
void dfs(int u)
{
f[u][1] = 1, f[u][0] = 0;
for(int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += f[j][1];
f[u][1] += min(f[j][0], f[j][1]);
}
}
int main()
{
while(scanf("%d", &n) == 1)
{
memset(h, -1, sizeof h);
memset(st, 0, sizeof st);
idx = 0;
for(int i = 0; i < n; i ++ )
{
int id, cnt;
scanf("%d:(%d)", &id, &cnt);
while(cnt --)
{
int ver;
scanf("%d", &ver);
add(id, ver);
st[ver] = true;
}
}
int root = 0;
while(st[root]) root ++;
dfs(root);
printf("%d\n", min(f[root][0], f[root][1]));
}
return 0;
}
代码思路
-
数据结构定义:
- 使用邻接表表示树结构,其中
h[N]
是头节点数组,e[M]
是边的终点数组,ne[M]
是下一个兄弟节点的索引数组,idx
用于记录当前使用的边的索引。 f[N][2]
是一个二维状态数组,其中f[u][0]
表示以节点u
为根的子树中选择不包含该节点的方案数,f[u][1]
表示包含该节点的方案数。st[N]
标记数组,用来记录某个节点是否已被其他节点直接连接过,辅助找到树的根节点。
- 使用邻接表表示树结构,其中
-
输入处理:
- 读取整数
n
表示节点数量。 - 接收每行输入,格式为"节点ID:(直接子节点数量) 子节点1 子节点2 ...",并构建邻接表表示的树结构。
- 读取整数
-
寻找根节点 :通过遍历
st[]
数组找到没有直接父节点的节点作为树的根,初始化为0。 -
深度优先搜索(DFS):
- 从根节点开始进行深度优先搜索,递归遍历整棵树。
- 对于每个节点
u
,先假设自己不被选中(f[u][1]=1
,表示以自己为根的子树至少有一种情况是选中自己的),不选中父节点的方案数默认为0(f[u][0]=0
)。 - 遍历所有子节点,递归调用DFS更新
f[u][0]
和f[u][1]
的值。f[u][0] += f[j][1]
意味着考虑不选当前节点时,子节点可选择不包含自己的方案数累加;f[u][1] += min(f[j][0], f[j][1])
则是考虑选当前节点时,子节点可以自由选择是否包含自己,取最小值是因为我们要找的是最小方案数。
-
输出结果 :最终,输出根节点的
f[root][0]
和f[root][1]
中的最小值,即为满足条件的最小方案数。
改进思路
- 增加注释:提高代码的可读性,特别是对于复杂逻辑的部分,通过注释说明每段代码的目的。
- 变量命名清晰:使变量名更具描述性,便于理解每个变量的用途。
- 常量分离:将数组大小等硬编码的常量提取为定义在顶部的常量,便于维护和修改。
- 函数封装:将部分功能(如读取树的构建)封装成独立的函数,提高代码模块化。
- 去除全局变量的过度依赖:尽量减少全局变量的使用,使用函数参数传递必要信息。62.