算法之动态规划2(LCS最长公共子序列, edit distance,交叉子串)

说过了装配站和矩阵链之后,这里再写三个跟字符串相关的,使用了动态规划算法的实例,一个是LCS,最长公共子序列,一个是Edit Distance,也就是字符串之间转换的代价。还有一个是interleaving string,也就是交叉子串的判断。


1 LCS,最长公共子序列,是评判两个字符串之间相关性的一个量度,它记录的是两个字符串中所有相等并且按照从前向后的顺序组合起来的字符的集合:

eg 对于s1 = "ABCFDRT" 和 s2 = "AGCFRL" 来说, 它们的最长公共子序列就是ACFR, 两个字符串之间的最长公共子序列可以并不唯一,下面就对这个问题剖析一下。

- 首先描述一个最长的公共子序列

假设字符串s1由 x1....xm构成,s2 由 y1...yn构成。s3作为LCS由z1...zk构成。

如果说xm == yn, 那么zk = xm = yn, 并且z1...zk-1所构成的一个序列,一定是s1(m-1)和s2(n-1)的LCS

如果xm != yn, 那么s3 应该是s1(m-1)和s2 的LCS 以及 s1 和 s2(n-1)的LCS中比较大的那一个。


- 由此引出了递归解

矩阵c[i,j]用来描述s1的前i个字符 与 s2的前j个字符,所对应的LCS的长度。

 

c[i,j] = 0 if i = 0 or j = 0
c[i,j] = c[i - 1, j- 1] +1, if xi = yj
c[i,j] = max(c[i-1,j], c[i,j-1]) if xi != yj

 


- 接下来计算LCS的长度

矩阵b[i,j]用来表述当前c[i,j]的值是来自哪里的,从而在最后用于重建LCS序列。

 

LCS-LENGTH(X,Y)
m = length(X)
n  = length(Y)
for i = 1 to m
     do c[i,0] = 0
for j = 1 to n
     do c[0,j] = 0
for i = 1 to m
     for j = 1 to n
          if X[i] == Y[j]
               then c[i,j] = c[i-1,j-1] + 1
                       b[i,j] = "diag"
          else if c[i-1,j] >= c[i,j-1]
               then c[i,j] = c[i-1,j]
                       b[i,j] = "up"
          else c[i,j] = c[i,j-1]
                        b[i,j] = "left"
return c and b

运行时间O(mn)

 


- 最后利用矩阵b构造一个LCS

 

PRINT-LCS(b,X,i,j)
if b[i,j] == "diag"
     then PRINT-LCS(b,X,i-1,j-1)
             print Xi
else if b[i,j] == "up"
     then PRINT-LCS(b,X,i-1,j)
else PRINT-LCS(b,X,i,j-1)

运行时间O(m+n)

 


注意到我们使用了两个矩阵c和b,额外的空间是2*m*n, 如果在构造LCS的时候,不用b的值,而是利用c中的值进行比较,也可以实现同样的效果,这时候额外的空间是m*n.

而如果我们只需要知道LCS的长度,不需要最后重建出LCS的话,那么所使用的空间可以进一步的减小,达到min(m,n)+常数空间。 这里假设n是比较小的那一个。

 

LCS-LENGTH(X,Y)
m = length(X)
n  = length(Y)
for j = 1 to n
     do c[j] = 0
for i = 1 to m
    do diag = 0
         left = 0
     for j = 1 to n
    do up = c[j]
          if X[i] == Y[j]
               then c[j] = diag + 1
          else if up >= left
               then c[j] = up
          else c[j] = left
          do diag = up
       left = c[j] 
return c



2 Edit Distance, 也叫Levenshtein Distance,是 Vladimir Levenshtein在1965年提出来的,主要衡量两个字符串之间转换的代价,已知转换可以是 插入 删除 或者替换,每个操作的代价都是1,给定两个字符串s1和s2,要求求出两者之间转换的最小代价。
根据题意,可以知道转换代价dist有如下的递推关系:
dist(i,j) = max(i,j)  if min(i,j) = 0, 如果任一字符串长度为0,那么转换代价就是另外一个字符串的长度。
dist(i,j)  = min( dist(i-1,j) + 1, 第一种情况,删除一个字符,代价为1
dist(i,j-1) + 1, 第二种情况,插入一个字符,代价为1
dist(i-1,j-1) + s1[i] == s2[j] 第三种情况,替换一个字符,同时取决于当前字符是否相等。
)
由此递归关系可以方便的写出一个动态规划的算法:
int LevenshteinDistance(string s1, string  s2){
    int m = s1.length();
    int n = s2.length();
    vector<vector<int>> dist(m+1,vector<int>(n+1,0));

  //  如果任一字符串长度为0,那么转换代价就是另外一个字符串的长度。
  for (int i = 1; i <= m;i++)
  {
      dist(i, 0) = i;
  }
  for (int j = 1; j <= n;j++)
  {
      dist(j, 0) = j;
  }
  for (int j  = 1; j <= n ; j++)
{
      for( int i = 1; i <= m ; i++)
     {
          if s1[i-1] = s2[j-1]   
            dist(i, j) = dist(i-1, j-1)       // 没有任何的代价
          else
            dist(i, j) = min
                    (
                      mins(d(i-1, j) + 1,  // a deletion
                      d(i, j-1) + 1),  // an insertion
                      d(i-1, j-1) + 1 // a substitution
                    )
        }
    }

  return d(m,n)
}

