深入学习C++:什么是树状DP

深入学习C++:什么是树状DP

一. 基础知识

树状DP的定义:

树形 DP 是动态规划在树结构上的应用,其核心思想是利用树的递归特性,通过子树的状态推导父节点的状态,最终求解整个树的最优解。由于树是一种无环的连通图,且具有明确的层次关系(父节点与子节点),天然适合用递归或递推的方式处理,因此树形 DP 在树相关的优化问题中被广泛使用。

树形 DP 的核心特点

1. 依赖树的递归结构:树的每个节点可以看作其子树的根,子树的解是父节点解的基础,因此通常采用自底向上(后序遍历)或自顶向下(前序遍历)的方式计算。

2. 状态定义与子树强相关:状态设计需体现 “当前节点 + 子树状态” 的关联,例如 “以节点u为根的子树中,满足某条件的最优值”。

3. 无后效性:树的层次结构确保子树的状态不会影响非子树节点的状态,符合动态规划 “无后效性” 的要求。

树形 DP 的基本步骤

1. 选择根节点:树是无向图,通常任选一个节点作为根(如节点 1),将树转化为 “有向树”(父节点指向子节点),简化递归逻辑。

2. 定义状态:根据问题需求,设计状态dp[u][...],其中u表示当前节点,方括号内的参数表示与u相关的附加状态(如 “选 / 不选u”“子树是否满足某条件” 等)。

3. 推导状态转移方程:基于子节点的状态,计算父节点的状态。例如,若u的子节点为v1, v2, ..., vk,则dp[u]的取值依赖于dp[v1], dp[v2], ..., dp[vk]的组合。

4. 递归计算:通过后序遍历(先处理子树,再处理父节点)或前序遍历(先处理父节点,再传递状态到子树),完成所有节点的状态计算。

5. 求解最终答案:根据根节点的状态(或结合其他节点的状态)得到整个树的最优解。

树状 DP 的典型应用场景与案例

树形 DP 的应用场景多为 “树的优化问题”,以下是几个经典案例:

案例 1:树上最大独立集

问题:在一棵树上选择若干节点,使得任意两个选中节点不相邻,求选中节点的最大数量(独立集:无相邻节点的子集)。

  • 状态定义

    • dp[u][0]:以u为根的子树中,不选u 时的最大独立集大小。
    • dp[u][1]:以u为根的子树中,u 时的最大独立集大小。
  • 状态转移

    • 若选u,则其所有子节点不能选(否则相邻),因此:
      dp[u][1] = 1 + sum(dp[v][0] for v in u的子节点)
    • 若不选u,则其每个子节点可选或不选(取最大值),因此:
      dp[u][0] = sum(max(dp[v][0], dp[v][1]) for v in u的子节点)
  • 最终答案max(dp[root][0], dp[root][1])root为任选的根节点)。

案例 2:树的直径

问题:树中任意两节点的最长路径(直径)长度(边数或节点数)。

  • 状态定义

    • dp[u]:以u为根的子树中,从u出发到子树中某节点的最长路径长度。
  • 状态转移
    对节点u的所有子节点v,计算dp[v] + 1+1表示uv的边),取其中的前两大值ab,则经过u的最长路径为a + b。遍历所有节点,最大的a + b即为树的直径。

  • 计算方式:通过后序遍历计算dp[u],同时记录全局最大的a + b

案例 3:树上最小支配集

