go动态规划核心是选对状态定义、控好子问题边界、避开map[int]int零值陷阱;climbstairs变体需重审最后一步来源,基础case与数组长度须匹配,记忆化应避免用零值判未计算。

Go 里写动态规划,核心不是背模板,而是选对状态定义 + 控制好子问题边界 + 避开 map[int]int 的零值陷阱。
怎么写 climbStairs:从 1/2 步到 1/2/3 步的迁移逻辑
经典爬楼梯题(LeetCode 70)只允许跳 1 或 2 步,状态转移是 dp[i] = dp[i-1] + dp[i-2];但一旦改成支持跳 3 步(常见变体),状态方程立刻变成 dp[i] = dp[i-1] + dp[i-2] + dp[i-3]——别硬套旧逻辑,得重审“最后一步可能从哪来”。
- 基础 case 必须对齐:跳 1/2 步时
dp[0]=1, dp[1]=1;跳 1/2/3 步时必须设dp[0]=1, dp[1]=1, dp[2]=2,否则i=3会错 - 数组长度要多开 1~3 位,避免
dp[i-3]越界;用切片时建议初始化为make([]int, n+3),再从dp[0]开始填 - 如果 n 很小(比如 ≤2),直接返回预设值,别进循环——边界判断漏掉会导致 panic 或结果错
memo[n] > 0 判断为什么经常出 bug
用 map[int]int 做记忆化时,memo[0] 是 0,memo[5] 未设置也是 0,Go 不区分“没算过”和“算出来是 0”。所以 if memo[n] != 0 这种写法在 n=0 且合法解为 0 的场景下必挂(虽然爬楼梯里 ways(0)=1,但其他 DP 问题如最小花费可能真出现 0)。
- 正确做法是用 “comma ok” 语法:
if val, ok := memo[n]; ok { return val } - 或者改用
map[int]*int,存指针,nil 表示未计算,非 nil 表示已缓存(适合需要存 0 的场景) - 更简单省事:直接用切片
memo := make([]int, n+1),初始全 0,再额外配个done := make([]bool, n+1)标记是否计算过
二维 DP 如 LongestCommonSubsequence 怎么避免索引偏移错误
字符串 LCS 的 DP 表通常定义为 lcs[i][j] 表示 a[0:i] 和 b[0:j] 的最长公共子序列长度,这意味着数组要开成 make([][]int, aLen+1) 和 make([]int, bLen+1),且循环从 i=0 到 aLen(含),不是 aLen-1。
- 常见错误:循环写成
for i := 1; i ,漏掉第 <code>aLen行,导致lcs[aLen][bLen]没更新 - 字符比较时用
aRunes[i-1] == bRunes[j-1],因为i和j是长度维度,不是下标维度 - 如果输入含中文或 emoji,必须转
[]rune再操作,直接用string[i]会按字节取,导致乱码和越界
knapsack 空间优化时为什么不能直接滚动数组一维化
0-1 背包的二维 DP 是 dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i])。想压成一维 dp[w],必须倒序遍历容量 w,否则正序会重复使用刚更新的值(相当于无限背包)。
- 错误写法:
for w := weight[i]; w → 会导致同一个物品被多次选中 - 正确写法:
for w := c; w >= weight[i]; w-- - 初始化:一维数组要全设为 0;若要求“恰好装满”,则除
dp[0]=0外其余初始化为负无穷(Go 中用math.MinInt64) - 注意:空间优化后无法还原具体选了哪些物品,要路径回溯必须保留二维表或额外记录决策
DP 最容易卡住的地方从来不是状态转移式,而是 base case 的定义粒度、索引与长度的混淆、以及 Go 中 map 零值语义带来的隐性分支。写完先跑 n=0、n=1、n=2 三个最简 case,比看十遍公式管用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











