动态规划进阶:状态机DP深度解析

1. 状态机DP概述

状态机DP是动态规划的一种特殊形式,通过定义多个状态以及状态之间的转移关系来解决问题。这类问题通常涉及状态之间的相互转换,每个状态代表系统在某一时刻的特定情况。

2. 状态机DP基本概念

2.1 状态机模型特点

  • 多状态:系统在不同时刻可能处于不同状态
  • 状态转移:状态之间按照特定规则转移
  • 状态依赖:当前状态的值依赖前一时刻的状态

2.2 通用模板

python 复制代码
def state_machine_dp_template(prices):
    n = len(prices)
    # 定义状态数组
    dp = [[0] * k for _ in range(n)]  # k为状态数量
    
    # 初始化第0天的状态
    dp[0][0] = base_value_0
    dp[0][1] = base_value_1
    # ... 其他状态初始化
    
    for i in range(1, n):
        # 根据状态转移方程更新每个状态
        dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
        dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
        # ... 其他状态转移
    
    return dp[n-1][target_state]

3. 买卖股票问题系列

3.1 买卖股票的最佳时机 I (LeetCode 121)

问题描述:只允许完成一笔交易(买入和卖出)。

状态定义
  • dp[i][0]:第i天不持有股票的最大利润
  • dp[i][1]:第i天持有股票的最大利润
状态转移方程
复制代码
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])  # 卖出或继续不持有
dp[i][1] = max(dp[i-1][1], -prices[i])              # 买入或继续持有(注意:只能买一次)
Python实现
python 复制代码
def maxProfit_I(prices):
    """
    买卖股票的最佳时机 I - 只能交易一次
    """
    if not prices:
        return 0
    
    n = len(prices)
    # 两种状态:0-不持有,1-持有
    dp = [[0] * 2 for _ in range(n)]
    
    # 初始化
    dp[0][0] = 0           # 第0天不持有,利润为0
    dp[0][1] = -prices[0]  # 第0天持有,需要买入
    
    for i in range(1, n):
        # 第i天不持有:要么前一天就不持有,要么前一天持有今天卖出
        dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
        # 第i天持有:要么前一天就持有,要么今天买入(注意只能买一次)
        dp[i][1] = max(dp[i-1][1], -prices[i])
    
    return dp[n-1][0]  # 最后一天不持有股票

#### 空间优化版本
def maxProfit_I_optimized(prices):
    if not prices:
        return 0
    
    n = len(prices)
    # 只维护两个变量
    dp0 = 0           # 不持有股票的最大利润
    dp1 = -prices[0]  # 持有股票的最大利润
    
    for i in range(1, n):
        # 保存前一天的值,避免被覆盖
        prev_dp0 = dp0
        prev_dp1 = dp1
        
        dp0 = max(prev_dp0, prev_dp1 + prices[i])
        dp1 = max(prev_dp1, -prices[i])  # 只能买一次,所以是-prices[i]
    
    return dp0
Java实现
java 复制代码
public class BestTimeToBuySellStockI {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        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], -prices[i]);
        }
        
        return dp[n-1][0];
    }
    
    // 空间优化版本
    public int maxProfitOptimized(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        int dp0 = 0;
        int dp1 = -prices[0];
        
        for (int i = 1; i < prices.length; i++) {
            int prevDp0 = dp0;
            int prevDp1 = dp1;
            
            dp0 = Math.max(prevDp0, prevDp1 + prices[i]);
            dp1 = Math.max(prevDp1, -prices[i]);
        }
        
        return dp0;
    }
}

3.2 买卖股票的最佳时机 II (LeetCode 122)

问题描述:可以完成多次交易(买入和卖出)。

状态转移方程
复制代码
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])  # 区别:可以多次买入
Python实现
python 复制代码
def maxProfit_II(prices):
    """
    买卖股票的最佳时机 II - 无限次交易
    """
    if not prices:
        return 0
    
    n = len(prices)
    dp = [[0] * 2 for _ in range(n)]
    
    dp[0][0] = 0
    dp[0][1] = -prices[0]
    
    for i in range(1, n):
        dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
        dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])  # 关键变化
    
    return dp[n-1][0]

