BM66 最长公共子串LCS(Longest Common Substring)和 BM65 最长公共子序列(二)
最长公共子串LCS
描述
给定两个字符串str1和str2,输出两个字符串的最长公共子串
题目保证str1和str2的最长公共子串存在且唯一。
要求: 空间复杂度 O(n2)O(n2),时间复杂度 O(n2)O(n2)
暴力解法:枚举法
基本思路
枚举法是最直观的暴力解法,通过比较所有可能的子串组合来找到最长的公共子串。
具体实现步骤
- 遍历所有起始位置:
- 遍历
str1
的每个字符作为子串的起始位置i
。 - 遍历
str2
的每个字符作为子串的起始位置j
。
- 扩展子串:
- 从
i
和j
开始,逐个字符比较str1
和str2
。 - 如果字符相等,则继续向后比较,增加当前子串长度。
- 如果字符不等,则停止扩展,记录当前子串长度。
- 维护最大值:
- 在每次找到公共子串时,比较其长度与当前最大值,更新最大值及其位置。
代码实现
1 | def longest_common_substring(str1, str2): |
时间复杂度分析
- 最坏情况:两个字符串完全不相同,每次比较都需要遍历到字符串末尾。
- 外层循环:
O(n)
(str1
的长度) - 内层循环:
O(m)
(str2
的长度) - 内层
while
循环:最坏O(min(n, m))
- 总时间复杂度:
O(n * m * min(n, m))
≈O(n^3)
(当n ≈ m
时)
空间复杂度
- 额外空间:仅使用了常数空间(
max_len
和end_pos
),因此空间复杂度为O(1)
。
动态规划(DP):O(n²)
定义一个dp表,表示局部最长公共子串长度(从末尾开始到开头,也就是最后一个公共子串长度,如果有多个,必须是末尾元素开始的那个,如果末尾开始没有则为0,这样变结尾后就能每次都统计局部最长公共子串长度,最终得到全局的所有局部最长子串长度)
dp[i][j]
的定义:- 表示 以
str1[i-1]
和str2[j-1]
结尾的公共子串的长度(注意不是全局最长)。 - 例如:
dp[3][3] = 2
表示str1[0:3]
和str2[0:3]
的末尾有长度为 2 的公共子串。
具体怎么做?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution:
def LCS(self , str1: str, str2: str) -> str:
height, width = len(str1), len(str2)
# 初始化dp表
dp = [[0] * (width+1) for _ in range(height+1)] # +1
max_len = 0
max_suffix_index_s1 = 0
for i in range(1,height+1):
for j in range(1,width+1):
if str1[i-1]==str2[j-1]:
dp[i][j] = dp[i-1][j-1]+1
if dp[i][j]>max_len:
max_len = dp[i][j]
max_suffix_index_s1 = i
else:
dp[i][j] = 0
return str1[max_suffix_index_s1-max_len:max_suffix_index_s1]
- 表示 以
后缀自动机(SAM): O(n)
奇怪的想法:
给定字符串a和字符串b,两个指针分别指向a和b,当匹配到相同字符后,让a+k,b+k,k表示匹配到的长度,当匹配到不同字符后,记录b’=b+k即可,接下来只需要让a++来匹配b’的位置,因为如果这个位置匹配不到,说明长度不及k,(但是和匹配k=0位置有什么区别吗?)匹配到了依然需要全部匹配,如果匹配不到还是很难处理,但是如果0和b‘匹配不到中间就不用匹配了,如果从0、1依次匹配就还要匹配,因为能直接获得长度相关的信息
字典解法:
1 | from collections import defaultdict |
最长公共子序列
最长公共子串很好理解,无非就是共同的最长句子,公共子序列表示:两个字符串有序、可以零散选出的字符组成的序列,案例:
示例1
输入:"1A2C3","B1D23"
返回值:"123456"
解法:
1 | class Solution: |
初始化思路
每个元素dp[i][j]
依赖左上dp[i-1][j-1]
、正上方dp[i-1][j]
或正左方dp[i][j-1]
的元素,可以直接初始化i=0和j=0的元素为0,然后从i=1开始一行一行计算,每行循环中列也从j=1开始
填充思路
将以s1[:i]
和s2[:j]
的LCS求解问题分解成三个子问题:
- 最后一个字符匹配,前面的串字符之间互相匹配
s1[i]
和s2[:j-1]
部分中的字符匹配,s2[j]
和s1[:i]
都不匹配或者相同字符用完,如果匹配,存在dp[i][j]=dp[i][j-1]
???
如果两个字符串最后一个位置相等,说明整体长度在原先加1,否则??只可能来自
恢复思路
如果最后一个字符相同,则直接增加,并且减一;否则化为子问题
dp[i][j]==dp[i-1][j-1]+1
不是必要条件
常见浅拷贝问题
在二维数组初始化时,不能使用两个乘法而是一个乘法,否则第二个维度只是1d数组的简单引用,导致修改每个列表都会影响别的列表,错误写法❌:
1 | dp = [[0] * (width+1)] * (height+1) |
正确写法✔:
1 | dp = [[0] * (width+1) for _ in range(height+1)] |
数组长度以及含义问题
dp数组表示的不是i,j结尾,而是i-1,j-1结尾!!而i=0 or j=0时只是辅助计算的部分;
依次遍历行即可,不要斜着遍历,无需手动i,j增加
1 | # 1. |