作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。
会一些的技术:数据分析、算法、SQL、大数据相关、python
欢迎加入社区:码上找工作
作者专栏每日更新:
备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级
1. 问题介绍和应用场景
最长公共子序列(LCS)问题是在信息学、生物学以及在日常软件开发中,特别是在版本控制系统的开发中广泛应用的一类问题。LCS可以用来比较两个序列(通常是字符串)的相似度,它寻找在两个序列中都出现过并且所有字符都以相同顺序排列,但不必连续的最长序列。
2. 问题定义和示例
定义: 给定两个字符串 s1
和 s2
,找出这两个字符串的最长公共子序列的长度。公共子序列是指在两个字符串中都能找到的序列,其字符顺序与原始字符串中的顺序相同但不必连续。
示例:
s1 = "abcde"
s2 = "ace"
最长公共子序列是 "ace"
,长度为3。
3. 状态定义
在动态规划中,状态的选择是解题的关键。在本问题中,我们定义 dp[i][j]
为考虑 s1
的前 i
个字符和 s2
的前 j
个字符时的最长公共子序列的长度。
4. 状态转移方程
要构建状态转移方程,考虑以下情况:
- 如果当前字符
s1[i-1]
和s2[j-1]
相等,那么这个字符一定在LCS中。因此,dp[i][j] = dp[i-1][j-1] + 1
。 - 如果不相等,那么LCS可能是
s1
的前i-1
个字符和s2
的前j
个字符的LCS,或者是s1
的前i
个字符和s2
的前j-1
个字符的LCS。因此,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
。
结合上述两点,状态转移方程可以表示为:
5. 初始化和边界情况
初始化对于动态规划问题至关重要,它可以确保状态转移正确进行。对于任何LCS问题:
- 当
i = 0
或j = 0
时,即考虑s1
或s2
的前0个字符,最长公共子序列长度自然为0。因此,dp[0][j] = 0
和dp[i][0] = 0
对所有的i
和j
。
6. 算法实现
以下是使用Python语言实现的LCS问题的动态规划算法:
def longest_common_subsequence(s1: str, s2: str) -> int: """ 计算两个字符串之间的最长公共子序列的长度。 :param s1: 第一个字符串 :param s2: 第二个字符串 :return: 最长公共子序列的长度 """ m, n = len(s1), len(s2) # 创建一个二维数组来存储最长公共子序列的长度。 dp = [[0] * (n + 1) for _ in range(m + 1)] # 自底向上地构建dp数组 for i in range(1, m + 1): for j in range(1, n + 1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 # 若当前字符相同,则将这一对字符加入到LCS中 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # 否则,考虑不包括s1[i-1]或s2[j-1]的情况 # dp[m][n] 包含了s1[0..m-1]和s2[0..n-1]的最长公共子序列的长度 return dp[m][n] # 示例使用 s1 = "abcde" s2 = "ace" print("最长公共子序列的长度:", longest_common_subsequence(s1, s2))
动态规划逻辑:
- 初始化一个
(m+1) x (n+1)
的二维列表dp
,其中dp[i][j]
存储s1
前i
个字符和s2
前j
个字符的最长公共子序列的长度。 - 遍历字符串
s1
和s2
,通过比较字符确定是否将字符加入到子序列中,或者决定忽略哪个字符以保持子序列尽可能长。
复杂度分析
- 时间复杂度:O(mn) - 因为需要填充一个
m x n
的矩阵。 - 空间复杂度:O(mn) - 用于存储每对索引的LCS长度。可以优化到 O(min(m, n)) 通过只保留当前和前一行的状态。
算法图解
图解示例
假设我们有两个字符串:
s1 = "abcde"
s2 = "ace"
我们需要找到它们的最长公共子序列。
动态规划表初始化
创建一个表格,其中行表示字符串 s1
,列表示字符串 s2
。我们使用一个 (m+1) x (n+1)
的表格,m
和 n
分别是两个字符串的长度。
初始表格:
| - | a | c | e | ----------------------- - | 0 | 0 | 0 | 0 | a | 0 | | | | b | 0 | | | | c | 0 | | | | d | 0 | | | | e | 0 | | | |
填充表格
- 当我们比较两个字符且它们相同时,我们在左上角的值基础上加一(因为发现了一个新的公共元素)。
- 当字符不同时,我们取左边和上边值中的最大值。
1.a vs a:
dp[1][1]
:s1
的a
与s2
的a
相同,所以dp[1][1] = dp[0][0] + 1 = 1
.
| - | a | c | e | ----------------------- - | 0 | 0 | 0 | 0 | a | 0 | 1 | | | b | 0 | | | | c | 0 | | | | d | 0 | | | | e | 0 | | | |
2.a vs c, a vs e:
dp[1][2]
: 取dp[1][1]
和dp[0][2]
的最大值,即dp[1][2] = max(1, 0) = 1
.dp[1][3]
: 取dp[1][2]
和dp[0][3]
的最大值,即dp[1][3] = max(1, 0) = 1
.
3.b vs a, c, e:
- 使用与第一行相同的逻辑,
dp[2][1] = 1
,dp[2][2] = 1
,dp[2][3] = 1
.
4.c vs a, c, e:
dp[3][1] = 1
: 使用上一行的结果。dp[3][2] = 2
:c
与c
匹配,dp[3][2] = dp[2][1] + 1 = 2
.dp[3][3] = 2
: 取dp[3][2]
和dp[2][3]
的最大值,即dp[3][3] = max(2, 1) = 2
.
5.d vs a, c, e:
- 同上,结果与第三行相同。
6.e vs a, c, e:
dp[5][1] = 1
,dp[5][2] = 2
,dp[5][3] = 3
:e
与e
匹配,dp[5][3] = dp[4][2] + 1 = 3
.
| - | 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 |
8. 总结
最长公共子序列问题不仅是一个理论上的问题,而且在实际应用中非常重要,比如在生物信息学中比较DNA序列,在软件工程中比较文件版本等。通过动态规划,我们能够有效地解决这个问题,即使在面对大规模数据时也能保持良好的性能。
欢迎关注微信公众号 数据分析螺丝钉