#### 贪心解法(更简单)
def maxProfit_II_greedy(prices):
    """
    贪心解法:只要有利润就交易
    """
    profit = 0
    for i in range(1, len(prices)):
        if prices[i] > prices[i-1]:
            profit += prices[i] - prices[i-1]
    return profit
Java实现
java 复制代码
public class BestTimeToBuySellStockII {
    // 状态机DP解法
    public int maxProfitDP(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        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];
    }
    
    // 贪心解法
    public int maxProfitGreedy(int[] prices) {
        int profit = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > prices[i-1]) {
                profit += prices[i] - prices[i-1];
            }
        }
        return profit;
    }
}

3.3 买卖股票的最佳时机 III (LeetCode 123)

问题描述:最多完成两笔交易。

状态定义
  • dp[i][0]:未进行过任何操作
  • dp[i][1]:第一次买入后
  • dp[i][2]:第一次卖出后
  • dp[i][3]:第二次买入后
  • dp[i][4]:第二次卖出后
Python实现
python 复制代码
def maxProfit_III(prices):
    """
    买卖股票的最佳时机 III - 最多交易两次
    """
    if not prices:
        return 0
    
    n = len(prices)
    # 5种状态
    dp = [[0] * 5 for _ in range(n)]
    
    # 初始化
    dp[0][0] = 0           # 未操作
    dp[0][1] = -prices[0]  # 第一次买入
    dp[0][2] = 0           # 第一次卖出(不可能当天买入卖出)
    dp[0][3] = -prices[0]  # 第二次买入(实际上不可能,但需要初始化为负无穷)
    dp[0][4] = 0           # 第二次卖出
    
    for i in range(1, n):
        # 状态0:保持未操作
        dp[i][0] = dp[i-1][0]
        
        # 状态1:第一次买入
        # 要么保持第一次买入状态,要么从未操作状态买入
        dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
        
        # 状态2:第一次卖出
        # 要么保持第一次卖出状态,要么从第一次买入状态卖出
        dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i])
        
        # 状态3:第二次买入
        # 要么保持第二次买入状态,要么从第一次卖出状态买入
        dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i])
        
        # 状态4:第二次卖出
        # 要么保持第二次卖出状态,要么从第二次买入状态卖出
        dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i])
    
    return max(dp[n-1][0], dp[n-1][2], dp[n-1][4])  # 取最大利润

#### 空间优化版本
def maxProfit_III_optimized(prices):
    if not prices:
        return 0
    
    # 初始化5个状态
    buy1 = -prices[0]  # 第一次买入
    sell1 = 0          # 第一次卖出
    buy2 = -prices[0]  # 第二次买入
    sell2 = 0          # 第二次卖出
    
    for i in range(1, len(prices)):
        # 注意更新顺序:从后往前更新,避免状态覆盖
        sell2 = max(sell2, buy2 + prices[i])
        buy2 = max(buy2, sell1 - prices[i])
        sell1 = max(sell1, buy1 + prices[i])
        buy1 = max(buy1, -prices[i])
    
    return max(sell1, sell2)
Java实现
java 复制代码
public class BestTimeToBuySellStockIII {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        int n = prices.length;
        int[][] dp = new int[n][5];
        
        // 初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        dp[0][4] = 0;
        
        for (int i = 1; i < n; i++) {
            dp[i][0] = dp[i-1][0];
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
            dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + prices[i]);
            dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] - prices[i]);
            dp[i][4] = Math.max(dp[i-1][4], dp[i-1][3] + prices[i]);
        }
        
        return Math.max(dp[n-1][0], Math.max(dp[n-1][2], dp[n-1][4]));
    }
    
    // 空间优化版本
    public int maxProfitOptimized(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        int buy1 = -prices[0];
        int sell1 = 0;
        int buy2 = -prices[0];
        int sell2 = 0;
        
        for (int i = 1; i < prices.length; i++) {
            sell2 = Math.max(sell2, buy2 + prices[i]);
            buy2 = Math.max(buy2, sell1 - prices[i]);
            sell1 = Math.max(sell1, buy1 + prices[i]);
            buy1 = Math.max(buy1, -prices[i]);
        }
        
        return Math.max(sell1, sell2);
    }
}

