算法学习心得——动态规划法实现最长公共子序列(LCS)

来源:互联网 发布:网络教育文凭毕业证 编辑:程序博客网 时间:2024/05/18 04:00

算法学习心得——动态规划法实现最长公共子序列(LCS)

一.问题说明

欲定义最长公共子序列(Longest Common Subsequence)问题,首先需要引入两个辅助性的概念,即:子序列的概念和公共子序列的概念。

    子序列的概念形式化为:设X = <x1, x2,┅, xm>,若有1≤i1< i2< ┅ <ik≤m,使得Z=< z1,z2,┅, zk> = < xi1, xi2,┅, xik>,则称Z是X的子序列,记为Z<X。举例来说明,比如:X=<A,B,C,B,D,A,B>, Z=<B,C,B,A>,  则有Z<X。

    然后介绍公共子序列的概念:设X,Y是两个序列,且有Z<X和Z<Y,则称Z是X和Y 的公共子序列。

    理解了子序列的概念和公共子序列的概念,那么就可以来解释最长公共子序列的概念了。若:若Z<X,Z<Y,且不存在比Z更长的X和Y 的公共子序列,则称Z是X和Y 的最长公共子序列,记为ZÎLCS(X , Y)。通俗易懂地讲就是一个序列S,若分别是两个或者多个已知序列的子序列,并且序列S是所有符合条件的子序列中最长的,那么就可以称S为已知的最长公共子序列。

    然而对于最长公共子序列的理解通常会有两种观点:第一种观点认为通过动态规划法求得的最长公共子序列有连续性质的要求,即所求的最长公共子序列只能由原序列中连续出现的若干个元素组成;第二种观点则认为对最长公共子序列没有连续性质的要求,即最长公共子序列是由原序列中的若干个元素所组成的,这些元素只需要满足最基本的相对顺序即可。本文中所介绍的最长公共子序列则是基于第二种观点。

   针对最长公共子序列问题的研究是因为有其重要性的应用领域,最长公共子序列主要用于生物信息学中DNA序列的比较。由于DNA序列非常庞大,比较两个或者若干个DNA序列的相似度就需要用到最长公共子序列。而且最长公共子序列也可以用于其他方面:比如比较两段文字或者两篇文章之间的相似程度,从而来辨别他们之间是否存在抄袭关系。

二.动态规划法介绍

由于最长公共子序列算法是基于动态规划法实现的,因此有必要介绍下动态规划法。说起动态规划法,顾名思义就是将原问题一层一层地分解为规模逐渐减小的同类型的子问题。而这些子问题往往具有高度重复性,基于子问题的高度重复性,动态规划法一般采用递推的方式,从规模最小的子问题开始依次计算规模逐渐增大的子问题。为了避免重复计算,通常是每次计算完后就把所得的结果保存起来。因此当再次遇到子问题时可以直接使用已经保存的结果,无须重新计算。

然而并不是所有的问题都适合动态规划法,只有当一个问题的子问题具有高度重复性,同时满足最优子结构(即:问题的最优解中包含着其每一个子问题的最优解)性质时,才能使用动态规划法求解。

三.最长公共子序列算法思想

清楚了最长公共子序列的概念,了解了动态规划法的精髓,那么现在可以来研究最长公共子序列算法的主要思想了。最长公共子序列问题属于多阶段决策问题中求最优解的一类问题。解决这类问题最直接的办法就是使用穷举法:列出所有长度不超过原序列的子序列,从长到短依次进行检查,看哪个序列满足条件。但是这种解法的时间复杂度往往是趋向去指数级别的,这种方法虽说简单易懂,但只能作为理论存在,实际应用中一般不会考虑这种方法。

         分析最长公共子序列问题,记 Xi=﹤x1,⋯,xi﹥即X序列的前i个字符 (1≤i≤m)(前缀);Yj=﹤y1,⋯,yj﹥即Y序列的前j个字符 (1≤j≤n)(前缀)假定Z=﹤z1,⋯,zk﹥∈LCS(X , Y)。

若xm=yn(最后一个字符相同),则不难用反证法证明:该字符必是X与Y的任一最长公共子序列Z(设长度为k)的最后一个字符,即有zk = xm = yn 且显然有Zk-1∈LCS(Xm-1, Yn-1)即Z的前缀Zk-1是Xm-1与Yn-1的最长公共子序列。

若xm≠yn,则亦不难用反证法证明:要么Z∈LCS(Xm-1, Y),要么Z∈LCS(X, Yn-1)。

由于zk≠xm与zk≠yn其中至少有一个必成立,若zk≠xm则有Z∈LCS(Xm-1 , Y),类似的,

若zk≠yn 则有Z∈LCS(X ,Yn-1)

若xm=yn,则问题化归成求Xm-1与Yn-1的LCS(LCS(X , Y)的长度等于LCS(Xm-1, Yn-1)的长度加1)

若xm≠yn 则问题化归成求Xm-1与Y的LCS及X与Yn-1的LCS

LCS(X, Y)的长度为:max{LCS(Xm-1 , Y)的长度, LCS(X , Yn-1)的长度}求LCS(Xm-1, Y)的长度与LCS(X , Yn-1)的长度

