LeetCode 1143:最长公共子序列——Java 二维动态规划详解

一、题目描述

给定两个字符串 text1text2,返回它们最长公共子序列的长度。如果不存在公共子序列,则返回 0

子序列是从原字符串中删除零个或多个字符后,在不改变剩余字符相对顺序的情况下得到的新字符串。

例如:

复制代码
"ace" 是 "abcde" 的子序列

因为可以删除字符 bd,剩余字符的顺序仍然是 a → c → e

但:

复制代码
"aec" 不是 "abcde" 的子序列

虽然三个字符都存在,但它们在原字符串中的相对顺序不一致。

题目示例:

复制代码
输入:text1 = "abcde",text2 = "ace"
输出:3
解释:最长公共子序列是 "ace"。

输入:text1 = "abc",text2 = "abc"
输出:3

输入:text1 = "abc",text2 = "def"
输出:0

二、子序列与子串的区别

"最长公共子序列"最容易误解的地方,是把子序列当成子串。

  • 子串要求字符在原字符串中连续;

  • 子序列不要求连续,但必须保持相对顺序。

对于字符串:

复制代码
"abcde"

"bcd" 既是子串,也是子序列;"ace" 是子序列,但不是子串。

本题允许跳过字符,因此不能只在两个字符串中寻找连续相同的部分。


三、为什么使用动态规划?

假设正在比较两个字符串末尾的字符,可以分为两种情况。

1. 末尾字符相同

如果当前两个字符相同,那么它们可以共同加入公共子序列。问题就转化成:

求两个字符串去掉当前字符后的最长公共子序列,再加上当前这个相同字符。

2. 末尾字符不同

如果当前两个字符不同,它们不可能同时作为同一公共子序列的最后一个字符。此时至少要舍弃其中一个:

  • 舍弃 text1 当前字符;

  • 舍弃 text2 当前字符。

分别计算这两种情况,再保留较大的结果。

大问题可以由更短字符串的答案推导,而且子问题会被重复使用,因此适合使用动态规划。


四、定义动态规划状态

定义:

复制代码
dp[i][j]:text1 前 i 个字符与 text2 前 j 个字符的
          最长公共子序列长度

注意,这里的 ij 表示字符数量,不是正在访问的字符下标。

例如:

复制代码
dp[3][2]

表示 text13 个字符与 text22 个字符的最长公共子序列长度。

如果:

复制代码
text1 = "abcde"
text2 = "ace"

那么 dp[3][2] 比较的就是:

复制代码
"abc" 和 "ac"

最终答案为:

复制代码
dp[len1][len2]

五、为什么 DP 数组要多一行一列?

代码创建:

复制代码
int[][] dp = new int[len1 + 1][len2 + 1];

多出的第 0 行和第 0 列用于表示空字符串。

  • dp[0][j]:空字符串与 text2j 个字符比较;

  • dp[i][0]text1i 个字符与空字符串比较。

空字符串与任何字符串的最长公共子序列长度都为 0,而 Java 创建 int 数组后默认值就是 0,因此不需要额外初始化。

这种设计还有一个好处:计算第一个字符时,可以安全访问 dp[0][0],不需要为数组负下标编写特殊判断。


六、推导状态转移公式

代码使用 ij 遍历字符串下标:

复制代码
for (int i = 0; i < len1; i++) {
    for (int j = 0; j < len2; j++) {
        // 比较 text1.charAt(i) 与 text2.charAt(j)
    }
}

由于 dp 的下标表示字符数量,所以字符 text1.charAt(i)text2.charAt(j) 对应的当前状态是:

复制代码
dp[i + 1][j + 1]

1. 当前字符相等

如果:

复制代码
text1.charAt(i) == text2.charAt(j)

当前字符可以加入公共子序列。去掉这两个字符后,对应的前置状态是 dp[i][j],因此:

复制代码
dp[i + 1][j + 1] = dp[i][j] + 1;

例如比较 "abc""ac" 的最后一个字符时,两个字符都是 c。只需要先求 "ab""a" 的答案,再加 1

2. 当前字符不相等

如果:

复制代码
text1.charAt(i) != text2.charAt(j)

需要比较两种选择:

复制代码
dp[i][j + 1]:舍弃 text1 当前字符
dp[i + 1][j]:舍弃 text2 当前字符

