JAVA代码—算法基础:求两个字符串的最长公共子序列问题(详解)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/seagal890/article/details/79178848

求两个字符串的最长公共子序列问题

最长公共子序列问题: 给定两个字符串A、B,求A与B的最长公共子序列(子序列不要求是连续的)
举例:
字符串A: abcicba
字符串B:abdkscab
其中:ab、abc、abca都是公共子序列,但是abca是最长公共子序列

从文件读取输入:
1A2C3D4B56
B1D23CA45B6A

输出:
123456
或者 12C4B6都正确

package com.bean.algorithmexec;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class LCS {

    /*
     * 最长公共子序列问题 给定两个字符串A、B,求A与B的最长公共子序列(子序列不要求是连续的) 
     * 举例: 
     * 字符串A: abcicba 
     * 字符串B:abdkscab
     * 其中:ab、abc、abca都是公共子序列,但是abca是最长公共子序列
     * 
     * 从文件读取输入:
     * 1A2C3D4B56
     * B1D23CA45B6A
     * 
     * 输出:
     * 123456 
     * 
     * 或者 12C4B6都正确
     * 
     * (这里的算法其实存在一个缺陷)
     */

    public static void main(String[] args) throws FileNotFoundException {
        // TODO Auto-generated method stub

        System.setIn(new FileInputStream("G:\\lcs.txt"));
        Scanner sc = new Scanner(System.in);

        String aStr = sc.nextLine(); //读取A字符串
        String bStr = sc.nextLine(); //读取B字符串
        int aLen = aStr.length();
        int bLen = bStr.length();
        //构造DP矩阵
        int[][] dp = new int[aLen + 1][bLen + 1];
        for (int i = 1; i < aLen + 1; i++)
            for (int j = 1; j < bLen + 1; j++)
                if (dp[i - 1][j] == dp[i][j - 1] && aStr.charAt(i - 1) == bStr.charAt(j - 1)
                        && dp[i - 1][j] == dp[i - 1][j - 1])
                    dp[i][j] = dp[i - 1][j] + 1;
                else
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        int max = dp[aLen][bLen];
        StringBuilder sb = new StringBuilder();
        while (max > 0) {
            if (dp[aLen - 1][bLen] == dp[aLen][bLen - 1] && dp[aLen - 1][bLen] + 1 == dp[aLen][bLen]) {
                sb.append(aStr.charAt(aLen - 1));
                max--;
                aLen--;
                bLen--;
            } else {
                if (dp[aLen][bLen - 1] == dp[aLen][bLen])
                    bLen--;
                else
                    aLen--;
            }
        }

        System.out.println(sb.reverse().toString());
    }

}

另一种算法设计

这个问题是一个经典的动态规划问题,先来介绍求解动态规划表的过程。

第1步:

如果str1的长度为M,str2的长度为N,则生成大小为M*N的矩阵dp,行数为M行,列数为N列。
dp[i][j]的含义是str1[0...i]str2[0...j]的最长公共子序列的长度。从左向右,从上向下计算矩阵dp。

其中:记str1中从第0个字符到第i个字符为 str1[0...i];记str2中从第0个字符到第j个字符为 str2[0...j]

矩阵dp第一列即dp[0...M1][0]dp[i][0]的含义是str1[0...i]str2[0]的最长公共子序列长度。
str2[0]只有一个字符,所以dp[i][0]最大为1。如果 str1[i]==str2[0],令dp[i][0]=1,一旦dp[i][0]被置为1,之后的dp[i+1...M1][0]也都为1。

例如:str1[0...M1]=ABCDEstr2[0]=B
str1[0]="A",与str2[0]不相等,所以dp[0][0]=0
str1[1]="B",与str2[0]相等,所以str1[0...1]str2[0]的最长公共子序列为“B”。
dp[1][0]=1.

之后的 dp[2...4][0]肯定都是1,因为 str[0...2]str[0...3]str[0...4]str2[0]的最长公共子序列肯定有“B”。

第2步:

矩阵dp第一行即dp[0][0...N1]与第1步同理,如果 str1[0]==str2[j],则令 dp[0][j]=1,一旦dp[0][j]1dp[0][j+1…N-1]$也都为1。

第3步:

对于一般位置(i,j),dp[i][j]的值只可能来自以下三种情况:

  • 可能是 dp[i1][j],代表 代表str1[0...i1]str2[0...j]的最长公共子序列长度。例如:str1=“A1BC2”,str2=“AB34C”。str1[0...3]即“A1BC”与str2[0...4]即“AB34C”的最长公共子序列为“ABC”,即dp[3][4]为3。str1[0...4]str2[0...4]的最长公共子序列也是“ABC”,所以dp[4][4]也为3。
  • 可能是 dp[i][j1],代表str1[0...i]str2[0...j1]的最长公共子序列长度。str1=“A1B2C”,str2=“AB3C4”。str1[0...4]即“A1B2C”与str2[0...3]即“AB3C”的最长公共子序列为“ABC”,即dp[4][3]为3。str1[0...4]str2[0...4]的最长公共子序列也是“ABC”,所以dp[4][4]也为3。
  • 如果 str1[i]==str2[j],还可能是 dp[i1][j1]+1。例如:str1=“ABCD”,str2=“ABCD”。str1[0...2]即“ABC”与str2[0...2]即“ABC”的最长公共子序列为“ABC”,即dp[2][2]为3。因为str1[3]==str2[3][3]==D,所以str1[0...3]str2[0...3]的最长公共子序列是“ABCD”。

