动态规划——最长公共子序列
设计与实现最长公共子序列问题的算法;
解决思路
首先需要明确子序列的概念,子序列与子串并不是一种东西,子序列我们可以理解为在一个序列中删除任意多个字符,剩余字符按原有顺序组成的序列就是原序列的子序列,其中删除的字符可以使连续的也可以是不连续的,对于任意一个字符来说,有删除与不删除两种选择,那么对于一个长为n的序列,他就有2^n个子序列。
我们想求两个序列的最长公共子序列,最简单的方法就是生成一个序列的所有子序列,然后检查这些子序列是否是另一个序列的子序列,并找到最长的公共子序列。这个方法其中的难点在于如何生成一个序列的所有子序列,方法一时利用二进制掩码来决定序列的某一位是否应该删除,然而这种方法涉及到了移位运算,int型变量有32位,因此当序列长度超过32时就会出错,即使换成long也只有64位,因此这种方法局限性很大。
另一种可以产生所有子序列的方法就是深度优先搜索,我们可以将母序列想象成一个迷宫,每一个字符都是迷宫的路径,而选择留下该字符和不留下该字符就是路径的两条分叉路,而已经遍历到了母序列的终点就是迷宫的死路,当已经遍历到死路时就回溯到上一层就,直至回到起点就可以得到所有子序列。这样的方法虽然可以克服掩码的确定,然而总共有2^n个子序列,因此无论子序列与另一个序列的比较过程有多简洁,它的时间复杂度也是指数级的,因此该方法我们了解了就好,不应当在我们的考虑范围内。
那么如何使用动态规划来解决这个问题呢?我们记两个序列分别为X={x1,x2,x3……xm}、Y={y1,y2,y3……yn},他们的一个公共子序列为Z={z1,z2,z3……zk},Z中的所有元素必定是XY共有的, 但是位置是不确定的,我们可以由此得到如下结论:
①若xm = yn,则zk = xm = yn,那么Zk-1为Xm-1和Yn-1的公共子序列
②若xm != yn,且zk != xm,那么Z是Xm-1和Y的公共子序列
③若xm != yn,且zk != yn,那么Z是X和Yn-1的公共子序列
根据以上结论,我们可以用c[i][j]来表示X的前i个字符与Y的前j个字符的最长公共子序列的长度,我们可以得到以下递推公式:
但是这样我们只能得到最长公共子序列的长度,如果我们希望得到最长公共子序列,我们还需要增加一个追踪函数b[i][j],递推公式和C[i][j]类似,如下:
当我们得到最长公共子序列的长度后,就可以根据箭头往回追踪得到最长公共子序列
算法描述
算法一、LCS_dp
输入:序列str1={x1,x2,x3……xm},序列str2={y1,y2,y3……yn}
①for i=0 to m c[i][0]=0
②for j=0 to n c[0][j]=0
③for i = 1 to m
④ for j = 1 to n
⑤ if str1[i-1]==str[j-1] then c[i][j] = c[i-1][j-1]+1,b[i][j]=↖
⑥ else if c[i-1][j] >= c[i][j-1] then c[i][j] = c[i-1][j],b[i][j]=↑
⑦ else c[i][j] = c[i][j-1],b[i][j]=←
⑧return c[m][n]
算法二、return_res(用于返回最长子序列)
输入:序列str1={x1,x2,x3……xm},序列str2={y1,y2,y3……yn},追踪矩阵b[n][m]
①i = m, j = n, res=””
②while i>0 && j>0 do
③ if str1[i-1] == str2[j-1] then res.insert(0,str[i-1])
④ if b[i][j] == ↖ then i–,j–
⑤ else if b[i][j] == ↑ then i–
⑥ else if b[i][j] = ← then j—
⑦return res
算法代码
求最长公共子序列长度,填充追踪矩阵的代码:
int LCS_dp(int n1, int n2)
{
for (int i = 0; i <= n1; i++)c[i][0] = 0;
for (int j = 0; j <= n2; j++)c[0][j] = 0;
for (int i = 1; i <= n1; i++)
{
for (int j = 1; j <= n2; j++)
{
if (str1[i-1] == str2[j-1])
{
c[i][j] = c[i - 1][j - 1] + 1;
b[i][j] = 'c';
}
else if (c[i - 1][j] >= c[i][j - 1])
{
c[i][j] = c[i - 1][j];
b[i][j] = 'u';
}
else
{
c[i][j] = c[i][j - 1];
b[i][j] = 'l';
}
}
}
return c[n1][n2];
}
得到最长公共子序列的代码:
string return_res(int n1, int n2)
{
int i = n1, j = n2;
string res = "";
while (i > 0 && j > 0)
{
if (str1[i - 1] == str2[j - 1])
{
string temp = "";
temp += str1[i - 1];
res.insert(0, temp);
}
if (b[i][j] == 'c')i--, j--;
else if (b[i][j] == 'u')i--;
else if (b[i][j] == 'l')j--;
}
return res;
}
算法分析
动态规划算法嵌套了两层循环,外层循环次数取决于序列1的长度n1,而内层循环的次数取决于序列2的长度n2,因此算法的时间复杂度为O(n1*n2)