不用递归裸写CountWaysDP是因为子问题重复导致O(3ⁿ)时间复杂度,n=40时卡死;必须用记忆化(comma ok判断)降至O(n),并注意base case和滚动更新顺序。

为什么不用递归裸写 CountWaysDP?
裸递归(比如直接写 CountWaysDP(n-1) + CountWaysDP(n-2) + CountWaysDP(n-3))在 n=30 时就明显卡顿,n=40 几乎卡死——这不是 Go 性能差,是子问题爆炸式重复计算。比如 CountWaysDP(25) 会被调用上百次。
- Go 的函数调用开销小,但指数级调用栈深度会迅速耗尽栈空间,触发
runtime: goroutine stack exceeds 1GB limit - 记忆化不是“锦上添花”,而是从 O(3ⁿ) 降到 O(n) 的必要手段
- 别用
map[int]int判断是否“已计算”只靠memo[n] != 0:因为n=0时结果就是 1,而memo[0]初始为 0,会误判为“未计算”,导致无限递归 - 正确做法是用 comma ok 语法:
if val, ok := memo[n]; ok { return val }
fibDPSpaceOptimized 和 CountWaysDP 迭代版怎么写才不翻车?
空间优化的核心是只保留最近几个状态值,但爬楼梯(1/2/3步)要留三个变量,斐波那契只需两个——错用变量数会导致逻辑错位,比如把 prev, curr, next 写成两个变量,dp[i] 就永远算不对。
- 初始化必须覆盖所有 base case:
dp[0]=1, dp[1]=1, dp[2]=2(注意:0阶是1种方式,1阶是1种,2阶是“1+1”和“2”共2种) - 迭代循环起始点容易错:从
i=3开始算,不是i=1;否则会越界或覆盖 base - 变量滚动顺序不能颠倒:
prev, curr, next = curr, next, prev+curr+next必须原子更新,否则中间值被覆盖 - 示例片段:
if n == 0 { return 1 }<br>if n == 1 { return 1 }<br>if n == 2 { return 2 }<br>a, b, c := 1, 1, 2<br>for i := 3; i <= n; i++ {<br> a, b, c = b, c, a+b+c<br>}<br>return c
二维 DP 如 LongestCommonSubsequence 的边界怎么设才不 panic?
Go 的 slice 索引从 0 开始,但 LCS 的状态定义里 lcs[i][j] 表示字符串 a[:i] 和 b[:j] 的最长公共子序列长度——这意味着数组要开 (aLen+1) × (bLen+1),否则访问 lcs[aLen][bLen] 必然越界。
- 常见 panic:
panic: runtime error: index out of range [10] with length 10,本质是忘了 +1 - 内层循环 j 要从 0 到
bLen(含),不是bLen-1;否则最后一列永远不填 - 字符比较别用
a[i] == b[j]:Go 字符串下标取的是 byte,中文或 emoji 会出错;必须转[]rune后再比aRunes[i-1] == bRunes[j-1] - 初始化可省略:
make([][]int, aLen+1)后每行用make([]int, bLen+1),零值自动是 0,不用显式赋 0
什么时候该用 map[int]int,什么时候该用 []int 做 memo?
取决于 n 的范围和稀疏性。n 小且连续(如楼梯 ≤ 1000)用 slice 更快、更省内存;n 很大但只计算少数几个值(比如只查 CountWaysDP(1e6) 和 CountWaysDP(1e9)),map 才有意义。
立即学习“go语言免费学习笔记(深入)”;
-
[]int:索引安全、缓存友好、无哈希开销;但若 n=1e9,make([]int, n+1)直接 OOM -
map[int]int:动态扩容、内存占用随实际调用数增长;但每次访问有哈希计算+指针跳转,比 slice 下标慢 3–5 倍 - 实战建议:n 1e6 且调用点稀疏(
- 别在函数里反复
make(map[int]int):传入复用的 map,避免 GC 压力


















