多维动态规划题解——最长公共子序列【LeetCode】记忆化搜索&&翻译成递推

143. 最长公共子序列

方法一:记忆化搜索


一、算法逻辑(每一步思路)

❓ 问题描述:

给定两个字符串 text1text2,返回它们的最长公共子序列的长度

公共子序列:在两个字符串中都出现过,且相对顺序一致,但不要求连续。


✅ 解题思路(递归 + 记忆化)

1. 状态定义:

定义 dfs(i, j) 表示:

  • text1[0..i]text2[0..j] 中的最长公共子序列的长度。
2. 状态转移:
if text1[i] == text2[j]:
    dfs(i, j) = dfs(i-1, j-1) + 1
else:
    dfs(i, j) = max(dfs(i-1, j), dfs(i, j-1))

解释:

  • 如果当前两个字符相等,则这个字符可以作为公共子序列的一部分,答案 +1;
  • 如果不相等,我们可以选择跳过一个字符,取两种可能中的最大值。
3. 终止条件(Base Case):
if i < 0 or j < 0:
    return 0

表示有一个字符串已经空了,公共子序列长度为 0。

4. 返回值:

最终返回 dfs(n - 1, m - 1),即原字符串全部范围的 LCS 长度。


二、算法核心点

✅ 核心技巧:记忆化搜索 + 状态递归

  • 通过 @cache 进行记忆化,避免重复计算(如果不用 cache,会超时);
  • 状态空间是 (i, j) 二维的。

这等价于二维动态规划的形式,但使用递归写法逻辑更清晰。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        n, m = len(text1), len(text2)
        @cache
        def dfs(i:int, j:int) -> int:
            if i<0 or j<0:
                return 0
            if text1[i] == text2[j]:
                return dfs(i-1, j-1)+1
            return max(dfs(i-1, j), dfs(i, j-1))
        return dfs(n-1, m-1)

三、复杂度分析

  • 时间复杂度:O(n * m)
    每个 (i, j) 状态只会被访问一次。
  • 空间复杂度:O(n * m)
    用于保存缓存结果(memo)和递归栈深度。

✅ 举个例子

输入:

text1 = "abcde"
text2 = "ace"

执行过程:

"abcde" 与 "ace"
比较 'e' 和 'e' -> 相等 => +1,继续比较 'd' 与 'c' ...
最终返回公共子序列是 "ace",长度为 3。

✅ 总结表

维度

内容

✅ 思路逻辑

定义 dfs(i, j):text1[0..i] 与 text2[0..j] 的 LCS 长度

✅ 状态转移

相等则 +1,否者取左/上两种子问题最大值

✅ 技巧

递归 + 记忆化(Top-down DP)

✅ 时间复杂度

O(n * m)

✅ 空间复杂度

O(n * m)

 

方法一:1:1 翻译成递推


✅ 一、核心思路解析

❓ 问题:

给定两个字符串 text1text2,找出它们的最长公共子序列的长度。


✅ 解题思路(二维 DP)

1. 状态定义:

定义二维数组 f

  • f[i][j] 表示 text1[0..i-1]text2[0..j-1] 的最长公共子序列长度。

注意这里的偏移:我们实际比较的是字符串的前 i 和前 j 个字符,但 f 的下标是从 1 开始的(第 0 行/列为 base case)。


2. 状态转移方程:
if text1[i] == text2[j]:
    f[i+1][j+1] = f[i][j] + 1
else:
    f[i+1][j+1] = max(f[i][j+1], f[i+1][j])

解释:

  • 如果两个字符相等,则这个字符可以加入公共子序列。
  • 否则,继承“去掉当前一个字符”的最优解。

3. 初始化:
f = [[0] * (m+1) for _ in range(n+1)]

第一行和第一列代表一个字符串为空时,LCS 长度为 0。


4. 最终答案:
return f[n][m]

表示完整的两个字符串的 LCS 长度。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        n, m = len(text1), len(text2)
        f = [[0]*(m+1) for _ in range(n+1)]
        for i,x in enumerate(text1):
            for j,y in enumerate(text2):
                if x==y:
                    f[i+1][j+1] = f[i][j]+1
                else:
                    f[i+1][j+1] = max(f[i][j+1], f[i+1][j])
        return f[n][m]

✅ 二、图解对比示意

假设输入:

text1 = "abcde"
text2 = "ace"

DP 表格 f:

""

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

结果是 3,对应 LCS 为 "ace"


✅ 三、复杂度分析

  • 时间复杂度:O(n * m)
    两层循环枚举所有子串组合。
  • 空间复杂度:O(n * m)
    使用了二维数组 f[n+1][m+1]

可进一步优化为 O(m) 空间,用一维滚动数组,但二维形式更直观清晰。


✅ 总结表格

项目

内容

状态定义

f[i][j]: text1 前 i 字符与 text2 前 j 字符的 LCS 长度

状态转移

如果相等:f[i+1][j+1] = f[i][j] + 1;否则:取最大

初始值

f[0][*] = f[*][0] = 0

返回值

f[n][m]

时间复杂度

O(n * m)

空间复杂度

O(n * m)(可优化)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值