为了获得最长结果,取两者最大值:

复制代码
dp[i + 1][j + 1] = Math.max(
        dp[i + 1][j],
        dp[i][j + 1]
);

完整状态转移公式为:

复制代码
如果 text1[i] == text2[j]:
    dp[i+1][j+1] = dp[i][j] + 1

否则:
    dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])

核心思想可以概括为:

当前字符相同就同时选取;当前字符不同就分别舍弃其中一个,并保留更优结果。


七、示例推演:"abcde""ace"

text1 放在行方向,text2 放在列方向。第 0 行和第 0 列表示空字符串。

最终 DP 表如下:

dp a c e
0 0 0 0
a 0 1 1 1
b 0 1 1 1
c 0 1 2 2
d 0 1 2 2
e 0 1 2 3

下面观察几个关键位置。

1. 比较 aa

两个字符相等:

复制代码
dp[1][1] = dp[0][0] + 1 = 1

最长公共子序列是 "a"

2. 比较 bc

两个字符不相等:

复制代码
dp[2][2] = max(dp[2][1], dp[1][2])
         = max(1, 1)
         = 1

3. 比较 cc

两个字符相等:

复制代码
dp[3][2] = dp[2][1] + 1
         = 1 + 1
         = 2

此时公共子序列可以是 "ac"

4. 比较 ee

两个字符相等:

复制代码
dp[5][3] = dp[4][2] + 1
         = 2 + 1
         = 3

最终:

复制代码
dp[5][3] = 3

最长公共子序列为 "ace",长度为 3


八、Java 完整代码

复制代码
class Solution {

    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length();
        int len2 = text2.length();

        // dp[i][j] 表示 text1 前 i 个字符和 text2 前 j 个字符的
        // 最长公共子序列长度
        int[][] dp = new int[len1 + 1][len2 + 1];

        for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {
                if (text1.charAt(i) == text2.charAt(j)) {
                    // 当前字符相同,同时加入公共子序列
                    dp[i + 1][j + 1] = dp[i][j] + 1;
                } else {
                    // 当前字符不同,分别舍弃其中一个字符
                    dp[i + 1][j + 1] = Math.max(
                            dp[i + 1][j],
                            dp[i][j + 1]
                    );
                }
            }
        }

        return dp[len1][len2];
    }
}

代码整体可以分为四步:

  1. 获取两个字符串的长度;

  2. 创建多一行、多一列的二维 DP 数组;

  3. 从左上到右下比较每一对字符并填写状态;

  4. 返回右下角的 dp[len1][len2]

九、复杂度分析

text1 长度为 mtext2 长度为 n

时间复杂度

两层循环会比较每一对字符,因此时间复杂度为:

复制代码
O(m × n)

空间复杂度

使用了一个 (m+1) × (n+1) 的二维数组,因此空间复杂度为:

复制代码
O(m × n)
相关推荐
风流 少年2 小时前
Spring AI 2.0:阿里云百炼平台(工作流应用)
java·后端·spring
长谷深风1112 小时前
AI Tool 设计:粒度、参数与错误恢复怎么做
java·大数据·人工智能·ai agent·agent工作流·智能体设计·ai产品设计
傅科摆 _ py2 小时前
动态规划复习
算法·动态规划
QQ_21696290963 小时前
【项目编号:project95315】SpringBoot公共自习室管理系统:座位预约、房间管理、签到核销、公告规则完整实战
java·spring boot·后端
嵌入式阿蔡3 小时前
面试高频考点 01:volatile / 中断 / 堆栈 八股精讲
java·面试·职场和发展·嵌入式实时数据库
Nil2083 小时前
leetcode 108有序数组转换为二叉搜索树
数据结构·算法·leetcode
hn小菜鸡3 小时前
LeetCode 763、划分字母区间
数据结构·算法·leetcode
秋饼3 小时前
LangChain4j + Java 实现企业级 Text-to-SQL 智能问数系统:从自然语言到安全可控的数据洞察
java·ai·技术分享·后端开发
Escalating_xu3 小时前
【C++ STL简介】从六大组件到容器、迭代器与算法协作
java·c++·算法
何以解忧,唯有..4 小时前
Python os模块详解:文件与目录操作指南
java·服务器·python