最长公共子序列的数量

时间:2012-03-24 16:35:51

标签: algorithm graph-theory dynamic-programming

我需要找到两个字符串A和B之间不同的最长公共子序列的数量。我目前正在使用正常的动态编程方法,然后使用回溯数组生成所有不同的子串,然后进行深度优先搜索从起始指数。

但是,由于可能的答案数量非常多,我的代码太慢了。有没有办法计算这些不同的最长公共子序列的数量而不实际生成它们?

到目前为止我的代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Stack;

class Node
{
String res = "";
int i;
int j;

public Node( int _i, int _j, String s )
{
    i = _i;
    j = _j;
    res = s;
}
}

public class LCSRevisited
{
static String a;
static String b;
static int m,n;
static int[][] memo;
static int[][] bt; // 1 means [i+1][j], 2 means [i][j+1], 3 means [i+1][j+1]
// 4  - means both

static HashSet <String> filter;

static void printAllStrings( )
{
    Iterator i = filter.iterator();

    while( i.hasNext())
    {
        System.out.println( i.next() );
    }
} 

 static void printSol()
 {
   System.out.print( memo[ 0 ][ 0 ]);

   // check how many UNIQUE such strings exist

   filter = new HashSet();
   Stack<Node> s = new Stack();
   Node start = new Node( 0, 0, "" );
   s.push( start );
   Node curr;
   String res;

   // use backtrack array to do a DFS

   while( !s.isEmpty() )
   {
        curr = s.pop();
        res = curr.res;

        if( ( curr.i>=m) || ( curr.j >=n ) )
        {
            filter.add( curr.res);
            continue;
       }

        // check backtrack value
        int i = curr.i;
        int j = curr.j;
        int back = bt[ i ][ j];

        if( back == 1 )
        {
            s.push( new Node( i+1, j, res ));
        }
        if( back == 2 )
        {
            s.push( new Node( i, j+1, res ));
        }
        if( back == 3 )
        {
            s.push( new Node( i+1, j+1, res+a.charAt(i) ));
        }
        if( back == 4 )
        {
            s.push( new Node( i, j+1, res ));
            s.push( new Node( i+1, j, res ));
        }
   }
   //printAllStrings();
   System.out.println(" " + filter.size() );
}

static void solve()
{
   // fill base cases
   m = a.length();
   n = b.length();
   memo = new int[ m+1 ][ n+1 ];
   Arrays.fill( memo[m], 0 );

   bt = new int[ m+1 ][ n+1 ];

   for( int i=0; i<m; i++ )
   {
       memo[ i ][ n ] = 0;    
   }

   // Now compute memo values
   for( int i=m-1; i>=0; i-- )
   {
       for( int j=n-1; j>=0; j-- )
       {
           if( a.charAt(i) == b.charAt(j))
           {
               memo[ i ][ j ] = 1 + memo[ i+1 ][ j+1 ];
               bt[ i ][ j ] = 3;
           }
           else
           {
               int r1 = memo[ i+1 ][ j ];
               int r2 = memo[ i ][ j+1 ];

               if( r1==r2 )
               {
                    memo[ i ][ j ] = r1;
                    bt[ i ][ j ] = 4;
               }
               else if( r1 > r2 )
               {
                   memo[ i ][ j ] = r1;
                   bt[ i ][ j ] = 1;
               }
               else
               {
                   memo[ i ][ j ] = r2;
                   bt[ i ][ j ] = 2;
               }
           }
       }
   }

   printSol();
 }

public static void main( String[] args ) throws IOException
{
 BufferedReader br = new BufferedReader( new InputStreamReader( System.in ));

int T= Integer.parseInt( br.readLine() );

while( T--> 0 )
{
    a = br.readLine();
    b = br.readLine();

    if( T>=1 )
    br.readLine();

    solve();
    // printArr( bt );
}
}
}

2 个答案:

答案 0 :(得分:1)

我想你可以使用像Rabin Karp这样的rolling hash function。这样,您可以计算较长公共子序列的新哈希值,而无需再次重新生成整个字符串并对其进行哈希处理。

实际上,我认为您可以使用纯DP来找到答案。假设您已经计算了LCS的DP表的值(我认为代码中为memo[][])。然后你可以像这样计算不同的LCS实例的数量

for j ← 0 to n do
    for i ← 0 to m do
        if i = 0 or j = 0 then
            D[i, j] ← 1
        else
            D[i, j] ← 0
            if ai  = bj  then
                D[i, j] ← D[i − 1, j − 1]
            else if L[i − 1, j] = L[i, j] then
                D[i, j] ← D[i, j] + D[i − 1, j]
            endif
            if L[i, j − 1] = L[i, j] then
                D[i, j] ← D[i, j] + D[i, j − 1]
            endif
            if L[i − 1, j − 1] = L[i, j] then
                D[i, j] ← D[i, j] − D[i − 1, j − 1]
            endif
        end if
    endfor
endfor

你的答案是D [n,m]。 希望我的想法得到帮助!

答案 1 :(得分:0)

也许使用一种特里可以帮助你在计算长度时生成实际序列,并在计算出trie中的遍历总数(线性时间)之后。

现在,memo [i] [j]表示A [i ... m]和B [j..n]的公共子序列的长度。我建议您还有一个lp [i] [j]表示每个指向trie中节点的指针列表,以便从该节点到trie根的路径为您提供A中最长的公共子句之一[我... m]和B [j..n]。要构建此lp,对于案例1和2,您只需复制lp [i + 1] [j]或lp [i] [j + 1]中的列表,两者都用于案例4,而对于3,则添加新节点值为A [i] = B [j]的树,并且在树中设置由lp [i + 1] [j + 1]指向的所有节点作为新节点的子节点。这些操作将是线性的(或者甚至更快,一些高级数据结构用于处理集合)。请注意,我所描述的实际上并不是一个trie / tree(一个节点可以有多个父节点)。

最后,为了计算,我认为遍历会很好,有一些额外的处理 - 传播计数:count [node] [level] = sum(count [sons(node)] [level-1])或,如果node是叶子数[node] [1] = 1,则count [node] [l!= 1] = 0。并且你的answear将为那些满足“最长”约束(\ sum_x {count [x] [l_max]})的“根”节点(in-degree 0)求和。

我不是100%确定我的解决方案,但它可能是改善问题答案的良好开端。