在最长公共子序列(LCS)问题中,设计出 dp[i][j] 这样的二维动态规划数组,是基于对问题本质的分析和动态规划思想的自然推导。具体思路可以拆解为以下几步:

1. 首先理解问题的核心矛盾

LCS 问题要解决的是:两个字符串中,找出长度最长的、顺序一致但不要求连续的公共子序列
例如 str1="abcbdab"str2="bdcaba",LCS 是 "bcab""bdab",长度为 4。

关键矛盾在于:子序列的“不连续性”和“顺序一致性” 导致无法用简单的遍历解决,必须记录中间状态才能逐步推导。

2. 从“子问题”入手(动态规划的核心思想)

动态规划的本质是将复杂问题拆解为可重复解决的子问题,并通过记录子问题的解来避免重复计算。

对于 LCS 问题,最自然的子问题拆分方式是:

  • 考虑 str1 的前 i 个字符(str1[0..i-1])和 str2 的前 j 个字符(str2[0..j-1]
  • 计算这两个“前缀子串”的 LCS 长度

为什么这样拆分?因为:

  • 整个问题的解(完整字符串的 LCS)可以由这些子问题的解组合而来
  • 子问题之间存在重叠(例如计算 i=3,j=4 时可能用到 i=2,j=3 的结果)

3. 定义状态:用 dp[i][j] 表示子问题的解

基于上述子问题,直接定义状态:
dp[i][j] = 字符串 str1 的前 i 个字符与 str2 的前 j 个字符的 LCS 长度。

为什么需要二维?因为子问题的边界由两个独立变量确定:istr1 的前缀长度)和 jstr2 的前缀长度)。少了任何一个维度,都无法唯一确定子问题的范围。

4. 推导状态转移方程(如何用子问题解求当前问题解)

确定状态后,需要找到 dp[i][j] 与更小的子问题(如 dp[i-1][j]dp[i][j-1]dp[i-1][j-1])之间的关系。

分两种情况分析:

  • 情况1:str1[i-1] == str2[j-1](当前字符相同)
    这两个字符可以直接加入 LCS 中,因此当前 LCS 长度 = 两个字符串各去掉最后一个字符后的 LCS 长度 + 1,即:
    dp[i][j] = dp[i-1][j-1] + 1

  • 情况2:str1[i-1] != str2[j-1](当前字符不同)
    两个字符无法同时加入 LCS,因此当前 LCS 长度取决于“去掉 str1 最后一个字符”或“去掉 str2 最后一个字符”两种情况的最大值,即:
    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

5. 确定初始状态(最小子问题的解)

i=0j=0 时(即其中一个字符串为空),LCS 长度必然为 0。因此:
dp[0][j] = 0(对任意 j
dp[i][0] = 0(对任意 i

6. 最终解的位置

整个问题的解是两个完整字符串的 LCS 长度,即 dp[str1.length][str2.length]

总结:为什么这样设计 dp 数组?

  1. 问题特性决定维度:LCS 涉及两个独立字符串的匹配,必须用两个维度(ij)分别表示它们的前缀范围。
  2. 子问题自然拆分:拆分前缀子串的 LCS 是最直观的子问题划分方式,能覆盖所有可能的匹配情况。
  3. 状态转移逻辑自洽:通过当前字符是否相同的两种情况,能清晰地用更小的子问题解推导出当前解。

这种设计是动态规划“拆分问题→定义状态→寻找转移关系”思想的典型应用,也是解决“两个序列匹配”类问题的通用思路(如编辑距离、最长重复子数组等均采用类似的二维 DP 设计)。