当前位置: 首页 > article >正文

算法精讲--动态规划(三):树形DP与状态机模型

🚀 算法精讲–动态规划(三):树形DP与状态机模型


📚 前情回顾:前两期咱们搞定 单序列DP多维DP,今天解锁更带劲的玩法!建议先预习:337. 打家劫舍 III122. 买卖股票的最佳时机 II

🌟 知识图谱

在这里插入图片描述


一、🌲 树形DP

1.1 打家劫舍III(337. 打家劫舍 III)

🔍 Step1:审题分析
  • 核心约束 :父子节点不能同时被偷
  • 目标 :二叉树结构中的最大偷窃金额
  • 特殊边界 :空树返回0
📐 Step2:方案设计

在这里插入图片描述

状态定义

  • int[2] res:res[0]不选当前节点,res[1]选当前节点

转移方程

res[][0] = max([0],[1]) + max([0],[1])  
res[][1] =.val +[0] +[0]  
🚀 Step3:执行验证
class Solution {  
    public int rob(TreeNode root) {  
        int[] res = dfs(root);  
        return Math.max(res[0], res[1]);  
    }  
  
    private int[] dfs(TreeNode node) {  
        if (node == null) return new int[2];  
        int[] left = dfs(node.left);  
        int[] right = dfs(node.right);  
  
        int notPick = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);  
        int pick = node.val + left[0] + right[0];  
        return new int[]{notPick, pick};  
    }  
}  

测试案例

输入:root = [3,2,3,null,3,null,1]  
输出:7(选根节点3、左子节点2的右子节点3、右子节点3的右子节点1)  
🎯 Step4:总结优化(后序遍历)
// 优化点:省略显式存储左右子树状态  
class Solution {  
    public int rob(TreeNode root) {  
        int[] res = postOrder(root);  
        return Math.max(res[0], res[1]);  
    }  
  
    private int[] postOrder(TreeNode node) {  
        if (node == null) return new int[2];  
        int[] left = postOrder(node.left);  
        int[] right = postOrder(node.right);  
        return new int[]{  
            Math.max(left[0], left[1]) + Math.max(right[0], right[1]),  
            node.val + left[0] + right[0]  
        };  
    }  
}  

优化解析

  1. 空间复用 :直接利用后序遍历返回值
  2. 时间复杂度 :O(n)
  3. 逻辑精简 :合并状态更新步骤

1.2 二叉树直径(543. 二叉树的直径)

🔍 Step1:审题分析
  • 核心定义:直径是任意两节点间最长路径(边数)
  • 关键发现:直径 = 左子树深度 + 右子树深度
  • 特殊边界:空树或单节点时直径为0
📐 Step2:方案设计

在这里插入图片描述

🚀 Step3:执行验证
class Solution {  
    private int maxDiameter = 0;  
  
    public int diameterOfBinaryTree(TreeNode root) {  
        calculateDepth(root);  
        return maxDiameter;  
    }  
  
    // 显式存储每个节点的左右深度  
    private int calculateDepth(TreeNode node) {  
        if (node == null) return 0;  
  
        int leftDepth = calculateDepth(node.left);  
        int rightDepth = calculateDepth(node.right);  
  
        // 计算当前节点为根的直径  
        int currentDiameter = leftDepth + rightDepth;  
        maxDiameter = Math.max(maxDiameter, currentDiameter);  
  
        return Math.max(leftDepth, rightDepth) + 1;  
    }  
}  

测试案例

输入:root = [1,2,3,4,5]  
输出:3(路径4->2->1->3 或 5->2->1->3)  
🎯 Step4:总结优化(逻辑融合)
class Solution {  
    int max = 0;  
  
    public int diameterOfBinaryTree(TreeNode root) {  
        depth(root);  
        return max;  
    }  
  
    // 将深度计算与直径统计融合  
    private int depth(TreeNode node) {  
        if (node == null) return 0;  
        int left = depth(node.left);  
        int right = depth(node.right);  
        max = Math.max(max, left + right); // 实时更新最大值  
        return Math.max(left, right) + 1;  
    }  
}  

