news 2026/10/3 18:34:06

动态规划三题拆解:最长有效括号、不同路径与最小路径和

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动态规划三题拆解:最长有效括号、不同路径与最小路径和

刷题刷到动态规划这块的朋友,应该都绕不开这三道经典题:力扣32“最长有效括号”、62“不同路径”、64“最小路径和”。很多人刷力扣是按题号顺序来的,但我觉得这三道题放在一起看更有意思——它们分别代表了动态规划里三个不同层次的模型:一维状态推导、二维状态表格、带权路径决策。从32到64,难度不是线性上升的,而是思维方式的切换。我先把话放这儿:如果你只打算背模板,三道题都能背下来,但换个变形题照样懵;如果你把这三种DP模型吃透了,后面再刷一堆中等难度的题都会觉得顺手很多。

这三道题表面上一个处理括号字符串,一个处理网格路径,一个处理矩阵最小代价,本质上都在问同一个问题:当前状态怎么从之前的状态转移过来。区别在于,32要处理“括号匹配的连续性”,62要处理“路径来源的叠加”,64要在叠加之外再考虑“最小代价选择”。理解了它们的共性和差异,你等于同时复习了动态规划的三块核心基石。

我自己刷这三题的时候,用一句话总结就是:32考的是DP数组的“下标语义”,62考的是DP表格的“初始化”,64考的是“边界的最后一公里”。这三关你能打通,动态规划的基本功就算站稳了。

1. 三道题放在一起看,到底在考什么

很多人刷题有个误区,喜欢一道一道地刷,刷完就忘。我建议换个思路:按模型归类。32、62、64这三道题就是非常好的归组样本。

1.1 从题目描述看它们的表面差异

先来快速回顾一下三道题都说了什么。

