LeetCode-1143:dp[i][j]如何表示的最优匹配?
摘要:本题在线练习:LeetCode 1143. 最长公共子序列 - 在线练习(免费 · 无需登录 · AI 辅助)(https:onefly.topzero2Leetcodeplayg
本题在线练习:LeetCode 1143. 最长公共子序列 - 在线练习(免费 · 无需登录 · AI 辅助)(https://onefly.top/zero2Leetcode/playground.html?id=1143)
配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开始刷 Hot 100。
题目概述
给定两个字符串 text1 和 text2,返回它们的最长公共子序列(LCS, Longest Common Subsequence)长度。
子序列强调“相对顺序”,不要求连续。例如 "abcde" 的子序列可以是 "ace"。
核心思路:把问题缩小成“两个前缀”的 LCS
设:
dp[i][j] 表示 text1 的前 i 个字符(text1[:i])与 text2 的前 j 个字符(text2[:j])的 LCS 长度。
这样定义的好处是:dp[i][j] 的答案可以由更短的前缀推出来。
转移分两种情况:
如果 text1[i-1] == text2[j-1],那么这个字符可以作为公共子序列的最后一个字符:
dp[i][j] = dp[i-1][j-1] + 1
如果不相等,那么最后一个字符至少有一个不参与 LCS,只能从“去掉一个末尾字符”的两个选择中取最大:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
初始化:只要有一个前缀长度为 0,则 LCS 为 0,所以第一行第一列都是 0。
代码实现(二维 dp)
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
逐行拆解:为什么要用 (m+1) x (n+1)
把 dp 做成多一行、多一列,是为了让 dp[0][*]、dp[*][0] 代表“空串前缀”,从而避免边界判断。
此时 dp[i][j] 永远对应 text1[:i] 和 text2[:j],下标 i-1、j-1 刚好指向各自的最后一个字符。
手动模拟:text1="abcde", text2="ace"
当遍历到:
i=1(a)、j=1(a)相等:dp[1][1]=dp[0][0]+1=1
i=3(c)、j=2(c)相等:dp[3][2]=dp[2][1]+1=2
i=5(e)、j=3(e)相等:dp[5][3]=dp[4][2]+1=3
最终答案为 3,对应公共子序列 "ace"。
进阶:一维滚动数组(可选)
二维 dp 直观,但空间是 O(mn)。如果只要长度,可以做空间优化到 O(n):
用 dp[j] 表示当前行的 dp[i][j]
需要一个变量 prev 保存上一行的 dp[i-1][j-1]
这里不展开写完整代码也能理解:本质是“当前格依赖左、上、左上”,滚动数组要额外记住“左上”。
复杂度分析
时间:O(mn)
空间:二维 O(mn),滚动后 O(n)
总结
LCS 的关键是把 dp[i][j] 定义成“两个前缀的最优值”。当这个定义建立起来,转移只有两句话:
字符相等:继承左上并 +1
字符不等:在“去掉一个末尾字符”的两条路里取最大
后续很多字符串 DP(编辑距离、最长重复子序列等)都与这套定义高度同源。
