[算法] 最长公共子序列(LCS)问题

最近刚刚结束了算法分析课程,并结束了考试,败在了最长公共子序列 的问题上,非常懊悔,于是把最长公共子序列问题拿出来再次学习。

一、什么是最长公共子序列

什么是最长公共子序列呢?举个简单的例子吧,一个数列S,若分别是两个或多个已知序列的子序列,且是所有符合条件序列中最长的,则S称为已知序列的最长公共子序列。

举例如下,如:有两个随机数列,1 2 3 4 5 6 和 3 4 5 8 9,则它们的最长公共子序列便是:3 4 5。

我在考试中由于对最长公共子序列一窍不通,于是乎自己瞎猜了一下他的定义,猜成了最长公共子集合,异常尴尬,导致题错的干干净净。。。这里先来回顾下最长公共子序列的定义知识。

算法导论书上介绍的子序列的定义是:一个给定序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。
给定两个序列X和Y,如果Z既是X的子序列,也是Y的子序列,我们称它是X和Y的公共子序列。
最长公共子串(Longest Common Substirng)和最长公共子序列(Longest Common Subsequence,LCS)的区别为:
子串是串的一个连续的部分,子序列则是从不改变序列的顺序,而从序列中去掉任意的元素而获得新的序列;也就是说,子串中字符的位置必须是连续的,子序列则可以不必连续
至于公共子集合。。。那就是可以改变序列的顺序也可以不连续了吧。。。

二、蛮力法

蛮力法是解决最长公共子序列问题最容易想到的方法,即对S的每一个子序列,检查是否为T的子序列,从而确定它是否为S和T的公共子序列,并且选出最长的公共子序列。

S和T的所有子序列都检查过后即可求出S和T的最长公共子序列。S的一个子序列相应于下标序列1,2,…,n的一个子序列。因此,S共有2^n个子序列。当然,T也有2^m个子序列。

因此,蛮力法的时间复杂度为O(2^n * 2^m),这可是指数级别的啊。

三、动态规划方法

无论是哪里,一种更快的方式都是值得提倡的,这里重点讲解书上的动态规划方法。

刻画最长公共子序列问题一共需要

1.刻画最长公共子序列的特征

这里写图片描述

2.步骤二:一个递归解

这里写图片描述
这里写图片描述

3.步骤三:计算LCS的长度

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

根据上图,我们可以得到其中公共子串:B C B A 和 B D A B。

以上截取自课本《算法导论》。。。我记录下这个算法在图中实现的过程

1.定义X序列和Y序列,X序列对应行,Y序列对应列,首先将元素填入行和列然后将0行0列分别填0(因为一个字符串为空必定没有公共子序列)
2.首先从1行1列开始填,看横纵坐标轴元素,若元素相同,值为左上角值加一,画指向左上角的箭头;若元素不相同,看上面和左面的值,哪一个大就填写哪一个值,并将箭头指向这个位置(前趋),若值相同则指向上方位置(其实这里指向上面还是左面都是可以的,只要过程中使用同样规则就好);直到整个矩阵填写完毕,最右下角的值即为最长公共子序列的长度
3.从最右下角往回回溯,每次遇见斜向指出的箭头,即记下来斜向箭头的出发点元素。最终到最左上角位置。得到一个逆序的目的子序列,反转即得到最终想要得到的最长公共子序列。

四.程序思想

引进一个二维数组c[][],用c[i][j]记录X[i]与Y[j] 的LCS 的长度,b[i][j]记录c[i][j]是通过哪一个子问题的值求得的,以决定搜索的方向。
我们是自底向上进行递推计算,那么在计算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]。

算法分析:
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m * n)次就会遇到i = 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为Θ(m * n)。

