动态规划入门(求最长公共子序列)

        

        最长公共子序列是动态规划中的一道经典题目了,今天学习了一下。记录下其求解过程,并加深一下对动态规划的了解。


        题目:给定两个序列X 和 Y,称序列Z是X和Y的公共子序列,如果Z即使X的一个子序列,又是Y的一个子序列。在最长子序列问题中,给定两个序列X和Y,希望找出X和Y的最长公共子序列。这就是常见LCS问题。


        Example:

        X =  A B C B D A B        Y = B D C A B A    我们可以用肉眼看出来 X 和 Y 的最长公共子序列为 B C A B 和  B D A B, 答案不唯一。

 

        问题:对于 序列X [ 1,2.....m], 序列 Y[ 1,2 .....n],找出序列X和Y的最长公共子序列。


        如何解决这个问题呢。我们首先会想到用一种蛮力的方法:

       方法1:

        我们首先枚举出来X的所有子序列,然后逐一检查 是否为Y的子序列,并且随时记录发现的最长子序列。我们称它为穷举法

        对于这种解决问题的方法,我们先确定下它的时间复杂度大概是都少:


        1.首先求出X总共有多少个子序列,我们可以把  X[ 1, 2 .....m]序列,当做一个二进制的一个位串。每个位上可以选择1或者0,选择1表示该位被选中,选择0表示该位没有被选中,则 X的一个子序列就分别对应 一种情况,而 X 总共有m个位,每个位置上有2个选择,1 或者0,So 总共有 2^m个子序列。

        2.对应X的每个子序列,如何判断是否在Y中存在,这个很简单我们只需要在Y中扫描一遍就可以判断了,所以时间复杂度为o(n)

        所以这个穷举法对应的时间复杂度为 o( n * 2^m ) 是 指数级时间复杂度,我们称这种方法的时间复杂度为 龟速。这显然 不是我们想要的方法。



      方法2:(Dynamic programming)


        我们需要简化和扩展上面的算法。先来定义几个符号:

        定义|S|为序列S 的长度,  定义 C[ i, j ]为 | LCS ( X[ 1,2 ...i], Y[ 1,2 ...j] )| ( 序列X[1,2...i] 和序列Y[1,2...j]的最长公共子序列的长度),  则 C[ m, n ] 就是序列X 和 Y的最长公共子序列长度。即C[ m, n] = | LCS ( X, Y ) |


        先来推倒出来一个公式,然后用剪切法加以证明:

 c[ i, j] =  c[ i - 1, j - 1] + 1                if x[ i ] == y[ j ];
          =  max( c[ i - 1, j ] c[ i, j - 1 ])   other wise
        证明:

        case1: 在 x[ i ] == y[ j ] 的情况下, 假设 z[ 1,2 ....k ] 是 X 和 Y 的最长公共子序列, 那么我们可以推出 z[ k ] = x[ i ] = y[ j ], 且z[ 1,2....k - 1]是 Xm-1 和 Yn-1的一个 LCS ,且c[ i, j ] = k。如下图所示:


        反证法: 如果 序列z 不包含X[i],所以我们可以添加X[i] 到z[ 1,2 ...k ]中(因为 x[ i ] = y[ j ] ,Match),所以序列z中一定包含 x[i],同理我们还可以推出 z[ 1,2 ... k -1 ] 是 LCS( x[ 1,2 ...i - 1], y[ 1,2.... j - 1]),这个也可以用反证法来证明,假设有个 序列w 是更长的公共子序列 (LCS( x[ 1,2 ...i - 1], y[ 1,2.... j - 1])) 则| w | > k - 1,然后我们同理可以在w 后面 加上x[ i ],所以它可以变成一个更长的LCS ,其值大于 k,所以 得到与上文矛盾,则可以证明一个c[ i - 1, j - 1] = k - 1   ==> c[ i , j ] = c[ i - 1, j - 1] + 1;  

       case 1的 推论得到了证明,我们同理可以证明case 2的推论。


       case2: 如果z[ k ] 不等于 x[ i ] ,那么z是 x[ 1,2 ...i - 1] 和y[ 1,2 ...j]的一个 LCS,该问题用反证法就可以证明了。 如果 x[ 1,2 ...j - 1] 和y 有一个更长LCS W,则W 也肯定是x[ 1,2, ...i ] 和y[1,2...j]的一个LCS,与当前推论矛盾。如果z[ k ] 不等于y[ j ],在z是x[1,2... i ] 和 y[ 1,2 ...j - 1]的一个LCS,同上可证明。


       动态规划的两个特征:

       1.最优子结构信息。 两个序列的一个LCS包含两个序列的前缀的一个LCS,所以说明本问题具有最优子结构信息。为了找出X 和Y的一个LCS,可能根据需要找出X 和Yn-1 的一个LCS以及Xm-1 和Y的一个LCS,这两个问题又同时包含Xm-1 和Yn-1的LCS 这个子问题,我们会从3个子问题中寻找一个最优的子问题解用到X和Y的一个LCS上。根据关系我们建立一个递归式:

          =  c[ i - 1, j - 1] + 1              if x[ i ] == y[ j ] && i > 0 && y > 0
