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)
问题描述:房屋围成一圈,不能抢劫相邻房屋。
思路:拆分为两个子问题
- 抢劫第一间到倒数第二间
- 抢劫第二间到最后一间
取两者的最大值
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 通用解题步骤
-
识别状态:
- 分析问题中的可能状态
- 定义状态表示方法
-
定义状态转移:
- 确定状态之间的转移关系
- 写出状态转移方程
-
初始化:
- 确定初始状态的值
- 处理边界情况
-
遍历更新:
- 按顺序更新所有状态
- 注意更新顺序(避免状态覆盖)
-
返回结果:
- 确定最终需要返回的状态
6.2 状态机DP模式总结
| 问题类型 | 状态数量 | 状态含义 | 转移特点 |
|---|---|---|---|
| 股票买卖I | 2 | 持有/不持有 | 只能买一次 |
| 股票买卖II | 2 | 持有/不持有 | 可无限次买卖 |
| 股票买卖III | 5 | 未操作/第一次买/第一次卖/第二次买/第二次卖 | 最多两次 |
| 股票买卖IV | 2(k+1) | 完成j次交易,持有/不持有 | 最多k次 |
| 含冷冻期 | 3 | 持有/冷冻期/不持有 | 卖出后冷冻一天 |
| 含手续费 | 2 | 持有/不持有 | 买卖支付手续费 |
| 打家劫舍I | 1 | 前i个房屋最大收益 | 不能相邻 |
| 打家劫舍III | 2 | 抢/不抢当前节点 | 树形结构 |
6.3 空间优化技巧
- 滚动数组:
python
# 只保留必要的前一状态
prev_dp0, prev_dp1 = dp0, dp1
dp0 = max(prev_dp0, prev_dp1 + prices[i])
dp1 = max(prev_dp1, prev_dp0 - prices[i])
- 状态压缩:
python
# 使用位运算压缩多个状态
state = (hold << 1) | sold # 将两个状态压缩为一个整数
- 变量复用:
python
# 使用临时变量避免状态覆盖
temp = dp0
dp0 = max(dp0, dp1 + prices[i])
dp1 = max(dp1, temp - prices[i])
6.4 常见错误与调试
-
状态定义错误:
- 状态含义不清晰
- 状态数量不足或过多
-
转移方程错误:
- 遗漏某些转移路径
- 转移条件错误
-
初始化错误:
- 初始状态值设置错误
- 边界情况处理不当
-
更新顺序错误:
- 状态覆盖导致错误
- 未正确处理依赖关系
6.5 调试技巧
- 打印状态表:
python
def print_state_table(dp, day):
print(f"Day {day}:")
print(f" Hold: {dp[day][0]}")
print(f" Not Hold: {dp[day][1]}")
- 小规模测试:
python
# 测试简单案例
test_cases = [
([1,2,3,4,5], 4), # 连续上涨
([7,6,4,3,1], 0), # 连续下跌
([1,3,2,5,4], 4), # 波动
]
- 状态追踪:
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 股票买卖变种
-
最大交易次数限制:
- 最多完成k笔交易(已完成)
- 每天最多交易一次
-
交易限制:
- 有冷冻期(已完成)
- 有手续费(已完成)
- 有交易成本
-
多市场交易:
- 同时关注多个股票
- 有资金限制
7.2 其他状态机问题
-
字符串匹配:
- 通配符匹配
- 正则表达式匹配
-
游戏问题:
- 预测赢家
- 石子游戏
-
路径问题:
- 不同路径带障碍
- 最小路径和
7.3 综合应用
-
状态机 + 其他算法:
- 状态机 + 二分查找
- 状态机 + 滑动窗口
- 状态机 + 优先队列
-
多维度状态:
- 时间维度 + 状态维度
- 空间维度 + 状态维度
8. 面试准备建议
8.1 必备技能
- 理解状态机的基本概念
- 掌握常见问题的状态定义
- 熟悉状态转移方程的推导
- 了解空间优化技巧
8.2 解题思路
- 分析状态:识别问题中的可能状态
- 定义转移:明确状态之间的转换关系
- 初始化:设置初始状态值
- 遍历计算:按顺序更新状态
- 返回结果:确定最终答案
8.3 沟通技巧
- 清晰解释状态定义
- 说明状态转移的逻辑
- 分析时间和空间复杂度
- 讨论可能的优化方案
9. 总结
状态机DP是动态规划中非常重要且实用的技巧,特别适合处理具有多个状态和状态转移的问题。掌握状态机DP不仅有助于解决特定的算法问题,更能培养系统思维和状态分析能力。
关键要点:
- 状态定义要清晰:明确每个状态的含义
- 转移方程要完整:覆盖所有可能的转移路径
- 初始化要正确:处理好边界情况
- 更新顺序要合理:避免状态覆盖
- 空间优化要掌握:减少内存使用
通过大量练习,深入理解状态机DP的思想和应用,能够有效提升解决复杂问题的能力。建议按照问题类型系统练习,从简单到复杂逐步深入。