很多初学者觉得 DP(动态规划)难,不是难在转移,而是难在“状态怎么想”。本文带你从暴力递归出发,层层递进,领悟状态设计的三个层次。
第一重境界:直觉模拟(暴力搜索)
拿到一个问题,先不要想什么最优子结构,直接用 DFS 去暴力枚举所有可能性。比如经典的 数字三角形 问题:
int dfs(int i, int j) {
if (i == n) return a[i][j];
return a[i][j] + max(dfs(i+1, j), dfs(i+1, j+1));
}
这个阶段的核心是 把决策树画出来,找到每个节点的“状态”是什么(在这里是 (i, j))。
第二重境界:记忆化与递推(发现重叠子问题)
暴力 DFS 会重复计算同一个 (i, j) 无数次。我们加上 memo 数组:
int dfs(int i, int j) {
if (i == n) return a[i][j];
if (memo[i][j] != -1) return memo[i][j];
return memo[i][j] = a[i][j] + max(dfs(i+1, j), dfs(i+1, j+1));
}
这就是 DP 的雏形。进一步地,我们可以去掉递归,改成循环递推(从底向上):
for (int i = n; i >= 1; i--)
for (int j = 1; j <= i; j++)
dp[i][j] = a[i][j] + max(dp[i+1][j], dp[i+1][j+1]);
此时状态定义:dp[i][j] 表示从第 i 行第 j 列出发到底层的最大路径和。
第三重境界:状态化简与降维(空间优化)
观察递推式,dp[i] 只依赖于 dp[i+1],所以我们可以把二维压成一维:
for (int i = n; i >= 1; i--)
for (int j = 1; j <= i; j++)
dp[j] = a[i][j] + max(dp[j], dp[j+1]);
这就是 0/1 背包中倒序枚举的底层逻辑。到了这一重境界,你不仅要会做,还要懂 为什么能省空间,以及 遍历顺序为什么不能乱。
总结:学 DP,不要一开始就硬想转移方程。先暴力,再记忆化,最后递推。这三重境界走完,任何 DP 题在你眼里都会清晰无比。