求最长公共子序列

本文探讨了如何使用动态规划解决寻找两个字符串最长公共子序列的问题,并展示了该方法在求解最长回文子序列的应用。

题目:

给两个字符串a,b,找出他们两的最长公共子序列长度,和一个最长公共子序列。

比如:a = “ABCBDAB”, b = “BDCABA”;

则最大子序列长度是4,满足的子序列有B D A B。

思路:

注意是子序列,不是子串,子序列不要求连续。
动态规划。
dp[i][j]表示字符串a从0~i位置,字符串b从0~j位置,此时的最长公共子序列。

如果i位置与j位置对应的字符相同,那么当前位置的LCS的长度等于上一个位置的LCS长度+1,即为dp[i][j]=dp[i-1][j-1]+1;

如果i位置与j位置对应的字符不相同,可能会是i-1位置与j位置字符相同,也可能是i与j-1位置字符相同。那么当前位置的LCS的长度要么等于(i-1,j)位置的,要么等于(i,j-1)位置的。dp[i][j]=max(dp[i-1][j],dp[i][j-1]);

再用一个二维数组记录路径。
seq=”“;代表我们要找的子序列
path[n][m]
如果a[i]=b[j],那么path[i][j]此处对应的状态设为0,可以加入到seq中;
如果a[i]≠b[j],那么path[i][j]为1表示从(i-1,j)到(i,j),如果path[i][j]为-1表示从(i,j-1)到(i,j)

我们根据记录的路径状态递归回溯将相同的字符加入到seq中。

代码:

#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;

int findLongestCommonSubsequenceLength(string &a,string& b,vector<vector<int>> &path){
    int la = a.size();
    int lb = b.size();
    vector<vector<int>> dp(la+1,vector<int>(lb+1,0));
    for (int i = 1; i <= la; i++){
        for (int j = 1; j <= lb; j++){
            if (a[i-1] == b[j-1]){
                dp[i][j] = dp[i-1][j-1] + 1;
                path[i][j] = 0;
            }
            else{
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                path[i][j] = dp[i - 1][j] > dp[i][j - 1] ? 1 : -1;
            }
        }
    }
    return dp[la][lb];
}
void LongestCommonSubsesequence(int i,int j,string &seq,vector<vector<int>> &path,string &a){
    if (i < 1 || j < 1){
        return;
    }
    if (path[i][j] == 0){
        LongestCommonSubsesequence(i-1,j-1,seq,path,a);
        seq += a[i-1];
    }
    else if (path[i][j] == 1){
        LongestCommonSubsesequence(i-1,j,seq,path,a);
    }
    else if (path[i][j] == -1){
        LongestCommonSubsesequence(i,j-1,seq,path,a);
    }
}
int main()
{
    string a = "ABCBDAB";
    string b = "BDCABA";
    int la = a.size();
    int lb = b.size();
    vector<vector<int>> path(la + 1, vector<int>(lb + 1));
    printf("%d\n", findLongestCommonSubsequenceLength(a, b,path));
    string seq;
    LongestCommonSubsesequence(la,lb,seq,path,a);
    cout << seq;
    system("pause");
    return 0;
}

补充:
其实求最长回文子序列也可以用最长公共子序列啊,把字符串逆序后,与原字符串找它们之间的最长公共子序列长度,即为,原字符串的最长回文子序列

<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]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值