这三个可能的值中,选最大的作为dp[i][j]的值。

算法设计如下:

public int[][] getdp(char[] str1, char[] str2) {
        int[][] dp = new int[str1.length][str2.length];

        dp[0][0] = (str1[0] == str2[0] ? 1 : 0);
        for (int i = 1; i < str1.length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
        }
        for (int j = 1; j < str2.length; j++) {
            dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0);
        }

        for (int i = 1; i < str1.length; i++) {
            for (int j = 1; j < str2.length; j++) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                if (str1[i] == str2[j]) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
                }
            }
        }

        return dp;
    }

dp矩阵中最右下角的值代表str1整体和str2整体的最长公共子序列的长度。通过整个dp矩阵的形态,可以得到最长公共子序列。具体方法如下:

第1步:从矩阵右下角开始,有三种移动方式:向上、向左、向左上。假设移动的过程中,i表示此时的行数,j表示此时的列数,同时用一个变量res来表示最长公共子序列。

第2步:如果dp[i][j]大于dp[i1][j]dp[i][j1],说明之前在计算dp[i][j]的时候,一定是选择了决策dp[i1][j1]+1,可以确定str1[i]等于str2[j],并且这个字符一定属于最长公共子序列,把这个字符放进res,然后向左上方移动。

第3步:如果dp[i][j]等于dp[i1][j],说明之前在计算dp[i][j]的时候,dp[i1][j1]+1这个决策不是必须选择的决策,向左上方移动即可。

第4步:如果dp[i][j]大于dp[i][j1],与第3步同理,向左方移动。

第5步:如果dp[i][j]同时等于dp[i1][j]dp[i][j1],向上还是向下无所谓,选择其中一个即可,反正不会有错过必须选择的字符。

也就是说:通过dp求解最长公共子序列的过程就是还原出当时如何求解dp的过程,来自哪个策略就朝哪个方向移动。
算法设计如下:

public String lcse(String str1, String str2) {

        if (str1 == null || str2 == null || "".equals(str1) || "".equals(str2)) {
            return "";
        }

        char[] chs1 = str1.toCharArray();
        char[] chs2 = str2.toCharArray();

        int[][] dp = getdp(chs1, chs2);

        int m = chs1.length - 1;
        int n = chs2.length - 1;

        char[] res = new char[dp[m][n]];
        int index = res.length - 1;
        while (index >= 0) {
            if (n > 0 && dp[m][n] == dp[m][n - 1]) {
                n--;
            } else if (m > 0 && dp[m][n] == dp[m - 1][n]) {
                m--;
            } else {
                res[index--] = chs1[m];
                m--;
                n--;
            }
        }

        return String.valueOf(res);

    }

完整代码

package com.bean.algorithmexec;

import java.io.FileNotFoundException;

public class LCS {

    /*
     * 最长公共子序列问题 给定两个字符串A、B,求A与B的最长公共子序列(子序列不要求是连续的) 举例: 字符串A: abcicba 字符串B:abdkscab
     * 其中:ab、abc、abca都是公共子序列,但是abca是最长公共子序列
     * 
     * 从文件读取输入: 1A2C3D4B56 B1D23CA45B6A
     * 
     * 输出: 123456
     * 
     * 或者 12C4B6都正确
     * 
     * (这里的算法其实存在一个缺陷)
     */

    public int[][] getdp(char[] str1, char[] str2) {
        int[][] dp = new int[str1.length][str2.length];

        dp[0][0] = (str1[0] == str2[0] ? 1 : 0);
        for (int i = 1; i < str1.length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
        }
        for (int j = 1; j < str2.length; j++) {
            dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0);
        }

        for (int i = 1; i < str1.length; i++) {
            for (int j = 1; j < str2.length; j++) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                if (str1[i] == str2[j]) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
                }
            }
        }

        return dp;
    }

    public String lcse(String str1, String str2) {

        if (str1 == null || str2 == null || "".equals(str1) || "".equals(str2)) {
            return "";
        }

        char[] chs1 = str1.toCharArray();
        char[] chs2 = str2.toCharArray();

        int[][] dp = getdp(chs1, chs2);

        int m = chs1.length - 1;
        int n = chs2.length - 1;

        char[] res = new char[dp[m][n]];
        int index = res.length - 1;
        while (index >= 0) {
            if (n > 0 && dp[m][n] == dp[m][n - 1]) {
                n--;
            } else if (m > 0 && dp[m][n] == dp[m - 1][n]) {
                m--;
            } else {
                res[index--] = chs1[m];
                m--;
                n--;
            }
        }

        return String.valueOf(res);

    }

    public static void main(String[] args) throws FileNotFoundException {
        // TODO Auto-generated method stub

        String aStr = "1A2C3D4B56"; // 读取A字符串
        String bStr = "B1D23CA45B6A"; // 读取B字符串
        System.out.println(aStr);
        System.out.println(bStr);

        LCS lcs = new LCS();
        String result = lcs.lcse(aStr, bStr);
        System.out.println(result);
    }

}

(完)

没有更多推荐了,返回首页