力扣32“最长有效括号”:给你一个只包含(和)的字符串,找出最长有效(格式正确且连续)括号子串的长度。经典例子是(()返回2,())返回2,()(()返回2,而(()())返回6。

力扣62“不同路径”:一个机器人位于m x n网格的左上角,每次只能向下或向右移动一步,问到达右下角总共有多少条不同路径。比如3 x 2的网格答案是3,7 x 3的网格答案是28。

力扣64“最小路径和”:给定一个包含非负整数的m x n网格,找出一条从左上角到右下角的路径,使得路径上的数字总和最小,每次同样只能向下或向右移动。一个经典测试用例是grid = [[1,3,1],[1,5,1],[4,2,1]],最小路径和是7。

这三道题如果用暴力法,统统不可行。32的暴力枚举子串要O(n²)甚至O(n³),62和64的暴力DFS直接指数爆炸。所以它们天然是算法题,不是考你写代码,而是考你怎么建模。

1.2 表面不同,本质同一:状态转移思维

我在刷题时习惯先问一个问题:如果我知道了所有“更小规模”的答案,能不能拼出“当前规模”的答案?

32里,如果我知道了以s[i-1]结尾的最长有效括号长度,能不能推出以s[i]结尾的长度?答案是能,但要分情况讨论。

62里,如果我知道了到达(i-1, j)的路径数和到达(i, j-1)的路径数,到达(i, j)的路径数就是两者之和。因为最后一步不是从上面来,就是从左边来。

64里,如果我知道了到达(i-1, j)的最小路径和和到达(i, j-1)的最小路径和,到达(i, j)的最小路径和就是“较小者 + 当前格子的值”。因为要保证总和最小,最后一步肯定选择代价更小的那个来源。

看,三句话结构完全一样。这就是动态规划的核心思考方式——把大问题拆成“最后一步 + 之前子问题”。你一旦习惯了这种提问方式,看到新题的第一反应就不会是“怎么枚举”,而是“最后一步是什么,之前的子问题是什么”。

提示:判断一道题能不能用DP,可以先问自己——“如果我算出了所有更小规模的最优解,能否在常数时间内组合出当前规模的最优解?”能,则DP;不能,大概率要换搜索或贪心思路。

1.3 从难度梯度看动态规划的成长路径

说实话,如果按代码量排,62和64的代码比32还短。但按思维量排,32反而是最绕的。

我的看法是:62是“入门”,它让你理解DP表是怎么填的;64是“进阶”,它让你理解初始化和边界的坑;32是“攻坚”,它让你理解DP下标的设计可能比状态转移本身更烧脑。

所以我把这三道题当成一套组合拳:先用62建立“表格感”,再用64强化“边界感”,最后用32提升“下标感”。刷完之后,你对动态规划的二维表格和一维滚动数组都会有比较深的肌肉记忆。

2. 力扣32:最长有效括号——最绕的下标设计

这道题我第一次刷的时候翻车了,看题解都看晕了,因为网上给的解法有两种:栈和动态规划。我后来是先把栈的解法弄懂,再切换到DP,才彻底理解下标语义。

2.1 栈解法:括号配对最直觉的写法

用栈解括号匹配是经典中的经典,但这里有个细节和普通的括号匹配不一样:我们找的是“连续有效括号子串”,所以不能只消消乐,还得记录“从哪里开始断了”。

栈里不存字符,而是存下标。这样每次遇到右括号能把栈顶弹出来,然后通过i - stack.peek() - 1或者i - stack[stack.length - 1]算出当前有效子串长度。具体步骤我直接给出来:

  1. 初始化一个栈,栈底提前放一个-1(这个很关键,后面说)。
  2. 从左到右遍历字符串,下标记为i。
  3. 遇到(:把i压入栈。
  4. 遇到):先弹出一个元素(即匹配掉一个左括号,如果栈顶是左括号下标的话;就算栈顶是别的,也该弹,因为我们要重新计算起点)。
  5. 弹出后,如果栈不为空,说明当前右括号找到了匹配对象,计算i - 栈顶元素,更新最大长度;如果栈为空,说明这个右括号是多余的,它不能和任何左括号匹配,于是把i压入栈,作为新的“起点基准”。

这个“起点基准”就是栈底那个-1的升级版。为什么要放-1?因为如果整个字符串从第一个字符开始就有效,比如"()",遍历到下标1时,弹出左括号下标0,栈里还剩-1,那么1 - (-1) = 2,长度就是2。如果栈底不提前放-1,这一步就算不出长度了。

放上完整代码(Python版,力扣支持的语言无所谓,思路一致):

def longestValidParentheses(s: str) -> int: stack = [-1] # 栈底基准下标 max_len = 0 for i, ch in enumerate(s): if ch == '(': stack.append(i) else: stack.pop() if stack: max_len = max(max_len, i - stack[-1]) else: stack.append(i) # 多余右括号,重置基准 return max_len

这个解法的时间复杂度O(n),空间复杂度O(n)。思路很好懂,也足够AC。

2.2 动态规划解法:状态转移方程里藏着两个分支

但是,如果你只会栈解法,我只能说这道题你掌握了一半。DP解法才是真正锻炼思维的地方。

DP思路是这样的:定义dp[i]表示以s[i]结尾的最长有效括号长度。注意这个**“以s[i]结尾”非常关键**,它限定了我们只统计连续且有效的子串。

然后分情况讨论:

情况一:s[i] == '(',那dp[i] = 0。因为以左括号结尾的子串不可能有效,肯定长度为0。这没什么好说的。

情况二:s[i] == ')',再分两个分支:

  • 分支A:s[i-1] == '(',也就是形如...(),此时dp[i] = dp[i-2] + 2。因为()本身贡献2,再加上()前面那个连续有效子串的长度dp[i-2]。举例:"(()())",计算下标5时,s[4]='(',所以dp[5] = dp[5-2] + 2 = dp[3] + 2 = 2 + 2 = 4。

  • 分支B:s[i-1] == ')',也就是形如...)),此时要考虑是否有一个左括号能和当前右括号匹配。这个左括号应该位于i - dp[i-1] - 1的位置(也就是“以 s[i-1] 结尾的有效子串再往前一个字符”)。如果这个位置存在且是(,那么匹配成功,dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2]。这里最后再加dp[i - dp[i-1] - 2]是因为匹配成功后,当前子串前面可能还接着另一个有效子串,要连起来。举例:"()(())",计算下标5时,s[4]=')',dp[4] = 2,所以i - dp[i-1] - 1 = 5 - 2 - 1 = 2,s[2]='(',匹配成功,dp[5] = dp[4] + 2 + dp[2 - 1] = 2 + 2 + dp[1] = 2 + 2 + 0 = 4。

