公共子序列的解释,其他博客已经解释的很清楚了,求两个字符串的最长相同的字符组成形式,并且允许不连续,但是每个字符的顺序必须相同。比如 “我们爱吃苹果”和“我爱苹果”的最长子序列就是“我爱苹果”,可以看出结果顺序一致,且两个字符串中都存在这些字符。
一、动态规划的解决思路
假设两个字符串分别为str1、str2,从最后一位开始判断:
1、如果str1和str2的最后一位相等,则我们可以断定,公共字符串中肯定包含最后一位,所以此时最大公共字符串的长度应该是1加上str1和str2去掉最后一位后的最大公共字符串的长度;
2、如果str1和str2的最后一位不相等,则我们现在需要计算两个情况:第一,我们放弃str1的最后一位字符,求去掉最后一位字符的str1和完整的str2的最大公共子序列的长度;第二,我们放弃str2的最后一位字符,求去掉最后一位字符的str2和完整的str1的最大公共子序列的长度。求出上述两种情况的基础上,我们取其中的最大值。
我们可以看出上面的思想依然是选择或不选择,传统的递归可以实现的情况。
(1)递归的解决代码
# ---------递归解决-------
'''
如果两个字符串的最后一位相等,则结果为两个字符串去掉最后一位后的LCS加最后一位;
如果两个字符串的最后一位不相等,则结果为每一个字符串单独去掉最后一位和另一个完整字符串的LCS中的大值。
'''
def lcs_rec(str1, str2):
if len(str1) <= 0 or len(str2) <= 0:
return 0
elif str1[-1] == str2[-1]:
return lcs_rec(str1[:-1], str2[:-1]) + 1
else:
sol_A = lcs_rec(str1[:-1], str2)
sol_B = lcs_rec(str1, str2[:-1])
if sol_A > sol_B:
return sol_A
return sol_B
(2)循环解决
递归的解决方案中,存在许多重复的计算,效率较低,所以我们采用“空间换时间”的策略,提高效率。
我们定义一个二维矩阵,矩阵大小为[len(str1)+1, len(str2)+1],每一维的大小为什么要加上1呢,因为要把字符串长度为0的情况考虑进去,这是循环展开计算的基础。假设矩阵为OPT,其中每一位OPT[i][j]的含义,代表字符串长度分别为i和j时的最大公共子序列长度。首先存在初始化的情况,当i或j分别为0时,此时OPT中对应位的值都是0。
如下表所示,假设str1长度为4,str2长度为5
i / j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | |||||
2 | 0 | |||||
3 | 0 | |||||
4 | 0 |
表格中除i==0和j==0以外的每一个位置OPT[i][j],都可以采用上面的动态变化来求解。当前位置是否相等,相等的话,OPT[i][j]等于两个字符串分别去掉该位置字符的最长公共字符串长度加1(即OPT[i-1][j-1]+1);如果不相等,则计算OPT[i-1][j]和OPT[i][j-1]中的最大值。
循环的解决代码:
# ---------循环解法---------
'''
以空间换时间
'''
def lcs_dp(str1, str2):
import numpy as np
# opt 表示从前往后,字符串1前i位和字符串2前j位的最大公共子序列长度
# 初始i == 0 or j == 0时,opt为0
opt = np.zeros((len(str1)+1,len(str2)+1))
for i in range(1, len(str1)+1):
for j in range(1, len(str2)+1):
# 此处opt的长度为(len(str1)+1,len(str2)+1),但是字符串要从0开始计算
if str1[i-1] == str2[j-1]:
opt[i][j] = opt[i-1][j-1] + 1
else:
sol_A = opt[i-1][j]
sol_B = opt[i][j-1]
opt[i][j] = max(sol_A, sol_B)
return opt[len(str1)][len(str2)]
二、问题拓展
如果现在不是求最大公共子序列的长度,而是求最大公共连续子序列的长度。我们应该怎么考虑。动态转移方程
OPT代表到达i,j时,能够一直连续的字符的最长数目;如果存在str1[i]不等于str2[j],则该位置的连续数目归0 ,然后再从后面开始查找连续子序列。相当于是求解局部最优,然后最后通过max求全局最优。
直接写循环的代码:
#------------如果把问题改成,求最长连续公共字符串---
def continue_lcs_dp(str1, str2):
import numpy as np
# opt 表示从前往后,字符串1前i位和字符串2前j位的最大公共子序列长度
# 初始i == 0 or j == 0时,opt为0
opt = np.zeros((len(str1)+1,len(str2)+1))
for i in range(1, len(str1)+1):
for j in range(1, len(str2)+1):
# 此处opt的长度为(len(str1)+1,len(str2)+1),但是字符串要从0开始计算
if str1[i-1] == str2[j-1]:
opt[i][j] = opt[i-1][j-1] + 1
else:
opt[i][j] = 0
return np.max(opt)