算法导论15章 动态规划 dynamic programming 复习第二弹 最长公共子序列

本文探讨了动态规划在解决最长公共子序列(LCS)问题中的应用,指出动态规划的关键在于optimal structure,即子问题的最优解能组合成原问题的最优解。通过分析LCS问题,揭示了其具有optimal substructure,从而可以避免重复计算,提高效率。文章讨论了如何构建算法,利用递归思想和决策树来理解问题,并强调了递归和动态规划之间的关系。
摘要由CSDN通过智能技术生成

书上所谓的optimal structure,是动态规划的应用条件。
关于这点我不是非常认同,比如我求斐波那契数列的第i个数,我也能将递归算法转化成动态规划的办法,记录已有的信息,来获得下一个信息。或者说这并不叫动态规划?只有optimal structure的问题才叫动态规划?感觉就是诡辩,动态规划的正式定义是什么?书上并没有给出描述,所以要探索DP方法的潜能是不可能的,因为只有一个模糊不清的定义。
按照书上所说,似乎DP方法只能用于optimal problem。

书中下一个例子是LCS,最长公共子序列问题。
虽然已经接触过很多次了,但我还从来没有把思考过程写出来.
从定义下手,子序列是某字符串S的子集,并且其字符顺序保持在S中的样子。
公共子序列就是string S1和S2的相等的子序列。
最长公共子序列LCS就是所有公共子序列中最长的那个,有时候不一定会只有一个。比如
S1 = abc
S2 = acb 这样的情况下LCS为LCS1 = ac,LCS2 = ab。
这样一个问题,怎么辨别它是具有optimal substructure的问题呢。
分析:整个问题的规模是在所有子序列中找到最大公共子序列。或者从公共子序列中找到最长序列。对于公共子序列的数量,最多可能有2^min(m,n)种,其中m为S1长度,n为S2长度。但LCS的最大长度只可能是min(m,n)。取得关键数据的值域是很重要的,因为这些可以帮我构建算法过程的计算空间的大小,花2^min(m,n)去找出所有公共子序列然后找最大值是极不合理的。所以选择考虑别的方法。可以看见的是2^min(m,n)这样的查找次数很多都发生了一定的重复计算,因为长度为i的公共子序列很可能是长度为i+1的公共子序列的前缀,导致了不合理的重复计算前i个字符的部分。这里发生重复的核心在于对于不同长度的子序列来说有前缀关系。这也明显的说明从长度为i的公共子序列到长度为i+1的公共子序列直接是存在直接关系的,在求长度为i+1的LCS之前可以利用长度为i的LCS的信息。
这样一种很自然的方法就是,记录长度为i的所有公共子序列,然后递推到长度为i+1的所有公共子序列,但这样也存在一个简单的优化问题,就是从长度为i的LCS我只需关注就是,这个LCS的结尾处在S1和S2中的位置,然后再从这两个位置开始搜索下一个长度为i+1的LCS,但同时这个方法问题也很大,有两个问题:
1,从长为i的公共子序列到长尾i+1的LCS要经过太多的查找。(估计)
2,记录的长为i的所有公共子序列很多根本就是不可能成为最优解.
3 , 最为麻烦的是算法不仅取决于S1和S2的长度,根依赖于S1和S2的重复度,比如:
S1 = abcd; S1 = aaaa;
S2 = efgh; 这样的完全没有公共子序列的情况是最快的,但是 S2 = aaaa;这样的情况是运算最大的。这样的情况下是指数级运算量,需要抛弃这个办法,果然想找到一个和书上不一样的办法是非常困难的,书中的思路应该说是最合适的吧。
求公共子序列也是需要利用了贪婪算法的思想,不然对于S1 = aaaaa;S2 = aaaaa;这样的情况十分不利。
求解字符串X和Y的最大公共子序列从决策树的角度看该如何解呢?决策树解决这个问题的过程是通过判断每个点是否在最大公共子序列中来完成的,先从X或者Y的第一个字符开始,如果X的第一个字符和Y的第一个字符相等,那么就求得了一个最大公共子序列的元素。但是问题在于当X和Y的第一个字符不等的时候该怎么办?这样就有两种可能,X的第一个元素不在LCS中,或者X的第一个元素在LCS中(看起来没什么改变),但是已经确定X[0]和Y[0]是不匹配的,那么此时就应该分支计算,产生递归,计算所有的可能性,如果X[0]不在LCS中那么就从X[1]开始重新获得和Y的LCS,否则的话X[0]在LCS中,那么这就说明Y[0]不在LCS中(Y[0]!=X[0]),这时候就从Y[1]开始和X一起匹配LCS。这就是决策树第一次决策的过程,一次类推,到X从i开始和Y从j开始匹配LCS时候也是先判断X[i]==Y[j]?然后根据结果,看X[i]和Y[i]是否在LCS的所有可能都要进行计算。在这个过程中就会发现一个大量重复计算的量就是在计算X[i]和Y[j]开始的lcs的字符串进行了重复计算,这是时候使用dp[i][j]记录这个量。其实这就是书上的办法,可以从不同的角度去看。
求解一个X和Y的lcs和分解成求解一个更小的问题的lcs,并且求解这些子问题直接不相干扰,这就是这个问题的最优子结构,其实看出最优子结构的瞬间就已经能简单的取得LCS的递推式了。就像一部到位一样,我应该从一个更合理的角度来推出结果。
之前就发现了dp能解决的问题递归一定能解决,没有办法能一开始就判断一个问题是不是最优子结构,因为知道怎样分解问题而子问题之间不相互干扰是很困难的。应该从更大的角度来看,如果递归方法能解决问题那么就有进一步讨论dp方法的可能性,因为dp方法本质上是在记录递归计算中完全重复计算的部分,加快速度而已,并且还加了子问题之间不能相互影响的前提条件。先用递归判断能否解决问题才是关键,递归只是把问题分解成相同的子问题。至于子问题之间是否干扰是不会有影响的。那么LCS问题能否分解成规模更小的子问题呢?回答必然就是肯定的,从X和Y的最后一个节点看它们是否相等,然后计算每个可能性,这时候已经至少排除了一个字符的规模,问题从计算一个大规模的问题,变成了计算多个更小的问题。说明递归是可行的。在看dp方法是否可行呢?发现了很多可以重复利用的值。象书上那种判断最优子结构的办法对我来说就容易走进一开始那种想法的误区,直接就试图用dp方法根本不可取。当然已经解决过的问题就另说,但毕竟不可能所有的都做过。

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
void print(int  b[100][100],string X,int i,int j)
{
    if(i == 0||j == 0) {
        cout<<endl;
        return ;
    }
    if(b[i][j] == 0){
        print(b,X,i-1,j-1);
        cout<<X[i-1];
    }else if(b[i][j] == 1){
        print(b,X,i-1,j);
    }else{
        print(b,X,i,j-1);
    }
}
void lcs_length(string x,string y)
{
        int m = x.size();
        int n = y.size();
        int b[100][100];
        int c[100][100];
        for(int i = 0;i<=m;i++)
            c[i][0] = 0;
        for(int i = 0;i<=n;i++)
            c[0][i] = 0;
        for(int i = 1;i<=m;i++){
            for(int j = 1;j<=n;j++)
            {
                if(x[i-1] == y[j-1]){
                    c[i][j] = c[i-1][j-1]+1;
                    b[i][j] = 0;
                }else if(c[i-1][j] >= c[i][j-1]){
                    c[i][j] = c[i-1][j];
                    b[i][j] = 1;
                }else{
                    c[i][j] = c[i][j-1];
                    b[i][j] = -1;
                }
            }
        }
    cout<<x<<"\n"<<y<<endl;
    print(b,x,m,n);
}
int main()
{
    string a = "aaaaabbbd";
    string b = "aaaaabbbd1";
    while(cin>>a>>b)
    {
        lcs_length(a,b);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值