目录
树形DP
定义
- 树形 DP 是在树上进行的动态规划。
- 它利用树的结构特点,通过递归或迭代的方式,在每个节点上进行状态计算和转移,以求解最优解。
运用情况
- 树的最长路径问题:找到树中一条路径,使得路径上所有边的权值之和最大。
- 数字转换问题:在给定的数字范围内,通过数字变换找到最长的变换步数。
- 树的中心问题:找到树中到其他节点最远距离最近的点。
- 没有上司的舞会问题:在满足条件的前提下,选择部分职员参加舞会,使得快乐指数总和最大。
注意事项
- 状态表示:选择合适的状态表示方式,能够简洁地描述问题的本质。
- 状态转移:确定状态之间的转移关系,确保能够正确地计算最优解。
- 初始化:根据问题的要求,对状态进行合理的初始化。
- 边界情况:处理好树的边界节点,确保算法的正确性。
- 优化:根据具体问题,考虑使用合适的优化技巧,如记忆化搜索、剪枝等。
解题思路
- 确定问题的状态:根据问题的要求,确定每个节点的状态。
- 建立状态转移方程:根据树的结构和问题的约束条件,建立状态之间的转移关系。
- 确定边界情况:处理好树的叶子节点或特殊情况的状态。
- 选择合适的遍历顺序:根据问题的特点,选择合适的遍历树的顺序,如前序遍历、后序遍历或层次遍历。
- 计算最优解:通过遍历树,根据状态转移方程计算每个节点的最优解,并最终得到整个树的最优解。
AcWing 285. 没有上司的舞会
题目描述
运行代码
#include <iostream>
#include <vector>
using namespace std;
const int N = 6001;
vector<int> adj[N];
int happy[N];
int dp[N][2];
void dfs(int u, int par) {
dp[u][1] = happy[u];
dp[u][0] = 0;
for (int v : adj[u]) {
if (v!= par) {
dfs(v, u);
dp[u][1] += dp[v][0];
dp[u][0] += max(dp[v][0], dp[v][1]);
}
}
}
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> happy[i];
for (int i = 1; i < n; i++) {
int l, k;
cin >> l >> k;
adj[k].push_back(l);
adj[l].push_back(k);
}
dfs(1, -1);
cout << max(dp[1][0], dp[1][1]) << endl;
return 0;
}
代码思路
-
数据结构定义:
- 定义一个邻接列表
adj[N]
来存储树的结构,其中adj[u]
存放节点u
的所有子节点。 happy[N]
数组存储每个节点的幸福值。dp[N][2]
是一个二维动态规划数组,其中dp[u][0]
表示以节点u
为根的子树中,不包含节点u
的最大幸福值路径和;dp[u][1]
表示以节点u
为根的子树中,包含节点u
的最大幸福值路径和。
- 定义一个邻接列表
-
DFS(深度优先搜索):
- 函数
dfs(u, par)
遍历以u
为根的子树。par
表示u
的父节点,用于避免回头访问。 - 对于
u
的每个子节点v
(不包括父节点par
),递归调用dfs(v, u)
。 - 更新
dp[u][1]
为当前节点的幸福值加上所有不包含当前节点的子树路径和的最大值(即dp[v][0]
)。 - 更新
dp[u][0]
为所有子树路径和的最大值,可以选择包含或不包含当前子树的根节点(即max(dp[v][0], dp[v][1])
)。
- 函数
-
计算结果:
- 在遍历完成后,根节点
1
的dp[1][0]
和dp[1][1]
中较大者即为所求的最大幸福值路径和。
- 在遍历完成后,根节点
改进思路
- 常量优化:
N = 6001
是一个相对较大的常量,如果题目保证了节点数的范围,可以适当减小这个值来节省空间。 - 输入验证:实际应用中,添加对输入数据的验证,如检查节点数是否在合理范围内,边的定义是否构成一棵有效的树等,可以增强程序的健壮性。
- 内存使用:如果树的深度非常大,可以考虑使用迭代法(如栈)来替代递归,减少调用栈的使用,避免栈溢出。
- 添加注释:为了提高代码的可读性,添加必要的注释来解释每个主要步骤的目的。
- 明确常量意义:虽然在这个上下文中
N
的意义相对明确,但给常量一个更具描述性的名称可以帮助理解代码。 - 输入验证:尽管题目假设了输入的有效性,但在实际编程中,增加对输入数据的基本验证是一个好习惯。
- 使用
const
引用避免拷贝:在dfs
函数中,将adj[u]
的迭代器定义为const
引用可以避免不必要的副本创建。
改进代码(AI)
#include <iostream>
#include <vector>
using namespace std;
const int MAX_NODES = 6001; // 更具描述性的常量名
vector<int> adj[MAX_NODES];
int happy[MAX_NODES];
int dp[MAX_NODES][2];
// 深度优先搜索,计算以u为根的子树的最大幸福值路径和
void dfs(int u, int parent) {
// 初始化当前节点的两种状态
dp[u][1] = happy[u]; // 包含u的最大路径和
dp[u][0] = 0; // 不包含u的最大路径和
// 遍历子节点
for (const int &v : adj[u]) { // 使用const &避免拷贝
if (v != parent) {
dfs(v, u); // 递归搜索子节点
// 更新u的dp值
dp[u][1] += dp[v][0]; // 考虑子树不含根节点的最大和
dp[u][0] += max(dp[v][0], dp[v][1]); // 子树最大和,选择包含或不包含子树根
}
}
}
int main() {
int n;
cin >> n;
if (n < 1 || n > MAX_NODES) { // 输入验证
cerr << "Invalid node count!" << endl;
return -1;
}
// 读取每个节点的幸福值
for (int i = 1; i <= n; ++i)
cin >> happy[i];
// 构建树的邻接表
for (int i = 1; i < n; ++i) {
int l, k;
cin >> l >> k;
adj[k].push_back(l);
adj[l].push_back(k); // 注意:无向图需双向添加
}
// 从根节点开始深度优先搜索
dfs(1, -1);
// 输出结果
cout << max(dp[1][0], dp[1][1]) << endl; // 输出最大幸福值路径和
return 0;
}
其它代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6010;
int h[N], e[N], ne[N], idx;
int happy[N];
int f[N][2];
bool has_fa[N];
void add(int a, int b)
{
e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}
void dfs(int u)
{
f[u][1] = happy[u];
for(int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][1] += f[j][0];
f[u][0] += max(f[j][0], f[j][1]);
}
}
int main()
{
int n; cin >> n;
for(int i = 1; i <= n; i++) cin >> happy[i];
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i++)
{
int a, b; cin >> a >> b;
add(b, a);
has_fa[a] = true;
}
int root = 1;
while(has_fa[root]) root++;
dfs(root);
cout << max(f[root][0], f[root][1]) << endl;
return 0;
}
代码思路
-
数据结构与初始化:
- 使用邻接表表示树的结构,
h[N]
存储每个节点的首条边索引,e[N]
和ne[N]
分别存储边的终点和下一条边的索引,idx
作为边的计数器。 happy[N]
数组存储每个节点的幸福值。f[N][2]
二维数组,其中f[u][0]
表示以节点u
为根的子树中不包含u
的最大权值和路径,f[u][1]
表示包含u
的最大权值和路径。has_fa[N]
数组标记节点是否有父节点,用于后续寻找根节点。
- 使用邻接表表示树的结构,
-
建立邻接表:通过
add(a, b)
函数添加边,这里建立的是从孩子指向父节点的边,因为输入时给的是每个节点的父节点。 -
深度优先搜索(DFS):
dfs(u)
函数递归遍历以节点u
为根的子树,计算f[u][0]
和f[u][1]
。对于每个子节点j
,先递归调用dfs(j)
,然后根据子节点的最优解更新当前节点的最优解。f[u][1] += f[j][0]
表示当前节点选中的情况,f[u][0] += max(f[j][0], f[j][1])
表示当前节点不选或选中的情况,取两者的较大值累加。 -
寻找根节点:因为输入的边是父节点指向子节点,可以通过一个标志数组
has_fa
来找到没有父节点的节点,即树的根。 -
输出结果:在遍历完树后,根节点的
f[root][0]
和f[root][1]
中较大的一个就是所求的最大权值和。
利用动态规划的思想来解决树上的问题,通过两次遍历(一次构建树,一次DFS计算最优解)实现了问题的求解。
标签:int,adj,dfs,DP,happy,285,节点,dp,AcWing From: https://blog.csdn.net/u014114223/article/details/139815582