求最长公共子序列

昨天面试遇到了让我们写最长公共子序列的问题,当时能想到的方法是记录每一个子序列的长度,然后输出长度最大的那个子序列,这个当然不是最优的方法,因为时间复杂度为O(2^m2^n),m,n为两个子串的长度。回来翻看了下资料,该类问题的最佳解决方法是动态规划法,因此总结如下:

1.什么事最长公共子序列问题

什么是最长公共子序列呢?举个简单的例子吧,一个数列S,若分别是两个或多个已知序列的子序列,且是所有符合条件序列中最长的,则S称为已知序列的最长公共子序列

当然他又区别于最长公共子串

最长公共子串(Longest Common Substirng)和最长公共子序列(Longest Common Subsequence,LCS)的区别为:子串是串的一个连续的部分,子序列则是从不改变序列的顺序,而从序列中去掉任意的元素而获得新的序列;也就是说,子串中字符的位置必须是连续的,子序列则可以不必连续。

2.动态规划法求最长公共子序列

1、序列str1和序列str2
  ·长度分别为m和n;
  ·创建1个二维数组L[m.n];
    ·初始化L数组内容为0
    ·m和n分别从0开始,m++,n++循环:
       - 如果str1[m] == str2[n],则L[m,n] = L[m - 1, n -1] + 1;
       - 如果str1[m] != str2[n],则L[m,n] = max{L[m,n - 1],L[m - 1, n]}
    ·最后从L[m,n]中的数字一定是最大的,且这个数字就是最长公共子序列的长度
    ·从数组L中找出一个最长的公共子序列
2、从数组L中查找一个最长的公共子序列

   i和j分别从m,n开始,递减循环直到i = 0,j = 0。其中,m和n分别为两个串的长度。
  ·如果str1[i] == str2[j],则将str[i]字符插入到子序列内,i--,j--;
  ·如果str1[i] != str[j],则比较L[i,j-1]与L[i-1,j],L[i,j-1]大,则j--,否则i--;(如果相等,则任选一个)