3.4 买卖股票的最佳时机 IV (LeetCode 188)

问题描述:最多完成k笔交易。

通用解法
python 复制代码
def maxProfit_IV(k, prices):
    """
    买卖股票的最佳时机 IV - 最多k次交易
    """
    if not prices or k == 0:
        return 0
    
    n = len(prices)
    
    # 如果k很大,退化为无限次交易
    if k >= n // 2:
        return maxProfit_II(prices)  # 无限次交易
    
    # 状态定义:第i天,已完成j笔交易,是否持有股票
    # 0: 不持有,1: 持有
    dp = [[[0] * 2 for _ in range(k + 1)] for _ in range(n)]
    
    # 初始化:第0天的状态
    for j in range(k + 1):
        dp[0][j][0] = 0          # 不持有
        dp[0][j][1] = -prices[0] if j > 0 else float('-inf')  # 持有
    
    for i in range(1, n):
        for j in range(k + 1):
            # 不持有股票:要么保持不持有,要么卖出
            if j > 0:
                dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
            else:
                dp[i][j][0] = dp[i-1][j][0]
            
            # 持有股票:要么保持持有,要么买入(买入算一次交易)
            dp[i][j][1] = max(dp[i-1][j][1], 
                            dp[i-1][j-1][0] - prices[i] if j > 0 else float('-inf'))
    
    # 最后一天,不持有股票,完成0到k次交易的最大值
    return max(dp[n-1][j][0] for j in range(k + 1))

#### 更优雅的解法(奇偶状态)
def maxProfit_IV_optimized(k, prices):
    if not prices or k == 0:
        return 0
    
    n = len(prices)
    
    # 特殊情况:k很大时退化为无限次交易
    if k >= n // 2:
        profit = 0
        for i in range(1, n):
            if prices[i] > prices[i-1]:
                profit += prices[i] - prices[i-1]
        return profit
    
    # dp[j][0]:完成j笔交易,不持有股票
    # dp[j][1]:完成j笔交易,持有股票
    dp = [[0, float('-inf')] for _ in range(k + 1)]
    
    for price in prices:
        # 注意:从后往前更新,避免状态覆盖
        for j in range(k, 0, -1):
            # 卖出:完成第j笔交易
            dp[j][0] = max(dp[j][0], dp[j][1] + price)
            # 买入:开始第j笔交易
            dp[j][1] = max(dp[j][1], dp[j-1][0] - price)
    
    return max(dp[j][0] for j in range(k + 1))
Java实现
java 复制代码
public class BestTimeToBuySellStockIV {
    public int maxProfit(int k, int[] prices) {
        if (prices == null || prices.length == 0 || k == 0) {
            return 0;
        }
        
        int n = prices.length;
        
        // 如果k很大,退化为无限次交易
        if (k >= n / 2) {
            int profit = 0;
            for (int i = 1; i < n; i++) {
                if (prices[i] > prices[i-1]) {
                    profit += prices[i] - prices[i-1];
                }
            }
            return profit;
        }
        
        // dp[i][j][0]:第i天,完成j笔交易,不持有股票
        // dp[i][j][1]:第i天,完成j笔交易,持有股票
        int[][][] dp = new int[n][k+1][2];
        
        // 初始化
        for (int j = 0; j <= k; j++) {
            dp[0][j][0] = 0;
            dp[0][j][1] = (j > 0) ? -prices[0] : Integer.MIN_VALUE;
        }
        
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= k; j++) {
                // 不持有股票
                if (j > 0) {
                    dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
                } else {
                    dp[i][j][0] = dp[i-1][j][0];
                }
                
                // 持有股票
                if (j > 0) {
                    dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]);
                } else {
                    dp[i][j][1] = dp[i-1][j][1];
                }
            }
        }
        
        int maxProfit = 0;
        for (int j = 0; j <= k; j++) {
            maxProfit = Math.max(maxProfit, dp[n-1][j][0]);
        }
        
        return maxProfit;
    }
}