优化解析

  1. 去冗余存储:不再显式存储每个节点的左右深度
  2. 时间复杂度:O(n)(每个节点访问一次)
  3. 空间复杂度:O(h)(递归栈深度,h为树高)

1.3 监控二叉树

🔍 Step1:审题分析
  • 监控规则:摄像头可覆盖自身、父节点、子节点
  • 目标:最少摄像头覆盖所有节点
  • 特殊边界:空树无需摄像头,单节点需1个摄像头
📐 Step2:方案设计

在这里插入图片描述

🚀 Step3:执行验证(显式状态)
class Solution {  
    int cameras = 0;  
  
    public int minCameraCover(TreeNode root) {  
        int rootStatus = dfs(root);  
        return rootStatus == 0 ? cameras + 1 : cameras;  
    }  
  
    // 0-未覆盖 1-已安装 2-已覆盖  
    private int dfs(TreeNode node) {  
        if (node == null) return 2;  
  
        int left = dfs(node.left);  
        int right = dfs(node.right);  
  
        if (left == 0 || right == 0) {  
            cameras++;  
            return 1;  
        }  
        if (left == 1 || right == 1) return 2;  
        return 0;  
    }  
}  

测试案例

输入:root = [0,0,null,0,0]  
输出:1(根节点安装摄像头即可覆盖所有节点)  
🎯 Step4:总结优化(逻辑精简)
class Solution {  
    int res = 0;  
  
    public int minCameraCover(TreeNode root) {  
        return (dfs(root) == 0 ? 1 : 0) + res;  
    }  
  
    private int dfs(TreeNode node) {  
        if (node == null) return 2;  
        int left = dfs(node.left), right = dfs(node.right);  
  
        if (left == 0 || right == 0) {  
            res++;  
            return 1;  
        }  
        return (left == 1 || right == 1) ? 2 : 0;  
    }  
}  

优化解析

  1. 状态合并:将根节点特殊判断合并到主逻辑中
  2. 代码精简:减少条件判断层级
  3. 贪心策略:优先在父节点安装以覆盖更多子节点


二、⚙️ 状态机模型

2.1 股票含冷冻期(309. 最佳买卖时机含冷冻期)

🔍 Step1:审题分析
  • 交易规则:卖出后需等待一天才能买入
  • 目标:获取最大利润
  • 特殊边界:单日价格直接返回0
📐 Step2:方案设计

在这里插入图片描述

🚀 Step3:执行验证(三维DP)
class Solution {  
    public int maxProfit(int[] prices) {  
        int n = prices.length;  
        int[][][] dp = new int[n][2][2]; // [天数][是否持有][是否冷冻]  
  
        // 初始化  
        dp[0][1][0] = -prices[0];  
  
        for (int i = 1; i < n; i++) {  
            // 当前持有股票:昨天持有未卖出 或 今天买入(需确保非冷冻期)  
            dp[i][1][0] = Math.max(dp[i-1][1][0], dp[i-1][0][0] - prices[i]);  
      
            // 当前未持有且处于冷冻期:昨天卖出  
            dp[i][0][1] = dp[i-1][1][0] + prices[i];  
      
            // 当前未持有且非冷冻期:昨天未持有(可能是冷冻期结束或保持)  
            dp[i][0][0] = Math.max(dp[i-1][0][0], dp[i-1][0][1]);  
        }  
        return Math.max(dp[n-1][0][0], dp[n-1][0][1]);  
    }  
}  
🎯 Step4:总结优化(状态压缩)
class Solution {  
    public int maxProfit(int[] prices) {  
        int hold = -prices[0], cold = 0, not = 0;  
        for (int i = 1; i < prices.length; i++) {  
            int preHold = hold;  
            hold = Math.max(hold, not - prices[i]);  
            not = Math.max(not, cold);  
            cold = preHold + prices[i];  
        }  
        return Math.max(cold, not);  
    }  
}  

优化解析

  1. 空间压缩:从 O(n) 降为 O(1)
  2. 状态合并:将三维状态压缩为三个变量
  3. 循环优化:直接遍历价格数组,无需维护完整DP表

2.2 买卖股票II(122. 最佳买卖时机 II)

