如何找到所有最长公共子序列集合

如何找到所有最长公共子序列集合

1. 问题描述:

一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切的说,若给定序列X={x1,x2,…,xm},则另一序列Z={z1,z2,…,zk},X的子序列是指存在一个严格递增下标序列{i1,i2,…,ik}使得对于所有j=1,2,…k有zj=xij 例如,序列Z={B,C,D,B}是序列X={A,B,C,B,D,A,B}的子序列,相应的递增下标序列为{2,3,5,7}.
给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列,公共子序列可以有多个,最长公共子序列是X和Y序列的最长的一个.
如何找到所有最长公共子序列集合_第1张图片

2. 解决

计算最长公共子序列的长度根据上面的式子来算就行了,但是两个序列的最长公共子序列不止一个,所以难点是如何得到它们,不难猜到,需要用回溯法来解决。具体做法是在计算长度的时候,用一个数组保存当前点是从哪个状态转移过来的。

如果是Xi == Yj,那就是从c[i-1][j-1]转移过来的,记为"↖"
如果是c[i-1][j] > c[i][j-1],那就是从c[i-1][j]转移过来的,记为"↑"
如果是c[i-1][j] < c[i][j-1],那就是从c[i][j-1]转移过来的,记为"←"
如果是c[i-1][j] == c[i][j-1],那两个状态都可以转移过来,记为"┘"

利用回溯法从右下角的节点的开始,根据节点存储的状态返回到上一个状态,如同根据方向走路一样。。如果是“↖”的状态表示是有一个Xi == Yj,将其追加到一个字符串lcs后面,如此反复,结束条件就是走到了边界,这时需要判断lcs的长度是否是最长的长度,如果是最长的,那就找到了一个保存下来,这个回溯就走到了终点,返回即可。

3. Java代码如下

public class Test {

    public static void main(String[] args) {
        String a = "XBACBDEAB";
        String b = "BXCDEAB";
        System.out.println(LCS(a,b));

    }

    public static int LCS(String a, String b) {
        int n = a.length(), m = b.length();
        //c[i][j]表示a长度为i和b长度为j时的最长公共子序列长度
        int[][] c = new int[n+1][m+1];
        //d[i][j]表示方向
        char[][] d = new char[n+1][m+1];
        for(int i = 1; i <= n; ++i) {
            for(int j = 1; j <= m; ++j) {
                if(a.charAt(i-1) == b.charAt(j-1)) {
                    c[i][j] = c[i-1][j-1] + 1;
                    //左上
                    d[i][j] = '↖';
                } else if(c[i-1][j] > c[i][j-1]){
                    c[i][j] = c[i-1][j];
                    //上
                    d[i][j] = '↑';
                } else if(c[i-1][j] < c[i][j-1]){
                    c[i][j] = c[i][j-1];
                    //左
                    d[i][j] = '←';
                } else {
                    c[i][j] = c[i][j-1];
                    //可向左可向右
                    d[i][j] = '┘';
                }
                System.out.print(j == m ? c[i][j] + "\n":c[i][j] + " ");
            }
        }
        for(int i = 0; i <= n; ++i){
            for(int j = 0; j <= m; ++j) {
                System.out.print(j == m ? d[i][j] + "\n":d[i][j] + " ");
            }
        }
        String lcs = "";
        Set lcsSet = new HashSet<>();
        backTrace(d,a,lcs,n,m,c[n][m],lcsSet);
        for(String s: lcsSet) {
            System.out.println(s);
        }
        return c[n][m];
    }

    public static void backTrace(char[][] d, String a, String lcs, int i , int j, int maxSublen, Set lcsSet){
        if(i == 0 || j == 0) {
            StringBuilder sb = new StringBuilder(lcs);
            lcs = sb.reverse().toString();
            //可能有些提早出来了,一定要判断长度是最长的,但是这样还是会有重复的字符串,所以还要做去重处理
            if (lcs.length() == maxSublen) {
                lcsSet.add(lcs);
            }
            return;
        }

        switch (d[i][j]){
            case '↖':
                lcs += a.charAt(i-1);
                backTrace(d,a,lcs,i-1,j-1,maxSublen,lcsSet);
                break;
            case '↑':
                backTrace(d,a,lcs,i-1,j,maxSublen,lcsSet);
                break;
            case '←':
                backTrace(d,a,lcs,i,j-1,maxSublen,lcsSet);
                break;
            case '┘':
                backTrace(d,a,lcs,i-1,j,maxSublen,lcsSet);
                backTrace(d,a,lcs,i,j-1,maxSublen,lcsSet);
                break;
            }
    }

}

你可能感兴趣的:(算法)