4. 带冷却期的股票买卖

4.1 最佳买卖股票时机含冷冻期 (LeetCode 309)

问题描述:卖出股票后有一天冷冻期,不能立即买入。

状态定义
  • dp[i][0]:持有股票
  • dp[i][1]:不持有股票,处于冷冻期(今天卖出)
  • dp[i][2]:不持有股票,不处于冷冻期
状态转移方程
复制代码
dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i])  # 只能从不处于冷冻期买入
dp[i][1] = dp[i-1][0] + prices[i]                   # 今天卖出
dp[i][2] = max(dp[i-1][2], dp[i-1][1])              # 保持或从冷冻期过来
Python实现
python 复制代码
def maxProfit_with_cooldown(prices):
    """
    含冷冻期的股票买卖
    """
    if not prices:
        return 0
    
    n = len(prices)
    # 三种状态
    dp = [[0] * 3 for _ in range(n)]
    
    # 初始化
    dp[0][0] = -prices[0]  # 持有
    dp[0][1] = 0           # 冷冻期(不可能第一天就卖出)
    dp[0][2] = 0           # 不持有也不在冷冻期
    
    for i in range(1, n):
        # 持有股票:要么继续持有,要么从不处于冷冻期买入
        dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i])
        
        # 冷冻期:今天卖出
        dp[i][1] = dp[i-1][0] + prices[i]
        
        # 不持有也不在冷冻期:要么保持,要么从冷冻期过来
        dp[i][2] = max(dp[i-1][2], dp[i-1][1])
    
    return max(dp[n-1][1], dp[n-1][2])  # 最后一天不能持有股票

#### 空间优化版本
def maxProfit_with_cooldown_optimized(prices):
    if not prices:
        return 0
    
    n = len(prices)
    hold = -prices[0]     # 持有股票
    sold = 0              # 冷冻期(今天卖出)
    rest = 0              # 不持有也不在冷冻期
    
    for i in range(1, n):
        prev_hold = hold
        prev_sold = sold
        prev_rest = rest
        
        hold = max(prev_hold, prev_rest - prices[i])
        sold = prev_hold + prices[i]
        rest = max(prev_rest, prev_sold)
    
    return max(sold, rest)
Java实现
java 复制代码
public class BestTimeToBuySellStockWithCooldown {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        int n = prices.length;
        int[][] dp = new int[n][3];
        
        // 初始化
        dp[0][0] = -prices[0];  // 持有
        dp[0][1] = 0;           // 冷冻期
        dp[0][2] = 0;           // 不持有也不在冷冻期
        
        for (int i = 1; i < n; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] - prices[i]);
            dp[i][1] = dp[i-1][0] + prices[i];
            dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1]);
        }
        
        return Math.max(dp[n-1][1], dp[n-1][2]);
    }
    
    // 空间优化版本
    public int maxProfitOptimized(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        int hold = -prices[0];
        int sold = 0;
        int rest = 0;
        
        for (int i = 1; i < prices.length; i++) {
            int prevHold = hold;
            int prevSold = sold;
            
            hold = Math.max(hold, rest - prices[i]);
            sold = prevHold + prices[i];
            rest = Math.max(rest, prevSold);
        }
        
        return Math.max(sold, rest);
    }
}

4.2 买卖股票的最佳时机含手续费 (LeetCode 714)

问题描述:每笔交易需要支付手续费。

python 复制代码
def maxProfit_with_fee(prices, fee):
    """
    含手续费的股票买卖
    """
    if not prices:
        return 0
    
    n = len(prices)
    dp = [[0] * 2 for _ in range(n)]
    
    dp[0][0] = 0
    dp[0][1] = -prices[0] - fee  # 买入时支付手续费
    
    for i in range(1, n):
        dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
        dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
    
    return dp[n-1][0]