这个分支B的转移方程,是这道题最劝退的地方。很多人就是在这里绕晕的。我画个图来理解一下:

下标: 0 1 2 3 4 5 字符: ( ) ( ( ) ) dp: 0 2 0 0 2 4

计算dp[5]时,s[5]=')',s[4]=')',所以要看i - dp[i-1] - 1 = 5 - 2 - 1 = 2这个位置,s[2]='(',匹配成功。此时dp[5] = dp[4] + 2 + dp[1],其中dp[1] = 2表示最前面的()。合起来整个字符串"()" + "(())"长度为 4,正确。

代码实现如下:

def longestValidParentheses(s: str) -> int: dp = [0] * len(s) max_len = 0 for i in range(1, len(s)): if s[i] == ')': if s[i-1] == '(': dp[i] = (dp[i-2] if i >= 2 else 0) + 2 else: # s[i-1] == ')' j = i - dp[i-1] - 1 if j >= 0 and s[j] == '(': dp[i] = dp[i-1] + 2 + (dp[j-1] if j >= 1 else 0) max_len = max(max_len, dp[i]) return max_len

注意边界:i-2可能越界,j-1也可能越界,都要加保护判断。我有一个习惯:写DP的转移方程时,先写无边界条件的版本,再分别在每个数组下标访问的地方补if ... else,这样可以避免漏掉边界。

2.3 为什么推荐两种解法都掌握

栈解法的优势是直观,不用琢磨复杂的转移方程;DP解法的优势是逻辑完整,能帮你建立“以某位置结尾”的DP思维框架。两种都能AC,但在面试场景下,如果面试官追问“这个状态到底代表什么”,你能随时说出dp[i]的语义,比背代码强得多。

我实际刷题的经验是:先用栈AC一道题,再用DP重新AC一遍,这个方法对动态规划入门特别有效。同样的题目,两种思路互相对照,比刷十道新题都管用。而且力扣的讨论区里,32题最有价值的不是代码,而是那些配着图解释分支B的手绘图,我建议刷到这题的朋友别急着看代码,先在草稿纸上按上面的方法画一遍。

注意:这道题的DP里dp[i]表示“以i结尾的最长有效括号长度”,而题目的答案是max(dp),不是dp[-1]。因为最长有效子串不一定在字符串末尾。这个“遍历过程中不断取max”的模式在很多DP题里通用。

3. 力扣62:不同路径——最经典的二维DP表格

相比32的下标烧脑,62就友好多了。它是标准的二维DP入门题,非常适合建立表格感。

3.1 状态定义和转移方程一次讲透

定义dp[i][j]为“从左上角到达(i, j)的路径数”。机器人每次只能向下或向右,所以到达(i, j)只有两种可能:

  • 从上方(i-1, j)走下来
  • 从左侧(i, j-1)走过来

因此:dp[i][j] = dp[i-1][j] + dp[i][j-1]。

这个方程简单到让人想笑,但它的推导过程是动态规划的核心范式。写出来:

def uniquePaths(m: int, n: int) -> int: # dp[i][j] 表示从起点到 (i, j) 的路径数 dp = [[0] * n for _ in range(m)] # 第一行和第一列都只有一条路径 for j in range(n): dp[0][j] = 1 for i in range(m): dp[i][0] = 1 for i in range(1, m): for j in range(1, n): dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[m-1][n-1]

3.2 初始化细节:第一行和第一列的“唯一路径”为什么是1

很多人写二维DP时会忽略初始化,直接套转移方程,结果数组越界或者答案全是0。这里的关键是:dp[0][j]和dp[i][0]没有“上方”或“左侧”,它们只能沿着边界一直走,所以路径数恒为1。

比如说3行7列的网格,第一行的所有格子都只能“一直向右走”到达,所以每个格子路径数都是1;第一列同理。如果你不从这些格子开始填表,内层循环到i=0或j=0时就会访问越界下标。

还有个小细节:整个dp数组的初始化值是多少不重要(反正会被覆盖),但建议统一初始化为0。因为dp[0][0]实际上是起点本身,从左到右从下到上都不需要经过什么路径,它本身就代表“1种方式”,因为机器人在起点时不需要移动就已经在起点。把dp[0][0]直接设为1也可以,但需要确保第一行第一列的循环逻辑一致,否则会重复赋值。