c[ i, j]  =  max( c[ i - 1, j ] c[ i, j - 1 ]) if x[ i ] != y[ j ] && i > 0 && j > 0
          =  0                                 if x = 0 || y == 0
          寻找问题最优解的时候 需要在3个子问题中做选择,其余2个会被排除掉。


      我们可以根据这个表达式轻易的写出一个指数时间的递归算法,我们可以看出来 当x[ i] != y[ j ]的情况下比较糟糕,我们会需要计算两个子问题的解。 下面用一张图来模拟下递归算法的运行,只模拟x[i] != y[j]的情况,看递归树是如何运行的。假设m = 7, n = 6.


        树的高度是m+n(看最右边的分支即可看出来) ,所以我们直接递归的话时间复杂度会是2^(m+ n),还是龟速。

        但是我们可以在树中看到,做了很多重复的计算和工作,比如第三排的 6,5 第四排的6,4  ,如果采用递归的方法的话,这些都是要重复计算的。我们有个经验就是不要做重复的计算。下面就是动态规划的第二个特点了



       动态规划的第二个特点:重叠子问题

       一个递归的方法会包含了很少数量的独立子问题,其余的都是在重复的计算。而对于Xm和Yn 这两个序列来说,其子问题的空间为o(mn)。

       我们可以采取做备忘录的方法来解决这个重复计算的问题。

       伪代码:

    定义c[m][n],并且初始化,然后在求c[i][j]的时候
    if c[ i ][ j ] == nil
        then compute it 根据我们上面的那个公式
    else
        return c[ i ][ j ];

          可以得出这个的时间复杂度为o(mn),空间复杂度也为o(mn).根据动态规划来自底向上计算。



       例题:点击打开链接 POJ 1458

       代码:

//
//  main.cpp
//  LCS
//
//  Created by mini on 12/23/13.
//  Copyright (c) 2013 mini. All rights reserved.
//


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

#define SIZE 999

int dp[ SIZE ][ SIZE ] = { 0 };
int Max( int & x, int & y ){
    return x > y ? x : y;
}
int main(int argc, const char * argv[])
{
    int i, j;
    string str1,str2;;
    size_t len1, len2;
    while( cin >> str1 >> str2 ){
        memset( dp, 0, sizeof( dp ) );
        len1 = str1.length();
        len2 = str2.length();
        for ( i = 1; i <= len1; i++ ) {
            for ( j = 1; j <= len2; j++ ) {
                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 ] );
                }
            }
        }
        cout << dp[ len1 ][ len2 ] << endl;
    }
    return 0;
    
}


        如果想得到LCS的路径,可以另外使用一个数组,记录走的路线。

//
//  main.cpp
//  LCS
//
//  Created by mini on 12/23/13.
//  Copyright (c) 2013 mini. All rights reserved.
//