#### 空间优化版本
def maxProfit_with_fee_optimized(prices, fee):
    if not prices:
        return 0
    
    n = len(prices)
    dp0 = 0
    dp1 = -prices[0] - fee
    
    for i in range(1, n):
        prev_dp0 = dp0
        prev_dp1 = dp1
        
        dp0 = max(prev_dp0, prev_dp1 + prices[i])
        dp1 = max(prev_dp1, prev_dp0 - prices[i] - fee)
    
    return dp0
Java实现
java 复制代码
public class BestTimeToBuySellStockWithFee {
    public int maxProfit(int[] prices, int fee) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        
        int n = prices.length;
        int[][] dp = new int[n][2];
        
        dp[0][0] = 0;
        dp[0][1] = -prices[0] - fee;
        
        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] - fee);
        }
        
        return dp[n-1][0];
    }
}

5. 打家劫舍问题系列

5.1 打家劫舍 I (LeetCode 198)

问题描述:不能抢劫相邻的房屋。

状态定义

dp[i]:抢劫前i个房屋能获得的最大金额

状态转移方程
复制代码
dp[i] = max(dp[i-1], dp[i-2] + nums[i-1])
Python实现
python 复制代码
def rob_I(nums):
    """
    打家劫舍 I - 线性排列
    """
    if not nums:
        return 0
    
    n = len(nums)
    if n == 1:
        return nums[0]
    
    dp = [0] * n
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    
    for i in range(2, n):
        dp[i] = max(dp[i-1], dp[i-2] + nums[i])
    
    return dp[n-1]

#### 空间优化版本
def rob_I_optimized(nums):
    if not nums:
        return 0
    
    n = len(nums)
    if n == 1:
        return nums[0]
    
    prev2 = nums[0]              # dp[i-2]
    prev1 = max(nums[0], nums[1])  # dp[i-1]
    
    for i in range(2, n):
        curr = max(prev1, prev2 + nums[i])
        prev2, prev1 = prev1, curr
    
    return prev1
Java实现
java 复制代码
public class HouseRobberI {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }
        
        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }
        
        return dp[n-1];
    }
    
    // 空间优化版本
    public int robOptimized(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int n = nums.length;
        if (n == 1) return nums[0];
        
        int prev2 = nums[0];
        int prev1 = Math.max(nums[0], nums[1]);
        
        for (int i = 2; i < n; i++) {
            int curr = Math.max(prev1, prev2 + nums[i]);
            prev2 = prev1;
            prev1 = curr;
        }
        
        return prev1;
    }
}

5.2 打家劫舍 II (LeetCode 213)

问题描述:房屋围成一圈,不能抢劫相邻房屋。

思路:拆分为两个子问题
  1. 抢劫第一间到倒数第二间
  2. 抢劫第二间到最后一间
    取两者的最大值
python 复制代码
def rob_II(nums):
    """
    打家劫舍 II - 环形排列
    """
    if not nums:
        return 0
    
    n = len(nums)
    if n == 1:
        return nums[0]
    
    # 两种情况:抢第一间不抢最后一间,或不抢第一间抢最后一间
    return max(rob_range(nums, 0, n-2), rob_range(nums, 1, n-1))

def rob_range(nums, start, end):
    """
    抢劫从start到end的房屋(线性)
    """
    if start > end:
        return 0
    
    n = end - start + 1
    if n == 1:
        return nums[start]
    
    prev2 = nums[start]
    prev1 = max(nums[start], nums[start+1])
    
    for i in range(start+2, end+1):
        curr = max(prev1, prev2 + nums[i])
        prev2, prev1 = prev1, curr
    
    return prev1
Java实现
java 复制代码
public class HouseRobberII {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int n = nums.length;
        if (n == 1) return nums[0];
        
