算法导论(十五)--动态规划,最长公共子序列

算法导论(十五)--动态规划,最长公共子序列

最长公共子序列(LCS)

给定两个序列x[1,…,m]和y[1,…,n],找出它们的一个最长公共子序列(LCS)(可能不唯一)。
例如:
x: A B C B D A B
y: B D C A B A
先肉眼看看它们的LCS是什么,是BDAB,BCAB和BCBA。
这个问题可以用穷举法(Brute-Force)来解决:检查x中的所有子序列,看看y里是否有一样的子序列。
分析一下这种方法:如果已知x的一个子序列,需要花费多少时间来判断它是不是y的子序列呢?就是y的长度O(n)。把y扫描一遍,如果发现一样的字符,就按x和y的下标开始递归,看剩下的有没有一样的。x有多少子序列呢?有2m个,因为x的每一个字符可以选或者不选,一共有m个字符。因此复杂度是O(n·2m),是指数级的,非常的慢。

简化:
1.Look at the length of LCS(x,y)
2.Extend the all to find LCS itself
方法:
考虑x和y的前缀以及如何用前缀描述LCS。定义c[i, j]是x[1, i]和y[1, j]的LCS,c[i, j]=|LCS(x[1, i],y[1, j])|,如果我们知道了c[i, j],那么c[m, n]就是要求的答案,c[m, n]=|LCS(x,y)|。下面就是找出c[i, j]的归纳式。

c [ i , j ] = { c [ i − 1 , j − 1 ] + 1 如 果 x [ i ] = y [ j ] m a x { c [ i − 1 , j ] , c [ i , j − 1 ] } 其 他 c[i, j] = \begin{cases} c[i-1, j-1]+1 & 如果x[i]=y[j] \\ max\{c[i-1 ,j], c[i, j-1]\} & 其他 \end{cases} c[i,j]={c[i1,j1]+1max{c[i1,j],c[i,j1]}x[i]=y[j]

也就是说,给定c[i, j],它的值是由两个严格小于它的值决定的。
就不证明了,可以自己举例。

动态规划的特征之一就是最优子结构,即问题的一个最优解包含了子问题的最优解。例如,如果z=LCS(x, y),那么任何z的前缀都是x的某个前缀和y的某个前缀的LCS。如果子问题的解不是最优的,那么总是可以用剪贴法找到一个全局最优解。

LCS问题的算法

1. 递归法
伪代码:

LCS(x,y,i,j)
if(x[i]==y[j])
	then c[i,j]<-LCS(x,y,i-1,j-1)+1
else
	c[i,j]<-max{LCS(x,y,i-1,j),LCS(x,y,i,j-1)}
return c[i,j]

分析:
这个程序的最坏情况是什么?对于所有的i,j,x[i]≠y[j]。画一下递归树,假设m=7,n=6:
在这里插入图片描述
这棵树的高度是m+n,是指数阶(2m+n),还是很慢。可以观察到这棵树的很多项都是重复的,整个子树都是一样的,说明要解决的是相同的子问题。
动态规划的第二个特征就是重叠子问题,即一个递归的过程中,少数独立的子问题被反复计算了很多次。LCS问题的子问题空间,也就是独立子问题的个数,是m*n。

2. 自顶向下的备忘法:
在计算完每个子问题的时候记录一下,那么下次需要这个值的时候就不用再算了。
伪代码:

//如果c[i,j]没有计算过,就计算,否则就返回
LCS(x,y,i,j)
if(c[i,j]==nil)
	then if(x[i]==y[j])
			then c[i,j]<-LCS(x,y,i-1,j-1)+1
		else
			c[i,j]<-max{LCS(x,y,i-1,j),LCS(x,y,i,j-1)}
return c[i,j]

分析:
这个算法花费的时间是O(mn)(平摊分析),花费的存储空间也是O(mn)。

3. 自底向上的计算表格法:
列出表格,根据公式计算:

字符0ABCBDAB
000000000
B00111111
D00111222
C00122222
A01122233
B01223334
A01223344

分析:
计算这个表的时间是O(m*n)。这个表可以通过回溯法重建LCS,就是从右下角的4出发,4是由一次选择产生的,选择左边的4或者上面的4,假设是选择左边的4产生的,我们向左走,这个4是根据左上角的3加1产生的,所以我们往左上角走,并标记出现在这个4位置上的字符A,以此类推,上图红色标出的就是我们的回溯路径。这只是LCS之一,如果在两个相同的数里选最大的,就会有不同的选择。

*实际上,用O(min(m,n))的空间就能解决问题,因为假如我们看行,只要算出了一行,那么上一行的数就没有用了。

代码:

void LCS(int* x,int m,int* y,int n)
{
    int **c=new int*[m+1];
    for(int i=0;i<=m;i++)
        c[i]=new int[n+1];
    for(int i=0;i<=m;i++)
        for(int j=0;j<=n;j++)
            c[i][j]=0;
    std::vector<int> v;
    for(int i=0;i<m;i++)
        for(int j=0;j<n;j++)
        {
            if(x[i]==y[j])
            {
                c[i+1][j+1]=c[i][j]+1;
                v.push_back(x[i]);
            }
            else if(c[i][j+1]>c[i+1][j])
                c[i+1][j+1]=c[i][j+1];
            else
                c[i+1][j+1]=c[i+1][j];
                
        }
    cout<<c[m][n]<<endl;
    for(int i=0;i<v.size();i++)
        cout<<v[i]<<" ";
    for(int i=0;i<=m;i++)
    {
        delete[] c[i];
        c[i]=NULL;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值