🔍 Step1:审题分析
  • 交易规则 :允许无限次交易,当天可买卖
  • 目标 :最大化利润
  • 关键发现 :累积所有上涨波段收益
📐 Step2:方案设计

在这里插入图片描述

状态定义

  • hold:持有股票的最大收益
  • notHold:未持有股票的最大收益
🚀 Step3:执行验证(二维DP)
class Solution {  
    public int maxProfit(int[] prices) {  
        int n = prices.length;  
        int[][] dp = new int[n][2];  
        dp[0][0] = 0;  
        dp[0][1] = -prices[0];  
      
        for (int i = 1; i < n; i++) {  
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);  
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);  
        }  
        return dp[n-1][0];  
    }  
}  
🎯 Step4:总结优化(滚动变量)
class Solution {  
    public int maxProfit(int[] prices) {  
        int hold = -prices[0], notHold = 0;  
        for (int i = 1; i < prices.length; i++) {  
            int preHold = hold;  
            hold = Math.max(hold, notHold - prices[i]);  
            notHold = Math.max(notHold, preHold + prices[i]);  
        }  
        return notHold;  
    }  
}  

优化解析

  1. 空间压缩 :从O(n)降为O(1)
  2. 状态转移 :直接利用前序状态
  3. 计算优化 :消除数组访问开销


三、📊 方法论对比表

问题类型基础版复杂度优化版复杂度优化技巧
树形DPO(n) / O(n)O(n) / O(h)后序遍历 + 状态压缩
状态机模型O(n) / O(n)O(n) / O(1)滚动变量法 + 状态合并

优化原则

  1. 空间换时间:优先保证时间复杂度最优
  2. 状态精简:合并冗余状态,保留核心状态转移
  3. 遍历顺序:利用问题特性(如树的后序遍历顺序)

四、💡 思维训练

拓展问题:2246. 相邻字符不同的最长路径

在这里插入图片描述

提示

  1. 维护每个节点的子节点中字符不同的最长路径
  2. 全局变量记录最大值
  3. 注意相邻节点字符不同的约束条件

五、📦 下期剧透

《动态规划(四):背包九讲》

预习指南

  1. 01背包经典题:分割等和子集
  2. 完全背包变形:零钱兑换II
  3. 多维约束题:一和零

复杂度预对比

在这里插入图片描述

🌟 思维拓展:尝试用树形DP思想解决2246. 相邻字符不同的最长路径,答案下期揭晓!


📢 互动环节:你在树形DP中遇到过哪些坑?


http://www.kler.cn/a/561557.html

相关文章:

  • 常见的keil 编译报警记录。
  • Windows Server 搭建 RADIUS 认证服务器
  • 【Leetcode】动态规划:从经典例题剖析解题精要
  • SQL进阶实战技巧:汽车转向次数分析 | 真实场景案例
  • 计算机网络之路由协议(OSPF路由协议)
  • HTTP/HTTPS 服务端口监测的简易实现
  • 2025年智能电力系统与数据驱动创新国际学术会议(IPSDDI 2025)
  • 从两地三中心到多地多中心,OceanBase如何实现金融级高可用
  • 【Python专栏】Python 开发-pycharm安装
  • 接上一主题,在Qt中,用信号代替函数指针,最终目标都是能直接使用lambda表达式,效果一样。
  • 【LLM】本地部署LLM大语言模型+可视化交互聊天,附常见本地部署硬件要求(以Ollama+OpenWebUI部署DeepSeekR1为例)
  • 【Linux网络编程】 HTTP协议
  • C++的异步相关操作
  • 信创终端上如何将PDF文件转为OFD文件
  • [java基础-JVM篇]2_垃圾收集器与内存分配策略
  • 从DeepSeek大爆发看AI革命困局:大模型如何突破算力囚笼与信任危机?
  • 量子计算的基本运算:Hadamard 门、CNOT 门、Pauli 门详解
  • 深度学习进阶:构建多层神经网络
  • Vue 3的Proxy比Vue 2的Object.defineProperty有哪些优势?
  • 和Claude对战黑白棋!一起开发AI对弈游戏