3.3 空间压缩:一维数组滚动更新的原理与写法

62题还有个大名鼎鼎的优化技巧:滚动数组,能把空间复杂度从O(m×n)压缩到O(n)。原理是:dp[i][j]只依赖上一行的dp[i-1][j]和当前行的dp[i][j-1],所以只要保留一行数据,从左到右原地更新即可。

def uniquePaths(m: int, n: int) -> int: dp = [1] * n for _ in range(1, m): for j in range(1, n): dp[j] = dp[j] + dp[j-1] return dp[n-1]

这段代码的精髓在于:dp[j]在更新前存的是上一行的值dp[i-1][j],dp[j-1]在当前行已经更新成dp[i][j-1]了,两者相加正好是dp[i][j]。很多初学者觉得这写法神乎其神,其实就是“当行从左到右滚动覆盖”。

提示:我用一个生活类比帮你记——这就像一个走廊里有一排计数器,每到一个格子,你把当前计数器的旧值加上左边计数器的新值,得到当前格子应该有的值。走到走廊尽头,这一排计数器正好变成了新的一行的数据。

62题的变体很多,比如有障碍物的63(不同路径II)、带权重的后续题等。你把62的空间压缩写法吃透之后,64题最小路径和也用得上。

4. 力扣64:最小路径和——在路径数量上叠加代价

如果说62是“数路”,64就是“选路”。同样是二维网格,同样是只能向右向下,但这次每个格子有个权值,你要找一条总和最小的路径。62的转移是加法,64的转移是取最小值再加当前值。

4.1 状态转移方程:取小者,再加当前格值

定义dp[i][j]为“从起点到达(i, j)的最小路径和”。

要到达(i, j),只有两个来源:上面的(i-1, j)和左边的(i, j-1)。为了让总和最小,当然选这两个来源里更小的一种,然后加上当前格子的值:

dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]

没错,就这么简单。代码:

def minPathSum(grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) dp = [[0] * n for _ in range(m)] # 起点初始化 dp[0][0] = grid[0][0] # 第一行 for j in range(1, n): dp[0][j] = dp[0][j-1] + grid[0][j] # 第一列 for i in range(1, m): dp[i][0] = dp[i-1][0] + grid[i][0] # 主循环 for i in range(1, m): for j in range(1, n): dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j] return dp[m-1][n-1]

4.2 和62的对比:同样是二维DP,差别在哪里

62和64代码结构几乎一样,只是dp[i][j]的计算方式从+变成了min +。但这里有个非常关键的差异,值得单独拎出来讲:初始化不是全1,而是累加前缀和。

62的第一行和第一列路径数全是1,因为一到边界就只有直线这一种走法。但64的第一行和第一列是“沿着边界一直走的代价累加”,因为边界上没有选择,从起点到某个边界格子的路径是唯一的,代价就是一路上所有格子值的和。

这个差异是64题最容易错的地方。有个同学问过我:“为什么第一行不能每个都是grid[0][j]?”我反问他:“你从起点走到(0, 2),难道不经过(0, 1)吗?经过的话,代价为什么不加?”他一拍大腿就明白了。边界上的路径是“唯一确定的路线”,代价必须连续累加。如果你只写grid[0][j],等于跳过了中间的格子,答案肯定错。

4.3 原地修改技巧:能否直接改原数组

有没有可能不用额外dp数组?可以。如果你不介意修改原数组,直接在grid上操作就行,因为每个格子只被读一次,改完也不会影响后续计算。这样空间复杂度降到O(1):

def minPathSum(grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) for i in range(m): for j in range(n): if i == 0 and j == 0: continue elif i == 0: grid[i][j] += grid[i][j-1] elif j == 0: grid[i][j] += grid[i-1][j] else: grid[i][j] += min(grid[i-1][j], grid[i][j-1]) return grid[m-1][n-1]

这段代码连额外空间都省了,但是有两个前提:一是你不在原数组上做别的用途,二是你清楚地知道自己在修改原数据。我个人的习惯是:刷题阶段宁可多开一个dp数组,可读性优先;如果面试官明确要求O(1)空间,再展示原地修改。写代码是给人看的,不是给自己炫技的。