public class LCSProblem 
{
    public static void main(String[] args)
    {
        //保留空字符串是为了getLength()方法的完整性也可以不保留
        //但是在getLength()方法里面必须额外的初始化c[][]第一个行第一列
        String[] x = {"", "A", "B", "C", "B", "D", "A", "B"};
        String[] y = {"", "B", "D", "C", "A", "B", "A"};

        int[][] b = getLength(x, y);

        Display(b, x, x.length-1, y.length-1);
    }
    /**
     * @param x
     * @param y
     * @return 返回一个记录决定搜索的方向的数组
     */
    public static int[][] getLength(String[] x, String[] y)
    {
        int[][] b = new int[x.length][y.length];
        int[][] c = new int[x.length][y.length];

        for(int i=1; i<x.length; i++)
        {
            for(int j=1; j<y.length; j++)
            {
                //对应第一个性质
                if( x[i] == y[j])
                {
                    c[i][j] = c[i-1][j-1] + 1;
                    b[i][j] = 1;
                }
                //对应第二或者第三个性质
                else if(c[i-1][j] >= c[i][j-1])
                {
                    c[i][j] = c[i-1][j];
                    b[i][j] = 0;
                }
                //对应第二或者第三个性质
                else
                {
                    c[i][j] = c[i][j-1];
                    b[i][j] = -1;
                }
            }
        }   

        return b;
    }
    //回溯的基本实现,采取递归的方式
    public static void Display(int[][] b, String[] x, int i, int j)
    {
        if(i == 0 || j == 0)
            return;

        if(b[i][j] == 1)
        {
            Display(b, x, i-1, j-1);
            System.out.print(x[i] + " ");
        }
        else if(b[i][j] == 0)
        {
            Display(b, x, i-1, j);
        }
        else if(b[i][j] == -1)
        {
            Display(b, x, i, j-1);
        }
    }
}

补:最长公共子字符串:类似最长子序列,只是公共子字符串要求必须是连续的。

java实现代码如下:

public class stringCompare {
    //在动态规划矩阵生成方式当中,每生成一行,前面的那一行就已经没有用了,因此这里只需使用一维数组,而不是常用的二位数组
    public static void getLCString(char[] str1, char[] str2) {
        int len1, len2;
        len1 = str1.length;
        len2 = str2.length;
        int maxLen = len1 > len2 ? len1 : len2;

        int[] max = new int[maxLen];// 保存最长子串长度的数组
        int[] maxIndex = new int[maxLen];// 保存最长子串长度最大索引的数组
        int[] c = new int[maxLen];

        int i, j;
        for (i = 0; i < len2; i++) {
            for (j = len1 - 1; j >= 0; j--) {
                if (str2[i] == str1[j]) {
                    if ((i == 0) || (j == 0))
                        c[j] = 1;
                    else
                        c[j] = c[j - 1] + 1;//此时C[j-1]还是上次循环中的值,因为还没被重新赋值
                } else {
                    c[j] = 0;
                }

                // 如果是大于那暂时只有一个是最长的,而且要把后面的清0;
                if (c[j] > max[0]) {
                    max[0] = c[j];
                    maxIndex[0] = j;

                    for (int k = 1; k < maxLen; k++) {
                        max[k] = 0;
                        maxIndex[k] = 0;
                    }
                }
                // 有多个是相同长度的子串
                else if (c[j] == max[0]) {
                    for (int k = 1; k < maxLen; k++) {
                        if (max[k] == 0) {
                            max[k] = c[j];
                            maxIndex[k] = j;
                            break; // 在后面加一个就要退出循环了
                        }
                    }
                }
            }
            for (int temp : c) {
                System.out.print(temp);
            }
            System.out.println();
        }
        //打印最长子字符串
        for (j = 0; j < maxLen; j++) {
            if (max[j] > 0) {
                System.out.println("第" + (j + 1) + "个公共子串:");
                for (i = maxIndex[j] - max[j] + 1; i <= maxIndex[j]; i++)
                    System.out.print(str1[i]);
                System.out.println(" ");
            }
        }
    }

    public static void main(String[] args) {

        String str1 = new String("binghaven");
        String str2 = new String("jingseven");
        getLCString(str1.toCharArray(), str2.toCharArray());
    }
}

猜你喜欢

转载自blog.csdn.net/mjl960108/article/details/54428711