LeetCode-1143:最长公共子序列,dp[i][j] 表示“前缀到前缀”的最优值

本题在线练习:LeetCode 1143. 最长公共子序列 - 在线练习(免费 · 无需登录 · AI 辅助)(https://onefly.top/zero2Leetcode/playground.html?id=1143)

配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开始刷 Hot 100。

题目概述

给定两个字符串 text1text2,返回它们的最长公共子序列(LCS, Longest Common Subsequence)长度。

子序列强调“相对顺序”,不要求连续。例如 "abcde" 的子序列可以是 "ace"

核心思路:把问题缩小成“两个前缀”的 LCS

设:

  • dp[i][j] 表示 text1 的前 i 个字符(text1[:i])与 text2 的前 j 个字符(text2[:j])的 LCS 长度。

这样定义的好处是:dp[i][j] 的答案可以由更短的前缀推出来。

转移分两种情况:

  1. 如果 text1[i-1] == text2[j-1],那么这个字符可以作为公共子序列的最后一个字符:
dp[i][j] = dp[i-1][j-1] + 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-1j-1 刚好指向各自的最后一个字符。

手动模拟:text1="abcde", text2="ace"

当遍历到:

  • i=1a)、j=1a)相等:dp[1][1]=dp[0][0]+1=1
  • i=3c)、j=2c)相等:dp[3][2]=dp[2][1]+1=2
  • i=5e)、j=3e)相等: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(编辑距离、最长重复子序列等)都与这套定义高度同源。

posted @ 2026-03-27 01:18  孤飞  阅读(48)  评论(0)    收藏  举报