5. 三道题横向对比:从状态定义到代码结构的异同

这三道题刷完之后,我建议你停下来做一个对比表格,把它们放在一起看,比单独刷十道题更有收获。

5.1 状态定义、转移方程、初始化、遍历顺序对比

题目状态定义转移方程初始化遍历顺序
32dp[i]:以s[i]结尾的最长有效括号长度s[i]=='('时0;s[i]==')'且s[i-1]=='('时dp[i-2]+2;s[i-1]==')'且j=i-dp[i-1]-1处是(时dp[i-1]+2+dp[j-1]dp[0]=0,其余为0从左到右
62dp[i][j]:到达(i,j)的路径数dp[i][j] = dp[i-1][j] + dp[i][j-1]第一行第一列全为1逐行逐列
64dp[i][j]:到达(i,j)的最小路径和dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]dp[0][0]=grid[0][0],第一行第一列为累加和逐行逐列

从这个表能看出什么?32是一维DP,62和64是二维DP;32和62的状态转移都不涉及“代价选择”,只有64涉及“取min”。这意味着32和62更偏向“计数型DP”,64是“最优型DP”。

5.2 为什么62和64的遍历顺序是从上到下、从左到右

很多初学者看到二维DP的循环嵌套就直接抄,不知道为什么。其实原因很简单:当前格子的值依赖上方和左方的值,所以必须保证计算(i,j)前,(i-1,j)和(i,j-1)都已经算好了。逐行从上到下、行内从左到右,天然满足这个依赖关系。

换个角度验证:如果你从右往左填一行,那么算(i,j)时左边的(i,j-1)还没算,结果就是错的。这个“依赖方向决定遍历方向”的规律,在DP里是通吃的。如果你遇到一个问题,发现状态依赖右边和下面,那就得换遍历方向,比如从右下往左上填。

5.3 空间压缩技巧是否是通用的

62的空间压缩(滚动数组)完全可以搬到64上,因为64同样只依赖上一行和当前行左边的值:

def minPathSum_rolled(grid: List[List[int]]) -> int: m, n = len(grid), len(grid[0]) dp = [0] * n dp[0] = grid[0][0] for j in range(1, n): dp[j] = dp[j-1] + grid[0][j] for i in range(1, m): dp[0] += grid[i][0] for j in range(1, n): dp[j] = min(dp[j], dp[j-1]) + grid[i][j] return dp[n-1]

这里的细节是:外层循环进入新一行时,dp[j]存的是上一行的值dp[i-1][j],dp[j-1]存的是当前行已经更新的值dp[i][j-1],所以直接min(dp[j], dp[j-1]) + grid[i][j]就是正确的当前格值。这个“上一行遗留 + 当前行新算”的思维,是滚动数组的核心。

6. 实操中的常见问题与排查心得

刷这三道题,我遇到过的典型问题不少。整理一份问题清单和排查方法给你。

6.1 力扣32容易踩的空和边界问题

32的DP解法里最容易错的点有两个:一是dp[i-2]可能越界(当i==1时),二是分支B里j = i - dp[i-1] - 1可能是负数。我建议你在写代码之前先把i=0和i=1这两种情况在纸上演算一遍,确认循环从i=1开始是安全的,然后所有越界访问都加上判断。

栈解法也有坑:如果输入是空字符串,栈初始[-1]不弹出,max_len为0,正确;如果输入全是右括号,比如")))",每次遇到右括号都会弹出、栈空、把当前下标压入,最后max_len还是0,正确。这个行为恰好验证了“多余右括号重置基准”的设计。

6.2 力扣62容易忽视的整数溢出问题

62的答案会随m和n快速增大。比如m=10, n=10时答案是48620,但m=23, n=12时答案已经到193536720,超过2亿。力扣的约束是1 <= m, n <= 100,当m=100, n=100时答案远超32位整数的上限。如果你用的语言默认int是32位,这里就会溢出。

我建议在 Python 里不用担心,但在 C++/Java 里要记得用long或long long。如果面试时被问到这个细节,你主动提一句“这个值可能超出32位范围,我需要用更大的整数类型”,会显得很有经验。