两者都需要求LCS(Xm-1,Yn-1)的长度,另外两个序列的LCS中包含了两个序列的前缀的LCS,故问题具有最优子结构性质 考虑用动态规划法。

    引进一个二维数组C,用C[i,j]记录Xi与Yj的LCS的长度如果我们是自底向上进行递推计算,那么在计算C[i,j]之前,C[i-1,j-1], C[i-1,j]与C[i,j-1]均已计算出来。此时

根据X[i]=Y[j]还是X[i]¹Y[j],就可以计算出C[i,j]:

若X[i]=Y[j],则执行C[i,j]←C[i-1,j-1]+1;

若X[i]¹Y[j],进行下述判断:

若C[i-1,j]≥C[i,j-1]则C[i,j]取C[i-1,j];

否则C[i,j]取C[i,j-1]。即有

C[i,j]=

为了构造出LCS,使用一个m´n的二维数组b,

b[i,j]记录C[i,j]是通过哪一个子问题的值求得的,以决定搜索的方向:

若C[i-1,j]≥C[i,j-1],则b[i,j]中记入“↑”;

若C[i-1,j]< C[i,j-1],则b[i,j]中记入“←”;(以上分析引自邓建明老师算法课程第三章最长公共子序列课件)

四.算法实现及实验分析

  最长公共子序列算法的实现是基于java语言的,开发工具是eclipse。CPU是Inter i5处理器,内存:3,系统是Windows xp

  LCS算法java实现

package seu.wjm;

 

import java.util.Random;

import java.util.Scanner;

 

/**

 * 基于动态规划法的最长公共子序列

 * @author wjm

 *

 */

public class LCS {

    public static void main(String[] args) {

    Scannerinput = new Scanner(System.in);

    System.out.println("请输入待比较的两个序列的长度(整数):");

    int len =input.nextInt();

   

        //设置待比较的两个序列的长度

    int subStrLen1 = len;

    int subStrLen2 = len;

   

        // 通过get_random_str(int)方法获取随机生成的字符串str1和str2

        String str1 = get_random_str(subStrLen1);

        String str2 = get_random_str(subStrLen2);

 

       

        // 构造二维数组记录子问题str1[i]和str2[i]的LCS的长度

        int[][] arr = new int[subStrLen1 + 1][subStrLen2 + 1];

 

        //获取系统当前时间(毫秒数)

        long startTime = System.nanoTime();

       

        // 最长公共子序列核心思想

        /**

         *            0                                                                    若i=0或j=0             

         * arr[i,j]=  arr[i-1,j-1]+1             若i,j>0且str1[i]==str2[j]

         *            max{arr[i,j-1],arr[i-1,j]} 若i,j>0且str1[i]!=str2[j]

         */

        for (int i = subStrLen1 - 1; i >= 0; i--) {

            for (int j = subStrLen2 - 1; j >= 0; j--) {

                if (str1.charAt(i) == str2.charAt(j)){

                    arr[i][j] = arr[i + 1][j +1] + 1;

 

                }

                else{

                    arr[i][j] = Math.max(arr[i+ 1][j], arr[i][j + 1]);

 

                }

            }

        }

 

        System.out.println("Str1:"+str1);

        System.out.println("Str2:"+str2);

        System.out.print("LCS:");

 

        /**

         * 用于计算LCS序列

         */

        int i = 0, j = 0;

        while (i < subStrLen1 && j <subStrLen2) {

            if (str1.charAt(i) == str2.charAt(j)) {

                System.out.print(str1.charAt(i));

                i++;

                j++;

            } else if (arr[i + 1][j] >= arr[i][j + 1]){

                i++;

                }

            else{

                j++;

                }

        }

        System.out.println();

        long endTime = System.nanoTime();

        System.out.println(" 算法所需时间: " + (endTime - startTime) + " ns");

    }

 

  

   

    //通过传人的长度随机生成指定长度的字符串,限制字符串是由小写字母构成的

    public static String get_random_str(int len) {

        StringBuilder builder = new StringBuilder("abcdefghijklmnopqrstuvwxyz");

        //StringBuilder对象sb用于记录最后字符串的内容

        StringBuilder sb = new StringBuilder();

        //生成一个随机类对象random

        Random random = new Random();

        int ranges = builder.length();

        for (int i = 0; i < len; i++) {

        //随机类对象random的nextInt(int)函数返回的是[0-ranges)之间随机的一个整数

           sb.append(builder.charAt(random.nextInt(ranges)));

        }

        return sb.toString();

    }

}

 

程序截图:

    经过实验分析可得出以下结论:在得到的随机字符串等长的前提下,最大公共子序列算法所需的时间复杂度和理论分析相符合。

五.学习心得

    本学期的算法课程使得我了解了动态规划法的核心思想,同时在课下通过网络和一些书籍(例如:编程之美,算法导论等)也学习了一些使用动态规划法解题的案例,熟悉了动态规划法解题的思路和步骤,对动态规划法有了较为深入的思考和认识。因此我觉得对于动态规划法的理解是我学习算法设计课程的最大收获,介于此,在参考了邓建明老师的算法课件的前提下,使用java语言实现了LCS算法。在编程过程中,不仅自己的动手实践能力得到了锻炼,同时自己停留在书本知识层面的理解进一步得到了实践的验证,使得自己对于动态规划法的认识更为深刻!