动态规划_求最长公共子序列LCS

来源:互联网 发布:制作婚纱照的软件 编辑:程序博客网 时间:2024/05/02 01:20

             学习动态规划有段时间了,我自己的感觉是看到题解很明白,但是拿到新题就后脑冒汗了,费解!我知道这其实是理解不深的缘故,动态规划是解决一类问题的方法,而不是解决某个问题的解法。今天我试着去感觉一下怎么去思考LCS的动态规划解。

        问题给出:给定了两个序列X=<x1,x2,x3......,xm>和Y=<y1,y2,y3.......,yn>,希望找出X和Y的最大长度公共子序列(Longest Common Sequence, LCS)。

        那么第一个问题是什么?分析X和Y怎样产生LCS,然后利用这个关系直接写出了动态规划转移方程,像算法导论给出的思路那样?不是的。至少我不是这么聪明,我的问题是:用什么思路去解,或者是在我的大脑库中有没有什么类似的题设对应的解题思路,或者更直接一点,为什么联想到用动态规划解

       回答这个问题,我的答案是有点取巧的。首先,要明白这个问题是有解的,一般也不会在不知情的情况下遇到一个哥德巴赫猜想去让你做,所以知道可以求解先让自己增加一点信心。然后,发挥中国学生锻炼了几十年的应试教育水平,找题设的提示!记得高中的时候做题,这个题是用三角函数的知识还是用解析几何的知识做是非常明了的,因为题里面要么大量出现了Sin、Cos或者给出了曲线方程,甚至大部分解析几何题都给出了椭圆或双曲线的坐标图,这些都是题设的信号。通常可以用动态规划解答的问题,都会存在“最好”、“最大”、“最小”或“最省钱”这类关键字,那么LCS问题的最大长度就是提示。提示我“或许可以试试动态规划”,思考就是一个尝试的过程,至少先找个方向往下走走,不行再回来。最后是一个个人感觉,通常能够用动态规划解的问题都让人产生一个感觉:面对了一堆乱麻,理不出头绪,让人无从下手。为什么这么说呢,因为动态规划面对的问题通常都很大,很难立刻就找到突破口。直接让你吃整个西瓜而不切开,任谁都会口渴无奈(八戒除外)。此外,动态规划一般需要人来逆向思维,这种反人类的思维方式的存在就是让人头疼而不会让人第一时间以这种思维方式思考问题,所以,拿到某些问题让人摸不着头脑本身也是一种提示。

         既然决定使用动态规划来尝试,那么下一步怎么走呢?自然是寻找状态和状态转移方程了。现在,在潜意识里应该保持一种冲动:我现在要能敏锐地发现,某个长LCS和组成它的子LCS是什么关系?怎样描述LCS的存在状态?现在能想到的就是,某个LCS由一定长度的X序列和一定长度的Y序列公共元素组成,而这个LCS的的子LCS(即子问题)是由短一点的X序列和Y序列的公共元素构成。那么二者是怎样转换的呢?这里的状态转移并不是很明显的,有些问题的转移很容易得出,比如公司聚会问题(Stewart教授是一家公司总裁的顾问,这家公司计划一个公司聚会。这个公司有一个层次结构;也就是,管理关系形成一棵以总裁为根的树。人事部给每个雇员以喜欢聚会的程度来排名,这是一个实数。为了使每个参加者都喜欢这个聚会,总裁不希望一个雇员和他(她)的直接上司同时参加。Stewart教授面对一颗描述公司结构的树,使用了左子女、右兄弟表示法。树中每个节点除了包含指针,还包含雇员的名字以及该雇员喜欢聚会的排名。描述一个算法,它生成一张客人列表,使得客人喜欢聚会的程度的总和最大),该问题中,某个人是否参加聚会直接影响了上下级的元素,所以考虑问题时自然是从总裁开始,自上而下,面临的选择也很明显,即这个人是否参加两种情况,取两种情况最优即可。当我开始思考LCS时,我发现我并不是按照算法导论的思路走的,我恰恰选择了一种自毁前路的思考方式,下面就说说我是怎样走入死胡同的。

         我想的是已经从X(1 to i )和Y(1 to j )的中找到一个LCS序列Z(t)。那么下一步就是如何在X(k>i)和Y(k>j)中找到更长的LCS序列Z(t+1)。如果X(i+1)=Y(j+1),那么LCS(t+1)=LCS(t)+1,并且将i增加到i+1, j增加到j+1,成功转移。但是如果X(i+1) != Y(j+1)呢,那么就要把X[或Y]序列后推一个元素,寻找X(i+1)和Y(j) [或X(i)和Y(j+1)]的LCS,关键的难题来了,怎么写这个递推方程!现在的情况就像下面的树形图一样,我站在B点希望推导出和A的关系,但是我发现要推导出A就必须知道C,D,E,而在这种思维方式下,我无法用数学公式表示C,D,E也就无法表示出递推关系,所以我会在这里失败。


        然而,这样思考一下也没有坏处,因为分析出的这种转化关系其实和正确的动态规划思路非常相近了。在尝试正确的思考前,反省一下自己为什么会毫无悬念地落入思维迷宫是值得的。最重要的一点就是,这是人的习惯性思维方式,正向地思考问题,而正向思考问题往往很难利用动态规划来解决问题。动态规划的一个关键点就是,动态规划是自顶向下地思考问题,注意我说的是思考问题的方式,不是解决问题,动态规划是自底向上地利用最优子结构解决问题的。即,着眼于大问题,思考怎样分解为小问题并且寻找大问题怎样由小问题合并而来的,说得学术点就是分治思想,通俗点就是要逆向思维。

      在学习动态规划时,强调了千百遍要分解问题分解问题,但在求解问题时立马抛诸脑后正向思考去了,其实思维也是有惰性的,一个不留神,思维就偷懒去了并且邪邪地看着自己犯错。

      那么回到题目,尝试逆向思考。思考前有一个假设,先假设已经找到了某状态下的最优解不必关心这个最优解是怎么来的。而前一种失败的思路潜意识里其实是寻求最优解的过程,仔细想一下,如果目测去寻找两个序列的LCS,我们也是按那种思路来的。

      现在假定已知X(1 to i)和Y(1 to j)的一个最长公共子序列Z(1 to k)的长度为L(i,j),那么现在怎样将这个大问题分解为小问题呢?那就要分析公共子序列是如何产生的了。注意,逆向思维。查看得到的这个最优子序列,

    如果X(i)=Y(j)=Z(k),说明X(i)和Y(j)恰好是公共序列Z(1 to k)的最后一个元素,那么Z(k-1)必然存在于X(1 to i-1)和Y(1 to j-1)中,即L(i-1,j-1)和L(i,j)相差1:L(i,j)=L(i-1,j-1)+1

     如果X(i)!=Y(j),说明Z(k)存在于X(m<=i)和Y(n<=j)中(m,n不同时取 =)即L(i,j)=max{ L(m,n) , m<=i,n<=j 且m,n不同时取等号}, 另外要注意到L(x,n)>=L(k<x, n)一定成立,因为X序列和Y序列的LCS一定比X 子序列和Y序列的LCS长,如LCS(10,20)>LCS(5,20)一定成立,同理L(n,y)>=L(n,k<y)一定成立,所以将上式改写为L(i,j)=max{ L(i-1,j), L(i,j-1)}

     此时就得出了LCS问题的递推方程,现在可以说这种思路尝试成功了,但对解题来说还剩下最后一点工作,边界条件。LCS的边界条件较简单即L(0,0)=0。

     将边界条件和上面两个递推式合起来就得到了LCS的解法。

     以上纯属个人思路整理,如有错误,请大家不吝指出。

附:LCS的ruby实现:

def lcs(x,y,memo={})return memo[[[x.length,y.length]]] if memo[[[x.length,y.length]]]if x.empty? || y.empty?memo[[[x.length,y.length]]]=[]elsif x.last==y.lastmemo[[[x.length,y.length]]]=lcs(x[0..-2],y[0..-2],memo)+[x.last]elsel1=lcs x[0..-2],y,memol2=lcs x,y[0..-2],memomemo[[[x.length,y.length]]]=l1.length<l2.length ? l2 : l1endmemo[[[x.length,y.length]]]end