        // 两种情况
        return Math.max(robRange(nums, 0, n-2), 
                       robRange(nums, 1, n-1));
    }
    
    private int robRange(int[] nums, int start, int end) {
        if (start > end) return 0;
        if (start == end) return nums[start];
        
        int prev2 = nums[start];
        int prev1 = Math.max(nums[start], nums[start+1]);
        
        for (int i = start+2; i <= end; i++) {
            int curr = Math.max(prev1, prev2 + nums[i]);
            prev2 = prev1;
            prev1 = curr;
        }
        
        return prev1;
    }
}

5.3 打家劫舍 III (LeetCode 337)

问题描述:房屋形成二叉树,不能抢劫直接相连的房屋。

树形状态机DP
python 复制代码
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def rob_III(root):
    """
    打家劫舍 III - 二叉树
    """
    def dfs(node):
        """
        返回一个元组:(不抢当前节点的最大收益, 抢当前节点的最大收益)
        """
        if not node:
            return (0, 0)
        
        left = dfs(node.left)
        right = dfs(node.right)
        
        # 不抢当前节点:子节点可抢可不抢
        not_rob = max(left[0], left[1]) + max(right[0], right[1])
        
        # 抢当前节点:子节点不能抢
        rob = node.val + left[0] + right[0]
        
        return (not_rob, rob)
    
    result = dfs(root)
    return max(result[0], result[1])
Java实现
java 复制代码
public class HouseRobberIII {
    public int rob(TreeNode root) {
        int[] result = dfs(root);
        return Math.max(result[0], result[1]);
    }
    
    private int[] dfs(TreeNode node) {
        if (node == null) {
            return new int[]{0, 0};
        }
        
        int[] left = dfs(node.left);
        int[] right = dfs(node.right);
        
        // 不抢当前节点
        int notRob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        
        // 抢当前节点
        int rob = node.val + left[0] + right[0];
        
        return new int[]{notRob, rob};
    }
}

6. 状态机DP解题模板总结

6.1 通用解题步骤

  1. 识别状态

    • 分析问题中的可能状态
    • 定义状态表示方法
  2. 定义状态转移

    • 确定状态之间的转移关系
    • 写出状态转移方程
  3. 初始化

    • 确定初始状态的值
    • 处理边界情况
  4. 遍历更新

    • 按顺序更新所有状态
    • 注意更新顺序(避免状态覆盖)
  5. 返回结果

    • 确定最终需要返回的状态

6.2 状态机DP模式总结

问题类型 状态数量 状态含义 转移特点
股票买卖I 2 持有/不持有 只能买一次
股票买卖II 2 持有/不持有 可无限次买卖
股票买卖III 5 未操作/第一次买/第一次卖/第二次买/第二次卖 最多两次
股票买卖IV 2(k+1) 完成j次交易,持有/不持有 最多k次
含冷冻期 3 持有/冷冻期/不持有 卖出后冷冻一天
含手续费 2 持有/不持有 买卖支付手续费
打家劫舍I 1 前i个房屋最大收益 不能相邻
打家劫舍III 2 抢/不抢当前节点 树形结构

6.3 空间优化技巧

  1. 滚动数组
python 复制代码
# 只保留必要的前一状态
prev_dp0, prev_dp1 = dp0, dp1
dp0 = max(prev_dp0, prev_dp1 + prices[i])
dp1 = max(prev_dp1, prev_dp0 - prices[i])
  1. 状态压缩
python 复制代码
# 使用位运算压缩多个状态
state = (hold << 1) | sold  # 将两个状态压缩为一个整数
  1. 变量复用
python 复制代码
# 使用临时变量避免状态覆盖
temp = dp0
dp0 = max(dp0, dp1 + prices[i])
dp1 = max(dp1, temp - prices[i])

6.4 常见错误与调试

  1. 状态定义错误

    • 状态含义不清晰
    • 状态数量不足或过多
  2. 转移方程错误

    • 遗漏某些转移路径
    • 转移条件错误
  3. 初始化错误

    • 初始状态值设置错误
    • 边界情况处理不当
  4. 更新顺序错误

    • 状态覆盖导致错误
    • 未正确处理依赖关系

6.5 调试技巧

  1. 打印状态表
