LeetCode-Go中的动态规划多状态问题:从股票买卖到打家劫舍
动态规划(Dynamic Programming, DP)是解决多阶段决策问题的高效方法,尤其在处理包含多种状态转换的场景时表现突出。本文将以LeetCode-Go项目中的经典问题为例,深入解析如何通过状态定义与转移方程设计,解决股票买卖和打家劫舍等多状态DP问题。这些解决方案均来自LeetCode-Go项目源码,该项目实现了100%测试覆盖率且运行效率优于99%的提交记录。
动态规划多状态问题的核心框架
多状态DP问题的关键在于定义清晰的状态变量和构建无后效性的转移方程。以股票买卖问题为例,我们需要跟踪"持有股票"和"不持有股票"两种核心状态,并考虑冷冻期、交易次数限制等约束条件。项目中采用的通用框架如下:
- 状态定义:
dp[i][s]表示第i天处于状态s时的最大收益(s=0表示不持有,s=1表示持有) - 转移方程:通过前一天的状态推导当前状态
- 空间优化:使用滚动数组将二维DP压缩为常数空间
股票买卖问题的状态演进
基础版:单次交易(Best Time to Buy and Sell Stock)
[121. Best Time to Buy and Sell Stock](https://link.gitcode.com/i/395fcd9f4bd0c9eecea9e397be6e5c02/blob/25c03cf13afa6ffb2bb305940c9bced152214536/leetcode/0121.Best-Time-to-Buy-and-Sell-Stock/121. Best Time to Buy and Sell Stock.go?utm_source=gitcode_repo_files) 要求只能完成一笔交易,本质是寻找价格序列中的最大差值。项目中采用贪心+DP的混合解法:
// 解法一 模拟DP
func maxProfit(prices []int) int {
if len(prices) < 1 {
return 0
}
minPrice, maxProfit := prices[0], 0 // 状态变量:最低买入价和最大利润
for i := 1; i < len(prices); i++ {
if prices[i]-minPrice > maxProfit {
maxProfit = prices[i] - minPrice // 更新最大利润(不持有状态)
}
if prices[i] < minPrice {
minPrice = prices[i] // 更新最低买入价(持有状态)
}
}
return maxProfit
}
该实现通过两个变量模拟了持有/不持有的状态转换,将空间复杂度优化至O(1),时间效率超越100%提交。
进阶版:含冷冻期的多次交易
当允许多次买卖但加入"卖出后次日不能买入"的冷冻期约束时(如[309. Best Time to Buy and Sell Stock with Cooldown]),需要引入三维状态:
dp[i][0]:不持有股票且不在冷冻期dp[i][1]:持有股票dp[i][2]:不持有股票且在冷冻期
状态转移关系如下:
dp[i][0] = max(dp[i-1][0], dp[i-1][2])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
dp[i][2] = dp[i-1][1] + prices[i]
打家劫舍系列的状态迁移
线性房屋:相邻约束问题
[198. House Robber](https://link.gitcode.com/i/395fcd9f4bd0c9eecea9e397be6e5c02/blob/25c03cf13afa6ffb2bb305940c9bced152214536/leetcode/0198.House-Robber/198. House Robber.go?utm_source=gitcode_repo_files) 是典型的相邻状态互斥问题,项目中定义了两种状态:
dp[i]:抢劫前i间房屋的最大金额- 转移方程:
dp[i] = max(dp[i-1], nums[i]+dp[i-2])
核心实现如下:
func rob(nums []int) int {
n := len(nums)
if n == 0 {
return 0
}
if n == 1 {
return nums[0]
}
// dp[i] 代表抢 nums[0...i] 房子的最大价值
dp := make([]int, n)
dp[0], dp[1] = nums[0], max(nums[1], nums[0])
for i := 2; i < n; i++ {
dp[i] = max(dp[i-1], nums[i]+dp[i-2]) // 不抢i或抢i(则i-1不能抢)
}
return dp[n-1]
}
环形房屋:首尾约束问题
当房屋呈环形排列时([213. House Robber II]),需要拆分两种场景:
- 不抢劫第一间房,问题退化为线性排列的nums[1:]
- 不抢劫最后一间房,问题退化为线性排列的nums[:n-1]
最终结果取两种场景的最大值,项目中通过调用基础版rob函数实现:
func robII(nums []int) int {
if len(nums) == 1 {
return nums[0]
}
return max(rob(nums[1:]), rob(nums[:len(nums)-1]))
}
多状态DP问题的通用解题策略
通过分析股票买卖和打家劫舍系列问题,可以提炼出多状态DP的解题模板:
1. 状态定义三要素
- 时间维度:通常为问题中的阶段(天数、房屋序号等)
- 状态变量:描述当前决策的核心属性(持有/不持有、是否抢劫等)
- 目标值:需要优化的目标函数(利润、金额等)
2. 状态转移四步法
- 枚举所有可能状态:列出每个阶段的所有可能状态
- 确定转移条件:分析状态间的转换规则(如"买入"需从"不持有"状态转换)
- 构建转移方程:用数学公式表达状态间的数值关系
- 处理边界条件:初始化起始状态和特殊情况
3. 空间优化技巧
- 滚动数组:当转移只依赖前1-2个状态时,用变量替代数组(如股票问题)
- 状态合并:将互斥状态合并为同一维度的不同取值(如打家劫舍的0/1状态)
实战应用:从源码到解题
LeetCode-Go项目中对多状态DP问题的处理展现了极高的工程技巧。以[0309. Best Time to Buy and Sell Stock with Cooldown]为例,项目代码通过以下方式确保效率:
- 状态压缩:将三维DP压缩为三个变量
func maxProfit(prices []int) int {
if len(prices) == 0 {
return 0
}
dp0, dp1, dp2 := 0, -prices[0], 0 // 初始状态
for i := 1; i < len(prices); i++ {
newDp0 := max(dp0, dp2)
newDp1 := max(dp1, dp0 - prices[i])
newDp2 := dp1 + prices[i]
dp0, dp1, dp2 = newDp0, newDp1, newDp2
}
return max(dp0, dp2)
}
- 枚举优化:通过单调栈处理价格波动(如[122. Best Time to Buy and Sell Stock II]的解法二)
总结与扩展
多状态动态规划通过引入额外的状态维度,有效解决了包含约束条件的决策问题。LeetCode-Go项目中的实现展示了三个关键优化方向:
- 状态精简:只保留影响决策的核心状态变量
- 空间压缩:利用滚动数组将O(n)空间降至O(1)
- 边界处理:通过预处理规避数组越界等异常情况
这些技巧不仅适用于股票和抢劫问题,还可推广到背包问题、图论路径规划等领域。建议结合项目中的动态规划专题源码进行系统学习,重点关注状态定义与转移方程的设计思路。
提示:更多多状态DP问题的解决方案可查看项目中的dp标签目录,包含从基础到进阶的完整实现。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




