1、最长公共子序列LCS(longest common sequence)
1. 问题描述
字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。
公共子序列的定义:如果Z既是X的子序列,又是Y的子序列,则称Z为X和Y的公共子序列。
最长公共子序列LCS的定义:2个序列的子序列中长度最长的那个。
令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增的下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
样例输入:str1 = “ABCBDAB” ,str2 = "BDCABA"
样例输出:LCS长度:4,LCS序列:BCBA。
2. 算法分析
解决方案:动态规划。解决LCS问题,需要把原问题分解成若干个子问题,所以需要刻画LCS的特征。
考虑最长公共子序列问题如何分解成子问题,设X=“x0,x1,…,xm”,Y=“y0,y1,…,yn”,并Z=“z0,z1,…,zk”为它们的最长公共子序列。不难证明有以下性质:
(1) 如果xm=yn,则zk=xm=yn,且“z0,z1,…,zk-1”是“x0,x1,…,xm-1”和“y0,y1,…,yn-1”的一个最长公共子序列(Zk-1是Xm-1和Yn-1的一个LCS);
(2) 如果xm!=yn,则若zk!=xm,那么“z0,z1,…,zk”是“x0,x1,…,xm-1”和“y0,y1,…,yn”的一个最长公共子序列(Z是Xm-1和Y的一个LCS);
(3) 如果xm!=yn,则若zk!=bn,那么“z0,z1,…,zk”是“x0,x1,…,xm”和“y0,y1,…,yn-1”的一个最长公共子序列(Z是X和Yn-1的一个LCS)。
这样,在找X和Y的公共子序列时,如果xm=yn,则进一步解决一个子问题,找“x0,x1,…,xm-1”和“y0,y1,…,yn-1”的一个最长公共子序列;如果xm!=yn,则要解决两个子问题,找出“x0,x1,…,xm-1”和“y0,y1,…,yn”的一个最长公共子序列和找出“x0,x1,…,xm”和“y0,y1,…,yn-1”的一个最长公共子序列,再取两者中较长者作为X和Y的最长公共子序列。
引进一个二维数组dp[][],用dp[i][j]记录X[i]与Y[j]对应的前i个和前j个字符的LCS的长度,以决定搜索的方向。其中X={x1 ... xm},Y={y1...yn},Xi = {x1 ... xi},Yj={y1... yj}。
我们是自底向上进行递推计算,那么在计算dp[i,j]之前,dp[i-1][j-1],dp[i-1][j]与dp[i][j-1]均已计算出来。此时我们根据X[i]=Y[j]还是X[i]!=Y[j],就可以计算出dp[i][j]。
问题的递推公式:

用白话文解释下这个公式:
p1表示X的前 i-1 个字符和Y的前 j 个字符的LCS的长度,
p2表示X的前 i 个字符和Y的前 j-1 个字符的LCS的长度,
p表示X的前 i-1 个字符和Y的前 j-1 个字符的LCS的长度,
p0表示X的前 i 个字符和Y的前 j 个字符的LCS的长度。
如果X的第 i 个字符和Y的第 j 个字符相等,则p0=p+1,如果X的第 i 个字符和Y的第 j 个字符不相等,则p0=max(p1,p2)。
因此,我们只需要从dp[0][0]开始填表,填到dp[m-1][n-1],就可以得到的dp[m-1][n-1]就是LCS的长度。
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m+n)次就会遇到i=0或j=0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为o(m*n)。
// 最长公共子序列的长度
int longestCommonSubsequence(string str1, string str2) {
int m = str1.size();
int n = str2.size();
if (m==0 || n==0)
return 0;
vector<vector<int>> dp(m+1, vector<int>(n+1));
for (int i=0; i<=m; ++i) {
for (int j=0; j<=n; ++j) {
if (i==0 || j==0) {
dp[i][j] = 0;
} else if (str1[i-1] == str2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
3. 构造LCS
通常,两个序列的最长公共子序列不唯一,采用回溯的方法可以输出最长公共子序列LCS:
在构造dp[][]数组时,也可以使用一个二维数组b来行为序列:
在对应字符相等的时候,用↖标记
在p1 >= p2的时候,用↑标记
在p1 < p2的时候,用←标记

// 构造最长公共子序列的表
vector<vector<int>> longestCommonSubsequence(string str1, string str2) {
int m = str1.size();
int n = str2.size();
if (m==0 || n==0)
return 0;
vector<vector<int>> dp(m+1, vector<int>(n+1));
vector<vector<int>> B(m+1, vector<int>(n+1));
for (int i=0; i<=m; ++i) {
for (int j=0; j<=n; ++j) {
if (i==0 || j==0) {
dp[i][j] = 0;
B[i][j] = -2; // -2表示没有方向
} else if (str1[i-1] == str2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
B[i][j] = 0; // 0表示斜向左上
} else {
if (dp[i-1][j] >= dp[i][j-1]) {
dp[i][j] = dp[i-1][j];
B[i][j] = -1; // -1表示竖直向上
} else {
dp[i][j] = dp[i][j-1];
B[i][j] = 1; // 1表示横向左
}
}
}
}
return dp;
}
// 输出LCS
void outPutLCS(vector<vector<int>> B, string X, int str1_len,int str2_len) {
if (str1_len==0 || str2_len==0) {
return;
}
if (B[str1_len][str2_len] == 0) { //箭头左斜
outPutLCS(B, X, str1_len-1, str2_len-1);
printf("%c", X[str1_len-1]);
} else if (B[str1_len][str2_len] == -1) {
outPutLCS(B, X, str1_len-1, str2_len);
} else {
outPutLCS(B, X, str1_len, str2_len-1);
}
}

本文介绍了最长公共子序列(LCS)算法,包括问题描述、动态规划解决方案和构造LCS的方法。通过二维数组dp[][]进行动态规划计算,最终求得LCS的长度。此外,还提到了最长上升子序列(LIS)的概念。

7240

被折叠的 条评论
为什么被折叠?