#include <iostream>
#include <string>
#include <stack>
using namespace std;

#define SIZE 999

int dp[ SIZE ][ SIZE ] = { 0 };
char position[ SIZE ][ SIZE ];
int Max( int & x, int & y ){
    return x > y ? x : y;
}
void PrintLCS( char p[ ][ SIZE ], string str, int i, int j ){
    if( i < 1 || j < 1 )
        return ;
    if ( p[ i ][ j ] == '=' ) {
        PrintLCS( p, str, i - 1, j - 1 );
        cout << str[ i - 1 ] << " ";
    }
    else if( p[ i ][ j ] == '^' )
        PrintLCS( p, str, i - 1, j );
    else
        PrintLCS( p, str, i , j - 1 );

}
int main(int argc, const char * argv[])
{
    size_t i, j;
    string str1,str2;;
    size_t len1, len2;
    while( cin >> str1 >> str2 ){
        memset( dp, 0, sizeof( dp ) );
        len1 = str1.length();
        len2 = str2.length();
        for ( i = 1; i <= len1; i++ ) {
            for ( j = 1; j <= len2; j++ ) {
                if( str1[ i - 1 ] == str2[ j - 1 ] ){
                    dp[ i ][ j ] = dp[ i - 1][ j - 1] + 1;
                    position[ i ][ j ] = '=';//找到相等元素
                }
                else {
                    if ( dp[ i - 1 ][ j ] >= dp[ i ][ j - 1 ] ) {
                        dp[ i ][ j ] = dp[ i - 1 ][ j ];
                        position[ i ][ j ] = '^';
                    }
                    else{
                        dp[ i ][ j ] = dp[ i ][ j - 1 ] ;
                        position[ i ][ j ] = '<';
                    }
                    
                }
            }
        }
        cout << dp[ len1 ][ len2 ] << endl;
        i = len1;
        j = len2;
        stack< char > s;
        while( 1 ){
            if( i < 1 || j < 1 )
                break;
            if ( position[ i ][ j ] == '=' ) {
                s.push( str1[ i - 1 ] );
                i--;
                j--;
            }
            else if( position[ i ][ j ] == '^' ){
                i--;
            }
            else if( position[ i ][ j ] == '<' ){
                j--;
            }
        }
        char p;
        while( !s.empty() ){
            p = s.top();
            s.pop();
            cout << p << " ";
        }
        cout << endl;
        PrintLCS( position, str1, (int)len1, (int)len2 );
        cout << endl;
    }
    return 0;
    
}




        改进代码:一旦设计出某算法后,常常可以在时间或者空间上对算法进行改进。

        1.我们可以完全去掉记录路径信息的数组,因为每个表dp[i][j]的值取决于Max(dp[i - 1][j],dp[i][j-1]),或者dp[i-1][j-1],所以我么可以根据给定c[i][j]的值,进行判断,在o(1)时间内判断c[i][j]是根据这3个之中的哪个判断出来的。所以根本不需要额外的数组来专门记录路径。

        Code:

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

#define SIZE 999

int dp[ SIZE ][ SIZE ] = { 0 };
int Max( int & x, int & y ){
    return x > y ? x : y;
}
void PrintLCS( int dp[ ][ SIZE ], string str1, string str2, int i, int j ){
    if( i < 1 || j < 1 )
        return ;
    if ( str1[ i - 1 ] == str2[ j - 1 ] && dp[ i ][ j ] == (dp[ i - 1 ][ j - 1 ] + 1 ) ) {
        PrintLCS( dp, str1, str2, i - 1, j - 1 );
        cout << str1[ i - 1 ] << " ";
    }
    else if( dp[ i ][ j ] == dp[ i - 1 ][ j  ] )
        PrintLCS( dp, str1, str2, i - 1, j );
    else
        PrintLCS( dp, str1, str2, i , j - 1 );
    
}
int main(int argc, const char * argv[])
{
    int i, j;
    string str1,str2;;
    size_t len1, len2;
    while( cin >> str1 >> str2 ){
        memset( dp, 0, sizeof( dp ) );
        len1 = str1.length();
        len2 = str2.length();
        for ( i = 1; i <= len1; i++ ) {
            for ( j = 1; j <= len2; j++ ) {
                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 ] );
                }
            }
        }
        cout << dp[ len1 ][ len2 ] << endl;
        PrintLCS( dp, str1, str2, (int)len1, (int)len2 );
        cout << endl;
    }
    return 0;
    
}

            这里需要注意的一点是。不能直接根据dp[i][j] = dp[ i - 1] [j - 1] + 1,就判断有相同的元素,还需要实际上去判断字符串上i,j位置上元素是否相同。


        我们可以继续优化代码,在求dp[i][j]的时候,只会用到dp的两行,即被计算的那一行和前面一行,所以我们可以用2行的数组就可以搞定,空间复杂度继续降低o(2n)

        Code:

Source Code

Problem: 1458		User: m68300981
Memory: 248K		Time: 16MS
Language: C++		Result: Accepted
Source Code
#include <iostream>
#include <string>
using namespace std;

#define SIZE 999

int dp[ 2 ][ SIZE ];//仅仅用2行来求最大值
int Max( int & x, int & y ){
    return x > y ? x : y;
}
int main(int argc, const char * argv[])
{
    int i, j;
    string str1,str2;;
    size_t len1, len2;
    while( cin >> str1 >> str2 ){
        memset( dp, 0, sizeof( dp ) );
        len1 = str1.length();
        len2 = str2.length();
        for ( i = 1; i <= len1; i++ ) {
            for ( j = 1; j <= len2; j++ ) {
                if( str1[ i - 1 ] == str2[ j - 1 ] )
                    dp[ 1 ][ j ] = dp[ 0 ][ j - 1] + 1;
                else {
                    dp[ 1 ][ j ] = Max( dp[ 0 ][ j ], dp[ 1 ][ j - 1 ] );
                }
            }
            for ( int k = 1; k <= len2; k++ ) {
                dp[ 0 ][ k ] = dp[ 1 ][ k ];
            }
        }
        cout << dp[ 1 ][ len2 ] << endl;
    }
    return 0;
    
}


        继续优化代码,可以使用dp的一行来更新,另外使用两个变量一直记录dp[i-1][j-1]和dp[i-1][j]的值,dp的一行也让然可以继续优化,使用两个字符串中的那个值比较小的那个。

        Code:

Source Code

Problem: 1458		User: m68300981
Memory: 240K		Time: 16MS
Language: C++		Result: Accepted
Source Code
#include <iostream>
#include <string>
using namespace std;

#define SIZE 999

int main(int argc, const char * argv[])
{
    int i, j;
    string str1,str2;;
    size_t length1, length2;     //两个字符串的长度
    int leftTop, top;     //记录当前c[i][j]的上边的值和左上的值
    while( cin >> str1 >> str2 ){
        if ( str1.length() > str2.length() ) {    //始终让str1的长度小于str2的长度
            string temp;
            temp = str1;
            str1 = str2;
            str2 = temp;
        }
        length1 = str1.length();
        length2 = str2.length();
        int * dp = new int[ length1 ];
        memset( dp, 0, sizeof(int) * length1 );
        int temp;
        leftTop = dp[0];
        top = dp[1];
        for ( i = 1; i <= length2; i++ ) {
            for ( j = 1; j <= length1; j++ ) {
                temp = dp[j];
                if ( str1[ j - 1] == str2[ i - 1 ] ) {
                    dp[ j ] = leftTop + 1;
                }
                else{
                    dp[ j ] = top > dp[ j - 1 ] ? top : dp[ j - 1 ];
                }
                if ( j == length1 ) {
                    top = dp[ 1 ];
                    leftTop = dp[0];
                }
                else{
                    leftTop = temp;
                    top = dp[ j + 1 ];
                }
            }
        }
        cout << dp[ length1 ] << endl;
        
    }
    return 0;
    
}



       




        

        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值