6.3 力扣64的初始化顺序和“先填边界还是先填整体”之争

64题初始化容易糊涂的是dp[0][0]到底要不要单独处理。我的写法是先单独给dp[0][0] = grid[0][0],然后第一行从j=1开始累加,第一列从i=1开始累加,最后主循环从i=1, j=1开始。这种“先把边界煮熟,再煮内部”的思路很直观。

但也有一些题解会把所有格子放在一个双重循环里用if-elif-else判断来源,像我上面展示的原地修改写法那样。两种写法都能AC,但维护性和可读性差别很大。我个人偏好“显式初始化边界”,因为边界上的语义就是和其他位置不同,写成独立的循环一目了然。

6.4 调试DP表格的实战方法:打印二维数组

这三道题,特别是62和64,如果你答案不对,我最推荐的调试方法不是看变量,而是把dp数组打印出来。你可以写一个简单的辅助函数:

def print_dp(dp): for row in dp: print(row)

然后在关键循环结束之后打印一次。比如62题,你跑一个3x3的输入,打印结果应该是:

[1, 1, 1] [1, 2, 3] [1, 3, 6]

如果看到某一行不对,马上就能定位是初始化问题还是转移方程写错。很多问题你看代码半天看不出来,表格一打印,毛病立刻现行。这个方法对任何二维DP都适用,包括64。

6.5 刷题顺序的心得:为什么先62再64再32

我的刷题顺序建议是:62 -> 64 -> 32。

先62是因为它最纯粹,没有任何弯弯绕,就是让新手第一次体验“填DP表”的感觉。64在62基础上加了“取min”,让你体会“最优型DP”和“计数型DP”的区别。32放到最后,因为它的一维DP其实比二维DP更难想象,特别是分支B的回溯索引,需要你脑子里能画出一个括号子串的“边界位置图”。

反过来如果你先刷32,很容易被劝退,产生“DP太难了”的错觉。实际上不是DP难,是32的边界情况多。用难度梯度喂自己,才是刷题的正确打开方式。

7. 一套通用的动态规划审题方法

说了这么多具体题目,最后送你一套通用的DP审题方法。这是我刷了几百题之后总结出来的,遇到新题直接用:

7.1 四步审题法:状态、转移、初始化、答案

第一步,定状态。问自己:要找的答案,能不能用“某个位置/某个元素结尾/到达”来描述?能,就把这个描述写成dp[i]或dp[i][j]。

第二步,写转移。问自己:当前状态能不能由“前一个/上边/左边的状态”推出?把这个关系写成方程。

第三步,定初始化。问自己:最小的子问题是什么?边界位置的值是多少?这一步最容易错,把所有数组越界位置都在草稿纸上标出来。

第四步,想答案。问自己:题目要的最终结果等于哪个状态?结尾的dp[n-1]不一定是对的,可能要遍历取max(如32题),可能要取表格右下角(如62、64)。

这个方法不能保证你解出所有DP题,但能保证你不至于拿到题后一脸懵。至少你能快速判断出“这道题合不合适用DP”。

7.2 从这三道题延伸出去的变体题目

刷完32、62、64,我建议你接着刷这几道题巩固:

  • 力扣63:不同路径II(62的变体,网格里有障碍物,转移时跳过障碍即可)
  • 力扣70:爬楼梯(和32一样是一维DP,只不过转移非常简单)
  • 力扣120:三角形最小路径和(64的变体,状态转移略复杂)
  • 力扣279:完全平方数(一维DP求最小数量)

你会发现,这些题的本质都逃不出前面总结的框架。所谓“刷题攻略”,不是让你背题解,而是让你积累足够多的“模型”,看到新题时能快速匹配到已有模型并做出微调。

就拿63来说,62的dp[i][j] = dp[i-1][j] + dp[i][j-1]需要加一个条件:如果(i,j)是障碍,dp[i][j] = 0。这就是“计数型DP遇到障碍怎么处理”的通用套路,你在62上理解了表格,在63上就不会慌。

8. 最后再分享一点刷题心态

这三道题我前前后后刷了不止五遍,每次重刷都有新体会。第一遍刷32的时候,我连题目都看不懂,什么叫“有效括号子串”都理解了半天;后来刷到64,还因为dp[0][0]初始化错误卡了半小时。但正是这些卡壳,让我现在看到同类题能一眼看穿状态定义和转移方向。