问题:选择最少的节点,使得树中所有节点要么被选中,要么与选中节点相邻(支配集)。

  • 状态定义

    • dp[u][0]u被选中时,子树的最小支配集大小。
    • dp[u][1]u未被选中,但被其子节点支配(至少一个子节点选中)时,子树的最小支配集大小。
    • dp[u][2]u未被选中,但被其父节点支配(所有子节点均未选中)时,子树的最小支配集大小。
  • 状态转移

    • dp[u][0]u选中后,子节点可自由选择(选 / 不选 / 被支配),取最小值之和加 1:
      dp[u][0] = 1 + sum(min(dp[v][0], dp[v][1], dp[v][2]) for v in 子节点)
    • dp[u][1]u未被选中,但至少一个子节点选中。需保证存在子节点v满足dp[v][0],其余子节点取min(dp[v][0], dp[v][1])
      dp[u][1] = sum(min(dp[v][0], dp[v][1])) + (若所有子节点都未选,则需补选一个子节点,取最小的dp[v][0] - min(dp[v][0], dp[v][1]))
    • dp[u][2]u未被选中,依赖父节点支配,因此子节点必须被自身或其子女支配(即子节点不能依赖u):
      dp[u][2] = sum(min(dp[v][0], dp[v][1]) for v in 子节点)
  • 最终答案min(dp[root][0], dp[root][1])(根节点无父节点,因此dp[root][2]无效)。

二. 树形 DP 的关键技巧

根的选择:树是无向的,通常任选一个节点(如1号节点)作为根,通过 DFS 将树转化为 “有向父子树”,简化递归。

状态设计:状态需精准反映 “节点与子树的关系”,例如 “选 / 不选”“是否被支配” 等,避免遗漏关键信息。

遍历顺序:多数问题采用后序遍历(先处理子树,再合并结果到父节点),少数问题(如传递父节点状态到子树)需前序遍历

空间优化:树的节点数为n时,状态数组通常为O(n)O(n×k)k为状态维度),时间复杂度多为O(n)(每个节点处理一次)。

三. 示例

问题描述:

在一棵二叉树上,每个节点代表一户人家,节点的值表示该户的财物数量。要求不能抢劫相邻的两户人家(即父节点和子节点不能同时被抢劫),求能抢劫到的最大财物总和。

树形 DP 设计:

1. 状态定义

对每个节点 u,定义两种状态:

  • dp[u][0]:不抢劫节点 u 时,以 u 为根的子树能获得的最大财物。
  • dp[u][1]:抢劫节点 u 时,以 u 为根的子树能获得的最大财物。
2. 状态转移方程
  • 若抢劫节点 udp[u][1]):
    则其左、右子节点 不能被抢劫,因此:
    dp[u][1] = u.val + dp[left][0] + dp[right][0]

  • 若不抢劫节点 udp[u][0]):
    则其左、右子节点 可抢或不抢(取最大值),因此:
    dp[u][0] = max(dp[left][0], dp[left][1]) + max(dp[right][0], dp[right][1])

3. 递归计算

采用 后序遍历 方式:

  1. 先递归计算左、右子树的状态。
  2. 再根据子树状态计算当前节点的状态。
  3. 最终答案为根节点两种状态的最大值:max(dp[root][0], dp[root][1])

代码如下: 

#include <iostream>
#include <algorithm>
using namespace std;

// 二叉树节点结构
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 递归计算每个节点的两种状态
pair<int, int> dfs(TreeNode* node) {
    if (node == nullptr) {
        return {0, 0}; // 空节点:不抢和抢的收益都是0
    }
    
    // 递归计算左子树
    auto [left_not_rob, left_rob] = dfs(node->left);
    // 递归计算右子树
    auto [right_not_rob, right_rob] = dfs(node->right);
    
    // 计算当前节点的两种状态
    int not_rob = max(left_not_rob, left_rob) + max(right_not_rob, right_rob);
    int rob = node->val + left_not_rob + right_not_rob;
    
    return {not_rob, rob};
}

int rob(TreeNode* root) {
    auto [not_rob_root, rob_root] = dfs(root);
    return max(not_rob_root, rob_root);
}

// 测试示例
int main() {

    TreeNode* root = new TreeNode(3);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->right = new TreeNode(3);
    root->right->right = new TreeNode(1);
    
    cout << "最大抢劫金额:" << rob(root) << endl; // 输出 7(抢劫3 + 3 + 1 或 2 + 3)
    
    return 0;
}

复杂度分析:

  • 时间复杂度O(n),其中 n 是树的节点数。每个节点仅被访问一次。
  • 空间复杂度O(h)h 是树的高度。递归栈的深度取决于树的高度(平衡树为 log n,最坏情况为 n)。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值