算法篇——最长公共子序列(三种解法:递归、备忘录、动态规划)

2,348 阅读3分钟

内容一 最长公共子序列

给定两个序列X和Y,基于备忘录方法,编 写程序找出X和Y所有的最长公共子序列

要求:基于备忘录方法

思考:多个LCS如何寻找

分析:时间复杂度

扩展一:动态规划以及备忘录方法和递归的比较

动态规划的基本要素: 1 最优子结构性质 当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。 2 重叠子问题性质
动态规划算法对每个问题只解一次,将其解保存在一个表格中,当再次需要解此问题时,用常数时间查看一下结果。因此,用动态规划算法通常只需要多项式时间。

备忘录方法: •用一个表格来保存已解决的子问题的答案,用的时候查表即可。 •采用的递归方式是自顶向下。 •控制结构与直接递归相同,区别在于备忘录方式为每个解过的子问题建立备忘录。 •初始化为每个子问题的记录存入一个特殊的值,表示并未求解。在求解过程中,查看相应记录如果是特殊值,表示未求解,否则只要取出该子问题的解答即可。

备忘录方法与动态规划和递归的区别:

1、动态规划是自底向上 ,备忘录方法是自顶向下,递归是自顶向下

2、动态规划每个子问题都要解一次,但不会求解重复子问题;备忘录方法只解哪些确实需要解的子问题;递归方法每个子问题都要解一次,包括重复子问题•

递归解法

/**
 * @author SJ
 * @date 2020/10/22
 */
public class LCS {
    //求两个序列的最长公共子序列
    public static void main(String[] args) {
        String s1="abcde";
        String s2="ace";
        char[] chars1 = s1.toCharArray();
        char[] chars2 = s2.toCharArray();
        int lcs = getLcs(chars1.length - 1, chars2.length - 1, chars1, chars2);
        System.out.println(lcs);


    }
    //暴力递归方法
    //指针i和j自顶向下对两个子序列进行扫描
    public static int getLcs(int i,int j,char[] s1,char[] s2){
        //找到最前面还没有找到公共元素
        if (i==-1||j==-1)
            return 0;
        //找到一个公共元素就继续向前找
        else if (s1[i]==s2[j])
            return getLcs(i-1,j-1,s1,s2)+1;
        else
            return Math.max(getLcs(i-1,j,s1,s2),getLcs(i,j-1,s1,s2));

    }


}

测试结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
3

Process finished with exit code 0

备忘录解法

/**
 * @author SJ
 * @date 2020/10/27
 */
public class LCS2 {
    public static void main(String[] args) {
        String s1 = "abcde";
        String s2 = "ace";
        new LCS2(s1.length() + 1, s2.length() + 1);
        int lcs = getLcs(s1.length(), s2.length(), s1.toCharArray(), s2.toCharArray());
        System.out.println(lcs);

    }

    //建立备忘录
    public static int[][] memo;

    public LCS2(int l1, int l2) {
        memo = new int[l1][l2];
        initializeMemo();
    }


    //初始化备忘录
    public static void initializeMemo() {
        for (int i = 0; i < memo.length; i++) {
            for (int i1 = 0; i1 < memo[i].length; i1++) {
                memo[i][i1] = -1;
            }
        }
    }

    public static int getLcs(int i, int j, char[] s1, char[] s2) {
        //递归求解之前先检查备忘录
        //之后的过程也是更新备忘录的过程
        if (memo[i][j] != -1)
            return memo[i][j];
        //第一行和第一列不放东西
        if (i == 0 || j == 0)
            memo[i][j] = 0;
        else if (s1[i - 1] == s2[j - 1])
            memo[i][j] = getLcs(i - 1, j - 1, s1, s2) + 1;
        else
            memo[i][j] = Math.max(getLcs(i - 1, j, s1, s2), getLcs(i, j - 1, s1, s2));

        return memo[i][j];
    }
}

测试结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
3

Process finished with exit code 0

动态规划解法

import java.util.*;

/**
 * @author SJ
 * @date 2020/10/27
 */
public class LCS3 {
    //得到最长公共子序列并输出
    //这次采用自底向上的动态规划的方法保存每一个子问题的解,方便回溯
    public  int[][] dp;//保存每一个子问题的最长公共子序列的长度
    public static Set<String> lcs=new HashSet<>();//保存具体的最长公共子序列

    public LCS3(int l1, int l2) {
        dp = new int[l1 + 1][l2 + 1];
        //在构造函数里顺便初始化了
        for (int i = 0; i < dp.length; i++) {
            for (int i1 = 0; i1 < dp[i].length; i1++) {
                if (i == 0 || i1 == 0)
                    dp[i][i1] = 0;
                else
                    dp[i][i1] = -1;
            }
        }
       // lcs = new HashSet<>();
    }

    //自底向上更新dp数组
    public void getLcs(char[] s1, char[] s2) {
        for (int i = 1; i < dp.length; i++) {
            for (int j = 1; j < dp[i].length; j++) {
                if (s1[i - 1] == s2[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }

        }

    }

    //从dp右下角开始回溯,求出最长公共子序列
 //从dp右下角开始回溯,求出最长公共子序列
    public void traceBack2(int i, int j, List<Character> temp, char[] s1, char[] s2) {
        //终止条件
        if (temp.size() == dp[s1.length][s2.length]) {
            // System.out.println(temp.toString());
            lcs.add(temp.toString());
            return;
        }
        while (i > 0 && j > 0) {
            if (s1[i - 1] == s2[j - 1]) {
                temp.add(0, s1[i - 1]);
                i--;
                j--;
            } else {
                if (dp[i - 1][j] > dp[i][j - 1])
                    i--;
                else if (dp[i - 1][j] < dp[i][j - 1])
                    j--;
                else {
                    traceBack(i - 1, j, temp, s1, s2);
                  
                    traceBack(i, j - 1, temp, s1, s2);
           
                    return;


                }


            }


        }
    }
    

    //输出dp数组(测试用)
    public void printDp() {
        for (int i = 0; i < dp.length; i++) {
            for (int i1 = 0; i1 < dp[i].length; i1++) {
                System.out.print(dp[i][i1] + " ");
            }
            System.out.println();
        }
    }

    //输出lcs列表(测试用)
    public static void printLcsList() {
        System.out.println("最长公共子序列有");
        for (String lc : lcs) {
            System.out.println(lc);
        }
    }
   
    public static void main(String[] args) {
        String s1 = "ABCBDAB";
        String s2 = "BDCABAD";
        LCS3 lcs3 = new LCS3(s1.length(), s2.length());
        lcs3.getLcs(s1.toCharArray(), s2.toCharArray());
        lcs3.printDp();
        List<Character> temp = new ArrayList<>();
        lcs3.traceBack(s1.length(), s2.length(), temp, s1.toCharArray(), s2.toCharArray());
        printLcsList();
    }
}

输出结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"
0 0 0 0 0 0 0 0 
0 0 0 0 1 1 1 1 
0 1 1 1 1 2 2 2 
0 1 1 2 2 2 2 2 
0 1 1 2 2 3 3 3 
0 1 2 2 2 3 3 4 
0 1 2 2 3 3 4 4 
0 1 2 2 3 4 4 4 
最长公共子序列有
[B, C, B, D]
[B, C, B, A]

Process finished with exit code 0

//dp回溯的时候有点错误,大家自行修改一下 文档是后期上传的,我懒得回去翻正确代码了