如果你现在觉得DP很难,我给你一个定心丸:DP的难是“门内难”,不是“门外难”。就是说,你只要推开第一道门,把62这种最基础的表格题搞明白,后面64、32甚至更难的题,都是在这个基础上叠加条件而已。不怕慢,就怕不总结。每次刷完题,花十分钟把状态定义、转移方程、初始化、遍历顺序四个方面写下来,比多刷十道题都值。

说实话,力扣上很多人是把这三道题当成“经典题”背下来的,但我更希望你能把这三题当成“模型题”来理解。背下来的是别人的经验,理解了的才是自己的判断力。动态规划的核心从来不是代码技巧,而是思维习惯:看到问题,先想“最后一步是什么,之前的状态怎么组合”——这个习惯一旦养成,你刷题的后半程会越走越顺。

按我自己的进度,从这三道题出发,大概两周就能把一维DP和二维DP的主要变体摸熟。希望这篇拆解能帮你少走一点弯路。刷题没有捷径,但有地图——62、64、32就是这张地图上最清楚的三个地标。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 18:32:53

OpenRIG:用铝型材和3D打印件搭建开放式模块化设备机架

OpenRIG 这个名字一开始只是我在旧货市场看到一堆闲置设备时冒出来的念头&#xff1a;手头的开发板、传感器、电源模块、树莓派、路由器全都散在纸箱里&#xff0c;每次要调试就得翻半天&#xff0c;插线靠猜&#xff0c;散热靠开窗。所以我决定自己搭一个开放式模块化机架&…

作者头像 李华
网站建设 2026/10/3 18:31:32

ZooKeeper原理与实战:从Hadoop高可用到分布式协调服务

直接从标题聊起。“ZooKeeper 知多少”这个问题&#xff0c;我在好几个社群和面试场合里都被反复问到过。很多人第一次接触它&#xff0c;是从报名Hadoop集群开始的——毕竟当年Hadoop 2.X版本里&#xff0c;NameNode的高可用全靠它撑场子。但如果你只是为了装一个集群去把ZooK…

作者头像 李华
网站建设 2026/10/3 18:31:31

电商用户行为日志驱动的协同过滤推荐系统毕业设计包

简介&#xff1a;本资源是一套完整的基于协同过滤的商品推荐系统毕业设计实现方案&#xff0c;面向计算机专业本科生及推荐系统初学者&#xff0c;解决电商场景下个性化商品推荐的核心问题。项目采用Python语言开发&#xff0c;集成NumPy、pandas与scikit-learn等主流库&#x…

作者头像 李华
网站建设 2026/10/3 18:31:27

STC8单片机低功耗延时优化:从循环等待到定时唤醒

做低功耗设备最容易被忽略的&#xff0c;恰恰是那些看起来人畜无害的延时函数。我之前调一个STC8电池供电的采集节点&#xff0c;一开始用delay_ms(1000)控制采样周期&#xff0c;满心以为1秒醒一次很省电&#xff0c;结果整机平均电流干到5mA以上&#xff0c;两节CR2032撑了不…

作者头像 李华
网站建设 2026/10/3 18:28:57

Spark电影推荐毕设源码实战:ALS调参与论文避坑指南

简介&#xff1a;这是一套面向计算机相关专业学生的Spark电影推荐系统毕业设计完整资料&#xff0c;适合正在准备毕业设计、课程设计或期末大作业的学习者&#xff0c;也适合希望积累推荐系统项目实战经验的同学。资源包共80个文件&#xff0c;约16.18MB&#xff0c;以Java源码…

作者头像 李华
网站建设 2026/10/3 18:28:31

Python+tkinter+MySQL图书管理系统:从课程设计到高并发实战

简介&#xff1a;这是一套面向计算机相关专业学生的图书管理系统课程设计资源&#xff0c;采用Python结合tkinter图形界面与MySQL数据库实现&#xff0c;适合正在做大作业、课程设计或需要项目实战练习的学习者参考。资源包共13个文件&#xff0c;包含6个py源码文件、4个txt数据…

作者头像 李华