📅  最后修改于: 2023-12-03 15:27:25.883000             🧑  作者: Mango
本文将介绍动态编程中的一个经典问题:最长公共子序列问题(Longest Common Subsequence,简称LCS)。该问题被广泛应用于文本处理、字符串匹配等领域。本文将介绍最长公共子序列问题的定义、朴素算法、优化算法,以及代码实现。
在介绍问题之前,先来回忆一下“子序列”和“子串”的概念。一个字符串的子串是指该字符串中任意连续的一段字符组成的字符串;而一个字符串的子序列则是指从该字符串中任意选择出若干个字符,且这些字符所在顺序与原字符串相同(例如,“abcdef”的一个子序列可以是“adf”)。我们可以将LCS问题表述为:给定两个字符串S和T,求它们的最长公共子序列的长度。
很显然,这是一个典型的搜索问题。我们可以枚举所有S和T的子序列,然后在这些子序列中找到最长的公共子序列。但是,这样的复杂度是指数级别的,难以接受。考虑到子序列的自相似性,我们可以尝试利用动态规划来解决该问题。
我们定义一个二维数组dp[i][j],其中dp[i][j]表示S[1...i]和T[1...j]的最长公共子序列长度。这个数组的基本思想是,用类似矩形分割的方式不断拆分子问题,直到整个问题都被拆分成最基本的子问题。对于长度为0或者S/T只有一个字符的情况,我们都可以直接把dp[i][j]赋为0。
考虑dp[i][j]是如何由dp[i-1][j-1], dp[i-1][j], dp[i][j-1]这三个子问题推导而来的。如果S[i]=T[j],那么S[1...i]和T[1...j]的最长公共子序列就是S[1...i-1]和T[1...j-1]的最长公共子序列再加上S[i](即S[i]和T[j]这个字符)。如果S[i]!=T[j],那么S[1...i]和T[1...j]的最长公共子序列就是S[1...i-1]和T[1...j]的最长公共子序列,或者S[1...i]和T[1...j-1]的最长公共子序列中更长的那一个。
最终的答案就是dp[m][n],其中m和n分别是S和T的长度。时间复杂度是O(mn),空间复杂度也是O(mn)。
我们可以看到,朴素算法的空间复杂度已经达到了O(mn),不算太优秀。实际上,在动态规划的过程中,我们往往只需要维护一行或者一列的状态信息。具体地,我们可以用两个数组dp1和dp2来轮流更新,其中dp1[i]表示S[1...i]和T[1...j]的最长公共子序列长度,而dp2[i]则表示S[1...i-1]和T[1...j]的最长公共子序列长度。在每次更新dp1之前,我们将dp1的状态传递给dp2。这样,在动态规划过程中,我们一直只需要维护两个长度为n的一维数组,而不是一个二维数组。这样,我们的空间复杂度就被降低到了O(n)。
除了空间的优化外,我们还可以对状态转移方程进行一定的优化。具体来说,我们在代码实现中通过交换S和T,保持T的长度更小,来达到时间复杂度更小的效果。考虑到j的循环是在1到n之间的,因此即便T比较长,时间复杂度并不会被影响。
下面是用Python实现的LCS问题代码。
def lcs(s1, s2):
m, n = len(s1), len(s2)
if m < n:
return lcs(s2, s1)
dp1 = [0] * (n + 1)
dp2 = [0] * (n + 1)
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp1[j] = dp2[j - 1] + 1
else:
dp1[j] = max(dp2[j], dp1[j - 1])
dp1, dp2 = dp2, dp1
return dp2[n]
我们可以将这段代码放在Markdown格式的文本中,以供其他程序员参考。
```python
def lcs(s1, s2):
m, n = len(s1), len(s2)
if m < n:
return lcs(s2, s1)
dp1 = [0] * (n + 1)
dp2 = [0] * (n + 1)
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp1[j] = dp2[j - 1] + 1
else:
dp1[j] = max(dp2[j], dp1[j - 1])
dp1, dp2 = dp2, dp1
return dp2[n]