NJU-高级算法-最长公共子序列

本文详细介绍了一种基于动态规划的算法,用于寻找两个字符串之间的最长公共子序列,并提供了详细的实现步骤和Java代码示例。文章还解释了如何通过递归回溯找到所有可能的最长公共子序列。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Description

给定两个字符串,返回两个字符串的最长公共子序列(不是最长公共子字符串),可能是多个。

Input

输入第一行为用例个数, 每个测试用例输入为两行,一行一个字符串

Output

如果没有公共子序列,不输出,如果有多个则分为多行,按字典序排序。

Sample Input 1

1
1A2BD3G4H56JK
23EFG4I5J6K7

Sample Output 1

23G456K
23G45JK

 

思路

动态规划

1.确定状态

对于str1和str2两个字符串,它们长度分别为m,n,设一个dp[m+1][n+1]的矩阵,dp[i][j]表示字符串str1的前i个字符配上str2的前j个字符的最长公共子序列的长度,这里要注意的是比较str1的第i个字符和str2的第j个字符时是比较的str1.charAt(i-1)和str2.charAt(j-1),注意下标.

str1[i-1]!=str2[j-1]时,dp[i][j] = max(dp[i-1][j], dp[i][j-1])

str1[i-1]==str2[j-1]时,dp[i][j] = dp[i-1][j-1] + 1

其中第一种情况表示,str1的第i个字符或str2的第j个字符(或二者都)不在dp[i][j]所代表的最长公共子序列中.

第二种情况表示,str1的第i个字符和str2的第j个字符相同,则dp[i][j]等于其子问题dp[i-1][j-1]加上当前字符的长度1.

2.初始化

dp[0...i][0] = 0,dp[0][0...j] = 0.

无论是对于str1还是str2,只要对另一字符串不取任何字符,那么他们所组成的最长公共子序列长度都为0.

3.边界情况

dp[m][n]就是str1和str2的最长公共子序列长度

4.计算顺序

从左到右,自上而下填写dp矩阵

 

如果只要求最长公共子序列长度那么到这里就结束了,该题中还要求求出所有公共子序列。这里采用递归回溯。

从dp矩阵最右下角dp[m][n]开始往上、往左、往左上方回溯,用一个字符串数组resCharArray保存最长公共子序列,用一个集合resSet保存不同的最长公共子序列;

1.左上

dp[i][j] > dp[i-1][j-1],那么在计算dp[i][j]时一定是选择了dp[i-1][j-1]+1这个决策,即str1[i-1]==str2[j-1],该字符属于最长公共子序列,将该字符放至resCharArray中.

2.上

dp[i][j] == dp[i-1][j],说明计算dp[i][j]时,str1[i-1]不属于最长公共子序列,往上回溯.

3.左

dp[i][j] == dp[i][j-1],说明计算dp[i][j]时,str2[j-1]不属于最长公共子序列,往左回溯.

4.分别向上和向左

dp[i][j] == dp[i-1][j] && dp[i][j] == dp[i][j-1],说明计算dp[i][j]时,要么str1[i-1]不在最长公共子序列中,要么str2[j-1]不在最长公共子序列中,但二者子问题dp[i-1][j]和dp[i][j-1]的最长公共子字符串长度相同,即存在两种路径得到不同的最长公共子序列,此时分别往上和往左递归.

 

代码

import java.util.*;

public class Main {

    private static List<String> LCS(String str1, String str2) {
        char[] cstr1 = str1.toCharArray();
        char[] cstr2 = str2.toCharArray();
        int[][] dp = fillDP(cstr1, cstr2);
        int m = cstr1.length, n = cstr2.length;

        // maxLen为最长公共子序列长度
        int maxLen = dp[m][n];

        // resCharArray用于保存回溯得到的子序列
        char[] resCharArray = new char[maxLen];

        // resSet用于无重复地保存回溯得到的可能的多个最长公共子序列
        Set<String> resSet = new HashSet<String>();

        // 递归回溯
        getLCSString(cstr1, cstr2, m, n, dp, maxLen, resCharArray, resSet);

        // 将resSet中的所有结果转存到一个链表中以对这些最长公共子序列按字典序排序
        List<String> resList = new ArrayList<String>();
        for (String temp : resSet) {
            resList.add(temp.trim());
        }
        Collections.sort(resList);
        return resList;
    }

    private static int[][] fillDP(char[] str1, char[] str2) {
        int m = str1.length, n = str2.length;
        int[][] dp = new int[m + 1][n + 1];

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

        return dp;
    }

    private static void getLCSString(char[] str1, char[] str2, int i, int j, int[][] dp, int index, char[] resCharArray,
            Set<String> res) {

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

        // index初始值为最长公共子序列长度,将字符保存到结果数组中时注意现将index--再存入相应下标位置
        while (index > 0) {
            // dp[i][j]同时等于dp[i][j-1]和dp[i-1][j]时,说明此时有两种路径得到最长公共子序列
            if (i > 1 && j > 1 && dp[i][j] == dp[i][j - 1] && dp[i][j] == dp[i - 1][j]) {
                // 要分别往上(i-1)回溯,往左(j-1)回溯
                getLCSString(str1, str2, i - 1, j, dp, index, resCharArray, res);
                getLCSString(str1, str2, i, j - 1, dp, index, resCharArray, res);
                // 完成以上回溯已则经得到结果直接返回即可
                return;
            } else if (i > 1 && dp[i][j] == dp[i - 1][j]) {
                i--;
            } else if (j > 1 && dp[i][j] == dp[i][j - 1]) {
                j--;
            } else {
                index--;
                i--;
                j--;
                resCharArray[index] = str1[i];
            }
        }
        res.add(String.valueOf(resCharArray));
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int cases = Integer.parseInt(sc.nextLine());
        while (cases-- > 0) {
            String str1 = sc.nextLine();
            String str2 = sc.nextLine();

            List<String> res = LCS(str1, str2);
            for (String resString : res) {
                System.out.println(resString);
            }
        }
        sc.close();
    }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值