python 复制代码
def print_state_table(dp, day):
    print(f"Day {day}:")
    print(f"  Hold: {dp[day][0]}")
    print(f"  Not Hold: {dp[day][1]}")
  1. 小规模测试
python 复制代码
# 测试简单案例
test_cases = [
    ([1,2,3,4,5], 4),  # 连续上涨
    ([7,6,4,3,1], 0),  # 连续下跌
    ([1,3,2,5,4], 4),  # 波动
]
  1. 状态追踪
python 复制代码
# 记录状态转移路径
path = []
for i in range(1, n):
    if dp[i][0] != dp[i-1][0]:  # 状态发生变化
        path.append(f"Day {i}: Buy at {prices[i]}")
    elif dp[i][1] != dp[i-1][1]:
        path.append(f"Day {i}: Sell at {prices[i]}")

7. 进阶练习题目

7.1 股票买卖变种

  1. 最大交易次数限制

    • 最多完成k笔交易(已完成)
    • 每天最多交易一次
  2. 交易限制

    • 有冷冻期(已完成)
    • 有手续费(已完成)
    • 有交易成本
  3. 多市场交易

    • 同时关注多个股票
    • 有资金限制

7.2 其他状态机问题

  1. 字符串匹配

    • 通配符匹配
    • 正则表达式匹配
  2. 游戏问题

    • 预测赢家
    • 石子游戏
  3. 路径问题

    • 不同路径带障碍
    • 最小路径和

7.3 综合应用

  1. 状态机 + 其他算法

    • 状态机 + 二分查找
    • 状态机 + 滑动窗口
    • 状态机 + 优先队列
  2. 多维度状态

    • 时间维度 + 状态维度
    • 空间维度 + 状态维度

8. 面试准备建议

8.1 必备技能

  1. 理解状态机的基本概念
  2. 掌握常见问题的状态定义
  3. 熟悉状态转移方程的推导
  4. 了解空间优化技巧

8.2 解题思路

  1. 分析状态:识别问题中的可能状态
  2. 定义转移:明确状态之间的转换关系
  3. 初始化:设置初始状态值
  4. 遍历计算:按顺序更新状态
  5. 返回结果:确定最终答案

8.3 沟通技巧

  1. 清晰解释状态定义
  2. 说明状态转移的逻辑
  3. 分析时间和空间复杂度
  4. 讨论可能的优化方案

9. 总结

状态机DP是动态规划中非常重要且实用的技巧,特别适合处理具有多个状态和状态转移的问题。掌握状态机DP不仅有助于解决特定的算法问题,更能培养系统思维和状态分析能力。

关键要点

  1. 状态定义要清晰:明确每个状态的含义
  2. 转移方程要完整:覆盖所有可能的转移路径
  3. 初始化要正确:处理好边界情况
  4. 更新顺序要合理:避免状态覆盖
  5. 空间优化要掌握:减少内存使用

通过大量练习,深入理解状态机DP的思想和应用,能够有效提升解决复杂问题的能力。建议按照问题类型系统练习,从简单到复杂逐步深入。

相关推荐
dragoooon342 小时前
[hot100 NO.91~95]
算法
windows_62 小时前
【无标题】
算法
踢足球09292 小时前
寒假打卡:2026-01-24
数据结构·算法
亲爱的非洲野猪3 小时前
动态规划进阶:多维DP深度解析
算法·动态规划
AlenTech3 小时前
197. 上升的温度 - 力扣(LeetCode)
算法·leetcode·职场和发展
橘颂TA3 小时前
【Linux 网络】TCP 拥塞控制与异常处理:从原理到实践的深度剖析
linux·运维·网络·tcp/ip·算法·职场和发展·结构与算法
tobias.b4 小时前
408真题解析-2010-9-数据结构-折半查找的比较次数
java·数据结构·算法·计算机考研·408真题解析
源代码•宸4 小时前
Leetcode—404. 左叶子之和【简单】
经验分享·后端·算法·leetcode·职场和发展·golang·dfs
WBluuue4 小时前
数据结构与算法:dp优化——优化尝试和状态设计
c++·算法·leetcode·动态规划