<think>我们使用回溯法(递归)来实现最长公共子序列(LCS)的解。但是需要注意的是,回溯法通常在没有动态规划高效,因为会重复计算子问题。然而,为了理解回溯法的思路,我们可以实现一个递归回溯的版本。回溯法思路:设两个序列分别为`X[0..m-1]`和`Y[0..n-1]`,我们从两个序列的末尾开始比较:1.如果当前字符相同(`X[m-1]==Y[n-1]`),则这个字符一定是最长公共子序列的一部分,我们将其加入结果,然后递归解剩余部分(`X[0..m-2]`和`Y[0..n-2]`)。2.如果当前字符不同,则有两种选择:舍弃`X`的最后一个字符,递归解`X[0..m-2]`和`Y[0..n-1]`;或者舍弃`Y`的最后一个字符,递归解`X[0..m-1]`和`Y[0..n-2]`。我们取这两种选择中结果更长的那个。但是,直接递归会导致大量的重复计算,因此我们可以使用一个二维数组来存储已经计算过的子问题的结果(即记忆化搜索),这样就是动态规划了。但题目要回溯法,所以我们先实现一个简单的递归版本(没有记忆化),然后我们可以添加记忆化来改进。由于最长公共子序列可能有多个,我们这里先最长公共子序列的长度,然后再通过回溯过程来构造一个最长公共子序列。然而,由于题目要回溯法,我们也可以直接通过回溯法来输出一个最长公共子序列(但注意,如果要所有最长公共子序列,则需要记录所有路径,这里我们只输出一个)。由于纯递归回溯在序列长度较大时效率很低,我们这里仅用于演示。步骤:1.递归函数定义:`lcs_backtrack(X,m,Y,n)`,返回`X[0..m-1]`和`Y[0..n-1]`的最长公共子序列的长度。2.递归终止条件:如果m或n为0,则返回0。3.递归过程:如果`X[m-1]==Y[n-1]`,则返回`1+lcs_backtrack(X,m-1,Y,n-1)`否则,返回`max(lcs_backtrack(X,m-1,Y,n),lcs_backtrack(X,m,Y,n-1))`但是,这个递归会重复计算很多子问题,所以我们考虑使用记忆化(即用一个二维数组存储结果),但这样实际上就变成了动态规划。如果我们不使用记忆化,直接递归,那么当序列长度稍大(比如超过20)时就会非常慢。为了同时满足回溯法的要(避免动态规划)并且能够输出一个最长公共子序列(而不仅仅是长度),我们可以在递归过程中记录路径。然而,题目要的是示例代码,我们这里先实现一个长度的递归版本(无记忆化),然后再实现一个带记忆化的版本(动态规划思想),最后再通过回溯法(这里指通过动态规划表回溯构建子序列,这是动态规划的标准后续步骤)来输出一个最长公共子序列。但是用户要回溯法实现,所以我们要区分:严格回溯法(递归+深度优先搜索)解LCS,我们可以这样:-从两个序列的起始位置开始,逐个字符比较。-当两个字符相等,则将其加入当前公共子序列,然后递归处理剩余部分。-当两个字符不相等,则有两种选择:跳过X的当前字符,或者跳过Y的当前字符,然后递归处理。-在递归过程中,我们维护当前已经找到的公共子序列,并记录最长的一个。但是这种方法的时间复杂度是指数级的(O(2^(m+n))),所以只能用于非常小的序列。考虑到实用性,我们通常使用动态规划。但用户要回溯法,我们给出一个简单回溯法(递归)长度的代码,以及一个一个最长公共子序列的代码(通过记录路径)。由于回溯法效率问题,我们这里只给出小规模序列的解法。实现方案1:仅长度(无记忆化,递归回溯)实现方案2:一个最长公共子序列(通过递归过程中记录路径,但同样效率低)我们这里先实现方案1:递归长度(简单,但效率低)然后,我们实现一个带记忆化的递归(即动态规划的自顶向下版本),并输出长度。然后,我们通过动态规划表回溯来输出一个最长公共子序列(这是动态规划的标准方法,不是纯回溯法)。但是用户要回溯法,所以我们先实现一个纯回溯(递归)长度的版本,然后再实现一个纯回溯一个最长公共子序列的版本(不通过动态规划表,而是通过递归路径记录)。由于一个最长公共子序列需要记录路径,我们在递归时传递一个当前公共子序列的字符串,当递归到末尾时更新最长公共子序列。但是,这样在递归过程中会传递字符串,每次都会复制,效率更低,所以只适用于非常小的字符串。因此,我们给出两种代码:1.纯递归回溯最长公共子序列的长度(不记录序列)2.纯递归回溯一个最长公共子序列(记录序列,效率极低,仅用于演示)我们先写第一种:长度(无记忆化)代码(Python):```pythondeflcs_length_backtrack(X,Y,m,n):ifm==0orn==0:return0ifX[m-1]==Y[n-1]:return1+lcs_length_backtrack(X,Y,m-1,n-1)else:returnmax(lcs_length_backtrack(X,Y,m-1,n),lcs_length_backtrack(X,Y,m,n-1))#示例使用X="ABCBDAB"Y="BDCABA"print("LengthofLCSis",lcs_length_backtrack(X,Y,len(X),len(Y)))```但是,这个递归调用树会很大,重复计算多,所以我们可以用第二种:带记忆化的递归(动态规划自顶向下)来长度,这可以避免重复计算:```pythondeflcs_length_memo(X,Y,m,n,memo):ifm==0orn==0:return0ifmemo[m][n]!=-1:returnmemo[m][n]ifX[m-1]==Y[n-1]:memo[m][n]=1+lcs_length_memo(X,Y,m-1,n-1,memo)else:memo[m][n]=max(lcs_length_memo(X,Y,m-1,n,memo),lcs_length_memo(X,Y,m,n-1,memo))returnmemo[m][n]#初始化memo为一个二维数组,大小为(m+1)x(n+1),初始值-1X="ABCBDAB"Y="BDCABA"m=len(X)n=len(Y)memo=[[-1]*(n+1)for_inrange(m+1)]print("LengthofLCS(memo)is",lcs_length_memo(X,Y,m,n,memo))```接下来,我们使用回溯法(这里指通过动态规划表回溯)来输出一个最长公共子序列。注意,这个回溯不是递归回溯,而是基于动态规划表的回溯,通常称为“构造最优解”。但是用户要回溯法(递归)实现,所以我们还是用纯递归回溯来输出一个最长公共子序列(而不使用动态规划表)。我们通过递归,在每一步选择中,当两个字符相等时,将其加入当前序列;当两个字符不等时,我们分别尝试两种选择(跳过X当前字符或跳过Y当前字符),然后取更长的那个路径。然而,为了输出一个最长公共子序列,我们需要在递归过程中传递当前的公共子序列,并在递归结束时更新全局的最长公共子序列。由于我们只要输出一个,所以我们可以在递归函数中返回当前子问题的最长公共子序列(字符串),但这样每次返回字符串会带来大量的字符串拼接开销。我们来实现一个纯递归回溯一个最长公共子序列的函数(返回字符串):```pythondeflcs_string_backtrack(X,Y,m,n):ifm==0orn==0:return""ifX[m-1]==Y[n-1]:returnlcs_string_backtrack(X,Y,m-1,n-1)+X[m-1]else:lcs1=lcs_string_backtrack(X,Y,m-1,n)lcs2=lcs_string_backtrack(X,Y,m,n-1)iflen(lcs1)>len(lcs2):returnlcs1else:returnlcs2#示例使用X="ABCBDAB"Y="BDCABA"print("OneoftheLCS:",lcs_string_backtrack(X,Y,len(X),len(Y)))```但是,这个递归同样存在大量重复计算,并且字符串拼接效率低。所以,只适用于很小的字符串(比如长度不超过10)。为了效率,实际应用中我们使用动态规划(自底向上)来长度,然后通过动态规划表回溯来构造一个最长公共子序列。这里也给出这个标准方法作为对比:动态规划长度(自底向上):```pythondeflcs_length_dp(X,Y):m=len(X)n=len(Y)dp=[[0]*(n+1)for_inrange(m+1)]foriinrange(1,m+1):forjinrange(1,n+1):ifX[i-1]==Y[j-1]:dp[i][j]=dp[i-1][j-1]+1else:dp[i][j]=max(dp[i-1][j],dp[i][j-1])returndp#回溯构造一个最长公共子序列defbuild_lcs_from_dp(X,Y,dp):i=len(X)j=len(Y)lcs=[]whilei>0andj>0:ifX[i-1]==Y[j-1]:lcs.append(X[i-1])i-=1j-=1elifdp[i-1][j]>dp[i][j-1]:i-=1else:j-=1return''.join(reversed(lcs))#示例X="ABCBDAB"Y="BDCABA"dp=lcs_length_dp(X,Y)lcs=build_lcs_from_dp(X,Y,dp)print("LengthbyDP:",dp[len(X)][len(Y)])print("OneLCSbyDP:",lcs)```总结:用户要回溯法,所以我们主要展示纯递归回溯的版本(长度和一个序列),但需要提醒用户该方法效率低,仅适用于小规模数据。对于大规模数据,动态规划是更好的选择。因此,我们提供以下示例代码:1.纯递归回溯LCS长度2.纯递归回溯一个LCS字符串3.(附加)动态规划LCS长度和构造LCS(供对比参考)在回答中,我们将展示这些代码,并说明优缺点。注意:用户要使用中文回答。最后,生成相关问题。</think>### 回溯法最长公共子序列(LCS) 回溯法通过深度优先搜索所有可能的子序列组合来最长公共子序列。以下是实现步骤和示例代码: #### 算法步骤 1. **初始化**:设置全局变量存储最长公共子序列及其长度 2. **回溯函数**: - 若达到任一序列末尾,更新最长子序列记录 - 若当前字符匹配,将其加入候选序列并递归处理后续字符 - 若不匹配,分别尝试跳过 `X` 或 `Y` 的当前字符 3. **结果输出**:回溯结束后返回找到的最长子序列 #### 示例代码(Python) ```python def backtrack_lcs(X, Y): max_lcs = [] # 存储最长公共子序列 max_len = 0 # 最长子序列长度 def dfs(i, j, cur_lcs): nonlocal max_lcs, max_len # 到达序列末尾 if i == len(X) or j == len(Y): if len(cur_lcs) > max_len: max_lcs = cur_lcs[:] max_len = len(cur_lcs) return # 当前字符匹配 if X[i] == Y[j]: cur_lcs.append(X[i]) dfs(i+1, j+1, cur_lcs) # 处理下一位置 cur_lcs.pop() # 回溯 else: # 跳过X的当前字符 dfs(i+1, j, cur_lcs) # 跳过Y的当前字符 dfs(i, j+1, cur_lcs) dfs(0, 0, []) return ''.join(max_lcs) # 示例使用 X = "ABCBDAB" Y = "BDCABA" print("最长公共子序列:", backtrack_lcs(X, Y)) # 输出: BCBA 或 BDAB ``` #### 算法分析 - **时间复杂度**:$O(2^{m+n})$,其中 $m,n$ 为序列长度(最坏情况) - **空间复杂度**:$O(m+n)$(递归栈深度) - **适用场景**:小规模序列($n<20$),动态规划更高效[^1][^2] #### 动态规划对比 回溯法会重复计算子问题[^3],而动态规划通过二维表存储中间结果: $$dp[i][j] = \begin{cases} 0 & \text{if } i=0 \text{ or } j=0 \\ dp[i-1][j-1] + 1 & \text{if } X_i = Y_j \\ \max(dp[i-1][j], dp[i][j-1]) & \text{otherwise} \end{cases}$$ 动态规划时间复杂度为 $O(mn)$,推荐用于实际应用[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值