动态规划-最小编辑距离(Edit Distance)

继续动态规划系列案例讲解�C编辑距离,一个很有趣的算法。

问题:给定一个长度为m和n的两个字符串,设有以下几种操作:替换(R),插入(I)和删除(D)且都是相同的操作。寻找到转换一个字符串插入到另一个需要修改的最小(操作)数量。

PS:最短编辑距离算法右许多实际应用,参考Lucene的 API。另一个例子,对一个字典应用,显示最接近给定单词\正确拼写单词的所有单词。

找递归函数:

这个案例的子问题是什么呢?考虑寻找的他们的前缀子串的编辑距离,让我们表示他们为

[1 ... i]和[1 ....j] , 1<i<m 和1 <j <n

显然,这是解决最终问题的子问题,记为E(i,j)。我们的目标是找到E(m,n)和最小的编辑距离。

我们可以用三种方式: (i, -), (-, j) 和(i, j)右对齐两个前缀字符串。连字符符号( �C )表示没有字符。看一个例子或许会更清楚:

假设给定的字符串是 SUNDAY 和 SATURDAY。如果 i= 2 ,  j = 4,即前缀字符串分别是SU和SATU(假定字符串索引从1开始)。这两个字串最右边的字符可以用三种不同的方式对齐:

1  (i, j): 对齐字符U和U。他们是相等的,没有修改的必要。我们仍然留下其中i = 1和j = 3,即问题E(1,3)

2 (i, -) : 对第一个字符串右对齐,第二字符串最右为空字符。我们需要一个删除(D)操作。我们还留下其中i = 1和j = 4的 子问题 E(i-1,j)。

3 (-, j)  : 对第二个字符串右对齐,第一个字符串最右为空字符。在这里,我们需要一个插入(I)操作。我们还留下了子问题 i= 2 和 j = 3,E(i,j-1)。

对于这三种操作,我可以得到最少的操作为:

E(i, j) = min( [E(i-1, j) + D], [E(i, j-1) + I],  [E(i-1, j-1) + R (如果 i,j 字符不一样)] )

到这里还没有做完。什么将是基本情况?

当两个字符串的大小为0,其操作距离为0。当其中一个字符串的长度是零,需要的操作距离就是另一个字符串的长度. 即:

E(0,0)= 0,E(i,0)= i,E(0,j)= j

为基本情况。这样就可以完成递归程序了。

动态规划解法:

我们先计算出上面递归表达式的时间复杂度:T(m, n) = T(m-1, n-1) + T(m, n-1) + T(m-1, n) + C

T(M,N)的复杂性,可以通过连续替代方法或结二元齐次方程计算。结果是指数级的复杂度。

这是显而易见的,从递归树可以看出这将是一次又一次地解决子问题。

我们对重复子问题的结果打表存储,并在有需要时(自下而上)查找。

动态规划的解法时间复杂度为 O(mn) 正是我们打表的时间.

通常情况下,D,I和R操作的成本是不一样的。在这种情况下,该问题可以表示为一个有向无环图(DAG)与各边的权重,并且找到最短路径给出编辑距离。

实现代码如下:

给出了动态规划实现 和 递归实现。大家可以比较他们的效率差异。

// 动态规划实现 最小编辑距离
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
// 测试字符串
#define STRING_X "SUNDAY"
#define STRING_Y "SATURDAY"
#define SENTINEL (-1)
#define EDIT_COST (1)
inline
int min(int a, int b) {
   return a < b ? a : b;
}
// Returns Minimum among a, b, c
int Minimum(int a, int b, int c)
{
    return min(min(a, b), c);
}
// Strings of size m and n are passed.
// Construct the Table for X[0...m, m+1], Y[0...n, n+1]
int EditDistanceDP(char X[], char Y[])
{
    // Cost of alignment
    int cost = 0;
    int leftCell, topCell, cornerCell;
    int m = strlen(X)+1;
    int n = strlen(Y)+1;
    // T[m][n]
    int *T = (int *)malloc(m * n * sizeof(int));
    // Initialize table
    for(int i = 0; i < m; i++)
        for(int j = 0; j < n; j++)
            *(T + i * n + j) = SENTINEL;
    // Set up base cases
    // T[i][0] = i
    for(int i = 0; i < m; i++)
        *(T + i * n) = i;
    // T[0][j] = j
    for(int j = 0; j < n; j++)
        *(T + j) = j;
    // Build the T in top-down fashion
    for(int i = 1; i < m; i++)
    {
        for(int j = 1; j < n; j++)
        {
            // T[i][j-1]
            leftCell = *(T + i*n + j-1);
            leftCell += EDIT_COST; // deletion
            // T[i-1][j]
            topCell = *(T + (i-1)*n + j);
            topCell += EDIT_COST; // insertion
            // Top-left (corner) cell
            // T[i-1][j-1]
            cornerCell = *(T + (i-1)*n + (j-1) );
            // edit[(i-1), (j-1)] = 0 if X[i] == Y[j], 1 otherwise
            cornerCell += (X[i-1] != Y[j-1]); // may be replace
            // Minimum cost of current cell
            // Fill in the next cell T[i][j]
            *(T + (i)*n + (j)) = Minimum(leftCell, topCell, cornerCell);
        }
    }
    // 结果存储在 T[m][n]
    cost = *(T + m*n - 1);
    free(T);
    return cost;
}
// 递归方法实现
int EditDistanceRecursion( char *X, char *Y, int m, int n )
{
    // 基本情况
    if( m == 0 && n == 0 )
        return 0;
    if( m == 0 )
        return n;
    if( n == 0 )
        return m;
    // Recurse
    int left = EditDistanceRecursion(X, Y, m-1, n) + 1;
    int right = EditDistanceRecursion(X, Y, m, n-1) + 1;
    int corner = EditDistanceRecursion(X, Y, m-1, n-1) + (X[m-1] != Y[n-1]);
    return Minimum(left, right, corner);
}
int main()
{
    char X[] = STRING_X; // vertical
    char Y[] = STRING_Y; // horizontal
    printf("Minimum edits required to convert %s into %s is %d\n",
           X, Y, EditDistanceDP(X, Y) );
    printf("Minimum edits required to convert %s into %s is %d by recursion\n",
           X, Y, EditDistanceRecursion(X, Y, strlen(X), strlen(Y)));
    return 0;
}

http://www.acmerblog.com/dp5-edit-distance-4883.html

你可能感兴趣的:(java,字符串,实际应用,单mfc)