递归+记忆化DP数组填充顺序问题:如何反转填充顺序?
修改递归逻辑以匹配迭代DP的填充顺序
问题本质
原递归+记忆化的实现是自顶向下的:从整个字符串的两端(i=0, j=n-1)开始,递归拆分出更小的子问题求解,因此DP数组的填充顺序是先填充大区间(长串),再填充小区间(短串)。而从索引0开始的嵌套循环是自底向上的:先填充所有长度为1的子串,再依次填充长度2、3...直到整个字符串,填充顺序是小区间先于大区间。
要让递归的DP填充顺序和迭代一致,核心是主动按子串长度从小到大的顺序触发递归求解,确保计算当前区间时,所有依赖的子问题已经被处理完毕。
具体修改方案
方案1:按子串长度递归遍历(推荐)
直接模拟迭代的逻辑,用递归按子串长度从小到大处理,先完成所有小区间的DP填充,再处理大区间:
def min_insertions(s): n = len(s) # 初始化DP数组,-1表示未填充 dp = [[-1] * n for _ in range(n)] # 先处理所有长度为1的子串(i==j),无需插入 for i in range(n): dp[i][i] = 0 # 递归函数:处理所有长度为l的子串 def dfs(l): # 递归终止:子串长度超过字符串长度 if l > n: return # 遍历所有长度为l的子串起始索引i for i in range(n - l + 1): j = i + l - 1 if s[i] == s[j]: # 两端字符相等,直接取内层子串的结果(长度l-2,已处理) dp[i][j] = dp[i+1][j-1] else: # 取左右子串的最小值+1(长度l-1,已处理) dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1 # 递归处理更长一级的子串 dfs(l + 1) # 从长度为2的子串开始触发递归 dfs(2) return dp[0][n-1]
示例验证
- 对于
"madam"(长度5):递归会先处理长度1的子串→长度2→长度3→长度4→长度5,DP填充顺序和迭代完全一致。 - 对于
"leetcode"(长度8):递归依次处理长度1到8的子串,每一步的子问题都已提前填充,最终DP数组的填充顺序和嵌套循环完全匹配。
方案2:原递归中优先求解子问题
如果不想完全重构递归逻辑,可以在原函数中先递归求解所有依赖的子问题,再计算当前区间的结果,强制让小区间先被填充:
def min_insertions(s): n = len(s) dp = [[-1] * n for _ in range(n)] def dfs(i, j): if i >= j: return 0 if dp[i][j] != -1: return dp[i][j] # 先递归求解所有依赖的子问题(强制先填充小区间) dfs(i+1, j) # 长度为j-i的子串 dfs(i, j-1) # 长度为j-i的子串 dfs(i+1, j-1) # 长度为j-i-2的子串 # 现在计算当前区间的结果 if s[i] == s[j]: dp[i][j] = dp[i+1][j-1] else: dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1 return dp[i][j] dfs(0, n-1) return dp[0][n-1]
说明
这种方式会提前触发所有子问题的求解,因此DP数组会先填充所有小区间,再逐步填充大区间,最终和迭代顺序一致。但注意会存在重复递归调用(比如dfs(1,3)可能被多个上层问题触发),效率略低于方案1。
核心总结
- 要匹配迭代的填充顺序,必须先处理所有更小的子问题,再处理当前问题。
- 方案1直接模拟迭代的长度遍历逻辑,用递归替代循环,填充顺序完全一致且效率较高。
- 方案2通过提前递归求解子问题,强制调整填充顺序,适合不想大幅修改原有递归结构的场景。
内容的提问来源于stack exchange,提问作者curiousengineer
相关产品推荐
相关产品推荐