这里需要的空间是O(mn),可以通过进一步的优化达到动态规划算法空间的节省,为min(m,n)+常数
  int minDistance(string word1, string word2) {
        int l1 = word1.length();
        int l2 = word2.length();
        string& w1 = (l1< l2?word1:word2);//shorter - horizontal
        string& w2 = (l1< l2?word2:word1);//longer - vertical
        int length1 = w1.length();
        int length2 = w2.length();
        if(!length1) return length2;
        vector<int> dist(length1,0);
        for(int i = 0; i < length1; i++)
            dist[i] = i+1;
        int diag,left,up;
        for(int jj = 0; jj < length2; jj++)
        {
            diag = jj;
            left = jj+1;
            for(int ii = 0; ii < length1;ii++)
            {
                int upper = dist[ii];
                if(w1[ii] == w2[jj]) dist[ii] = diag;
                else dist[ii] = min(min(diag,left),upper) + 1;
                diag = upper;
                left = dist[ii];
            }
        }
        return dist[length1 - 1];
    }



3 交叉子串Interleaving string
给定字符串s1,s2和s3, 判断s3是不是由s1所交叉组成的。
比如s1 = "aabcc" , s2 = "dbbca"那么s3 = “aadbbcbcac”是交叉串,而"aadbbbaccc"不是交叉串。
考虑这个问题的时候,要从s3的构成入手,s3不断的从s1和s2中挑选字符,构成当前的s3,
假如说s3最后的一个字符与s2最后的一个字符相等,那么s3剩下的字符应该是s1和s2剩下的字符的一个交叉串。
而如果s3最后一个字符与s1最后一个字符相等,那么s3剩下的字符则是s2和s1剩下的字符的一个交叉串。这就形成了如下的递归关系:
isInterleaving(s1,len1,s2,len2,s3,len3)
 = (s3(len3-1) == s2(len2 -1)) && isInterleaving(s1,len1,s2,len2-1,s3,len3-1) ||
     (s3(len3 - 1) == s1(len1 - 1)) && isInterleaving(s1,len1-1,s2,len2,s3,len3-1)

利用二维的动态规划算法,可以有效的避免重复计算。
 bool isInterleave(string s1, string s2, string s3) {
        int m = s1.length(),n = s2.length(),k = s3.length();
         // 长度简单判断
        if(k != m+n) return false;
        vector<vector<int>> matrix(m+1,vector<int>(n+1,false));
        matrix[0][0] = true;
         // 在开头找s1和s3的重合部分,初始化
        for(int i = 1; i <= m; i++)
        {
            char c1 = s1[i-1];
            char c3 = s3[i-1];
            if(c1 == c3)
                matrix[i][0] = true;
            else break;
        }
          // 在开头找s2和s3的重合部分,初始化
        for(int j = 1; j <= n; j++)
        {
            char c2 = s2[j-1];
            char c3 = s3[j-1];
            if(c2 == c3)
                matrix[0][j] = true;
            else break;
        }
       
        for(int i = 1; i <= m; i++)
        {
            char c1 = s1[i-1];
            for(int j = 1; j <= n; j++)
            {
                char c2 = s2[j-1];
                char c3 = s3[i+j-1];
                if(c1 == c3)
                {
                    matrix[i][j] = matrix[i][j]||matrix[i-1][j]; //字符串s1向前退一步
                }
                if(c2 == c3)
                    matrix[i][j] = matrix[i][j]||matrix[i][j-1]; //字符串s2向前退一步
            }
        }
       
        return matrix[m][n];
    }

当然肯定也有一个一维的算法,更加的节省空间:

bool isInterleave(string s1, string s2, string s3) {          
    int m=s1.size(), n=s2.size(), k=s3.size();        
    if(m+n!= k) return false;

    //switch to save space later on.
    if(n>m)  {string tmp=s2; s2=s1; s1=tmp;}

    vector<bool> matrix(s2.size()+1, false);
    matrix[0]=true;

    for(int i=1; i<=s2.size(); i++) {
     {
            char c2 = s2[i-1];
            char c3 = s3[i-1];
            if(c2 == c3)
                matrix[i] = true;
            else break;
     }


    for(int i=1; i<=s1.size(); i++){
        matrix[0] = s1.substr(0, i)==s3.substr(0,i);
        for(int j=1; j<=s2.size(); j++)
        {
             if(s2[j-1] == s3[i+j-1])
                  matrix[j] = matrix[j-1];
             if(s1[i-1] == s3[i+j-1])
                  matrix[j] = matrix[j];       
        }
    }
    return matrix.back();
}




字符串问题中使用动态规划算法的还有很多,比如最长公共子串问题,就是在LCS的基础上稍微有了一些改变。




 

 

 

posted @ 2013-06-14 21:53  爱生活,爱编程  阅读(361)  评论(0编辑  收藏  举报