树形DP——AcWing 285. 没有上司的舞会

目录

树形DP

定义

运用情况

注意事项

解题思路

AcWing 285. 没有上司的舞会 

题目描述

运行代码

代码思路

改进思路

改进代码(AI)

其它代码

代码思路

树形DP

定义

  • 树形 DP 是在树上进行的动态规划。
  • 它利用树的结构特点,通过递归或迭代的方式,在每个节点上进行状态计算和转移,以求解最优解。

运用情况

  • 树的最长路径问题:找到树中一条路径,使得路径上所有边的权值之和最大。
  • 数字转换问题:在给定的数字范围内,通过数字变换找到最长的变换步数。
  • 树的中心问题:找到树中到其他节点最远距离最近的点。
  • 没有上司的舞会问题:在满足条件的前提下,选择部分职员参加舞会,使得快乐指数总和最大。

注意事项

  • 状态表示:选择合适的状态表示方式,能够简洁地描述问题的本质。
  • 状态转移:确定状态之间的转移关系,确保能够正确地计算最优解。
  • 初始化:根据问题的要求,对状态进行合理的初始化。
  • 边界情况:处理好树的边界节点,确保算法的正确性。
  • 优化:根据具体问题,考虑使用合适的优化技巧,如记忆化搜索、剪枝等。

解题思路

  • 确定问题的状态:根据问题的要求,确定每个节点的状态。
  • 建立状态转移方程:根据树的结构和问题的约束条件,建立状态之间的转移关系。
  • 确定边界情况:处理好树的叶子节点或特殊情况的状态。
  • 选择合适的遍历顺序:根据问题的特点,选择合适的遍历树的顺序,如前序遍历、后序遍历或层次遍历。
  • 计算最优解:通过遍历树,根据状态转移方程计算每个节点的最优解,并最终得到整个树的最优解。

AcWing 285. 没有上司的舞会 

题目描述

285. 没有上司的舞会 - AcWing题库

运行代码

#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;
}

代码思路

  1. 数据结构定义:

    • 定义一个邻接列表adj[N]来存储树的结构,其中adj[u]存放节点u的所有子节点。
    • happy[N]数组存储每个节点的幸福值。
    • dp[N][2]是一个二维动态规划数组,其中dp[u][0]表示以节点u为根的子树中,不包含节点u的最大幸福值路径和;dp[u][1]表示以节点u为根的子树中,包含节点u的最大幸福值路径和。
  2. 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]))。
  3. 计算结果:

    • 在遍历完成后,根节点1dp[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;
}

代码思路

  1. 数据结构与初始化:

    • 使用邻接表表示树的结构,h[N]存储每个节点的首条边索引,e[N]ne[N]分别存储边的终点和下一条边的索引,idx作为边的计数器。
    • happy[N]数组存储每个节点的幸福值。
    • f[N][2]二维数组,其中f[u][0]表示以节点u为根的子树中不包含u的最大权值和路径,f[u][1]表示包含u的最大权值和路径。
    • has_fa[N]数组标记节点是否有父节点,用于后续寻找根节点。
  2. 建立邻接表:通过add(a, b)函数添加边,这里建立的是从孩子指向父节点的边,因为输入时给的是每个节点的父节点。

  3. 深度优先搜索(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])表示当前节点不选或选中的情况,取两者的较大值累加。

  4. 寻找根节点:因为输入的边是父节点指向子节点,可以通过一个标志数组has_fa来找到没有父节点的节点,即树的根。

  5. 输出结果:在遍历完树后,根节点的f[root][0]f[root][1]中较大的一个就是所求的最大权值和。

利用动态规划的思想来解决树上的问题,通过两次遍历(一次构建树,一次DFS计算最优解)实现了问题的求解。

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
树形动态规划(Tree DP)是一种常用的动态规划算法,用于解决树结构相关的问题。在Python中,可以使用递归或者迭代的方式实现树形DP树形DP的基本思想是,从树的叶子节点开始,逐层向上计算每个节点的状态,并利用已经计算过的节点状态来更新当前节点的状态。这样可以通过自底向上的方式,逐步计算出整个树的最优解。 下面是一个简单的示例,演示如何使用树形DP解决一个二叉树中节点权值之和的最大值问题: ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_sum(root): if root is None: return 0 # 递归计算左右子树的最大权值和 left_sum = max_sum(root.left) right_sum = max_sum(root.right) # 当前节点的最大权值和为当前节点值加上左右子树中较大的权值和 return root.val + max(left_sum, right_sum) # 构建一个二叉树 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.left.left = TreeNode(4) root.left.right = TreeNode(5) # 计算二叉树中节点权值之和的最大值 result = max_sum(root) print(result) ``` 这段代码中,我们定义了一个`TreeNode`类来表示二叉树的节点,其中`val`表示节点的权值,`left`和`right`分别表示左子节点和右子节点。`max_sum`函数使用递归的方式计算二叉树中节点权值之和的最大值,通过比较左右子树的最大权值和来确定当前节点的最大权值和。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

筱姌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值