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

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

我们今天要探讨的动态规划问题来源于俄罗斯科学家Levenshtein提出的两个对象之间的不相似度,在音频、语言翻译等领域有广泛的应用。如果用于评估字符串之间的不相似度,那么又称为最小编辑距离MED(Minimum Edit Distance),它规定从string 1到转换成 string 2的最少操作数,最少操作数(MED)越大, 那么两个字符串相似度越低;最少操作数(MED)越小,那么两个字符串的相似度就越高,如果两个字符串完全相同,那么最小编辑距离值为0。

今天要解决的问题来源于Leecode 问题 72, 问题描述:

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

这是典型的动态规划问题,因为word 1和word 2可以划分为子问题,而且子问题与原问题有类似的解结构。为了证明其是动态规划问题,借助《算法导论》中的CRCC 分析流程,对此问题进行解决分析。

  1. 表征最优问题的结构(Characterize the structure of the optimal solution)

首先确定操作对象为word1当中的字符,需要确认将其转化为word2所使用的最少操作数(MED),那么对于word1中的每个字符(包括∅),针对word1中的第i个字符,和word2中的第j个字符,有两大类:

如果word1[i]和word[j]相等,那么只需要把各自的操作对象字符往前移动一位,对于实际的最少操作数(MED)没有影响。如果word1[i]和word[j]不相等,面临三个选项:

​ a.) 删除操作,删除word1中的当前字符i

​ b.) 替换/更新操作,对于word1中的当前i字符,使用word2中的第j个字符进行替代

​ c.) 插入操作,在第i个位置插入word2的第j个字符

需要操作的动作是,三个选项都需要尝试,然后采纳操作数最小的操作动作。我们不排斥三个选择当中的任何一个,我们要做的是try them and try THEM ALL, 这个思想类似穷举法,我们对所有的可能都进行判断,然后择优选择。这一点也是动态规划不同于贪心算法的关键,贪心算法每一步只选择某个最优项目,无需对所有选择进行判断,然后基于当前最优前景的选择,进行下一步的相关操作。所以动态规划类似撒网,贪心算法类似垂钓,两类问题在对待子问题的选择上不同。

  1. 递归定义最优解的值

接下来来到最关键的一步,通过递归来定义最优解的值,假定word1=, word2=,我们定义数组dp[i] [j]来定义的最少操作数(MED),对于最少操作数的矩阵dp[i] [j],可以有下面的选择:

a.)如果x[i]==y[j],那么dp[i] [j]=dp[i-1] [j-1], 也就是说,如果二者相等,的最少操作数(MED)等于的最少操作数(MED)。
d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] , w h e n   t h e   x [ i ]   e q u a l s   t o   y [ j ] dp[i][j]=dp[i-1][j-1], when\ the\ x[i]\ equals\ to\ y[j] dp[i][j]=dp[i1][j1],when the x[i] equals to y[j]

b.)如果x[i]≠y[j],这时候需要从删除、插入、更新三个选择中选择最小的最少操作数(MED),况且这三个选择的代价相同,无论选择哪一个,其最少操作数(MED)都需要追加1。

​ 接下来我们用公式表示这三个选择。
d p [ i ] [ j ] = m i n { d p [ i − 1 ] [ j ] + 1 , d p [ i ] [ j − 1 ] + 1 , d p [ i − 1 ] [ j − 1 ] + 1 } dp[i][j]=min\left\{dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+1\right\} dp[i][j]=min{dp[i1][j]+1,dp[i][j1]+1,dp[i1][j1]+1}
dp[i-1] [j]+1表示假定选择删除第word1中的第i个字符,作为最少操作数(MED)的情况,相当于word1指向i字符的指针先向前移动1位,同时由于word2中的第j位字符还没有进行任何比较(删除word1[i]后,无法进行比较),所以其指针位置不变化。

dp[i][j-1]+1表示假定在word1的i字符之后插入与word2当中的第j个字符,那么这个时候相当于word1的i指针没有变化,当时word2[j]由于已经找到匹配(与word1中的插入字符相同),那么就需要把word2的j指针向前移动1位。

dp[i-1][j-1]+1表示假定把word1中的第i个字符替换为与第j个字符相同,那么这时候,word1中的指针i和word2当中的指针j都需要往前移动1位。

上述三个情况完成后,需要选择三类操作中最小的最少操作数,作为当前dp[i][j]的最少操作数(MED)的值。如果word1字符串为空,那么这时候就需要追加word2当前的字符串长度至word1当中,插入的长度等于当前word2的长度;如果word2为空串,那么就需要删除word1当中的所有字符串,确保二者一致,删除的长度等于当前word1的长度。

为了能更简洁阐述上述观点,采用下列表格对dp[i][j]的变化过程进行说明;
动态规划问题-最小编辑距离(Minimum Edit Distance)_第1张图片

我们现在假定需要求解DP[1][2]的值(深蓝色单元格),具体可以理解为word1=‘∅h’, word2='∅ro’情况下最小操作数。

由于word1[1]!=word2[2],所以我们需要在删除、替换、更新三个策略中选择结算的最小操作数(MED)选取最小的值,这时候我们有:

DP[1][2]=DP[0][2]+1=2+1=3,我们用图示来说明具体操作路径:

  • 删除操作

动态规划问题-最小编辑距离(Minimum Edit Distance)_第2张图片

如橙色箭头所示,首先在word1插入’r’,word1变为’∅rh’,最少操作数为1; 接着在word1中插入’o’,word1变为’∅roh’,最少操作数为2(=1+1);最后我们删除’h’,word1变为’∅ro’,完成操作(最少操作数为3),在此过程中,最少操作数总数为3,为了方便,抽象为:

D[i][j]=D[i-1][j]+1, 最后一步操作表示删除word1中的第i个元素,我们可以得到当前D[i][j]值为这个数字,值得一提的是,一定要强调当前,因为这个数字不是最终的D[i][j]的数字。回到前文的示例,我们有当前的D[1][2]的具体值。

D[1][2]=D[0][2]+1=2+1=3;

  • 替换/更新操作

动态规划问题-最小编辑距离(Minimum Edit Distance)_第3张图片

具体过程解析为,在word1的∅后面先插入’r’, word1字符转换称为 ‘∅rh’,最小操作数为1;接着我们替换word1中的’h’为’o’,word1转换为’∅ro’,最小操作数为2(1+1).

最后一步的操作可以表示为:

DP[i][j]=DP[i-1][j-1]+1,此步骤表示替换操作,具体到上面的例子中,我们可以看到,当前的DP[1][2]的值可以表示为:

DP[1][2]=DP[0][1]+1=1+1=2;

  • 插入操作
    动态规划问题-最小编辑距离(Minimum Edit Distance)_第4张图片

第一步操作,替换word1(‘∅h’)中的’h’为’r’,转换word1为(‘∅r’),此步骤的最小操作数1;

第二操作,在当前的word1(‘∅r’)后面追加’o’,转换word1为(‘∅ro’),此步骤的最小操作数为2(1+1), 其中最后一步的操作可以抽象为:

DP[i][j]=DP[i][j-1]+1, 表示插入操作,具体到当前的实际例子中,我们有:

DP[1][2]=DP[1][1]+1=1+1=2

截止到目前位置,我们分别对删除、替换/更新、插入三个选择进行计算,汇总结果:

DP_Delete=3;

DP_Update=2;

DP_Insert=2;

DP[1][2]=min{DP_Delete, DP_Update,DP_Insert}=2;

在此示例中,如果仔细分析,可以发现DP_Update 和DP_Insert均可以取得相同的最小操作数,实际上这两种结果都是合法的结果,所以我们可以所,最小编辑距离(MED)的具体的解选择并不唯一。

回到最初的问题,如果要求解word1='∅horse’和word2='∅ros’二者字符串的最小操作数,我们沿着图示中蓝色方块进行,就可以求得结果。

动态规划问题-最小编辑距离(Minimum Edit Distance)_第5张图片

  1. 计算最优解的值

完成上述定义后,计算最优解的值过程就相对就比较简单。递归或者迭代都是可以选择的程序构建方法。本例中我们采用两个方法进行说明。

在具体执行程序之前,需要特别提到的是,MED问题属于选择性的子问题,不同于Rod-cutting或者Matrix-Chain-Multiplication问题,不需要对前面所有的子问题进行评估,只需要评估当前以及当前临界的问题即可,所以如果采用递归流程,在程序中就可以省去循环过程,只需要多个不同的判断分支即可解决问题。这是MED问题不同于rod-cutting和Matrix-chain-multiplication的问题的关键地方。我们先采用bottom-up的方法计算求解过程。

a-1) 头文件定义

/**
 * @file Edit_Distance.h
 * @author your name ([email protected])
 * @brief 
 * @version 0.1
 * @date 2023-02-25
 * 
 * @copyright Copyright (c) 2023
 * 
 */
#ifndef EDIT_DISTANCE_H
#define EDIT_DISTANCE_H
#include 
#include 
#include 

#define M 5
#define N 3

/**
 * @brief Fid the minimum operation times by changing the source string to target source
 * 
 * @param source Source string to be updated/changed
 * @param target Target string we need pay attention to
 * @param m Length of source string
 * @param n Length of target string
 * @param DP To store the min_edit_distance 
 */
void min_edit_distance(char *source,char *target, int m, int n, int DP[M+1][N+1]);

/**
 * @brief Find the minimum opeation times from three types
 *
 * @param num_delte Number of deltetion
 * @param num_insert Number of insertion
 * @param num_update Number of updating
 * @return int -Return the minimum number of three type of operatoin
 */
int min(int num_delete, int num_insert, int num_update);

#endif

b-1) 函数实施

/**
 * @file Edit_Distance.c
 * @author your name ([email protected])
 * @brief 
 * @version 0.1
 * @date 2023-02-25
 * 
 * @copyright Copyright (c) 2023
 * 
 */
#ifndef EDIT_DISTANCE_C
#define EDIT_DISTANCE_C
#include "Edit_Distance.h"

void min_edit_distance(char *source, char *target, int m, int n, int DP[M + 1][N + 1])
{
    int i;
    int j;
    int num_delete;
    int num_update;
    int num_insert;
	
    //If source string is not empty while target string is empty
    //just delete the current length of source string from the source string
    for(i=0;i<=m;i++)
    {
        DP[i][0]=i;
    }
	  //If source string is empty while target string is not empty
    //just insert the current length of source string into the source string
    for(i=0;i<=n;i++)
    {
        DP[0][i]=i;
    }

    for(i=1;i<=m;i++)
    {
        for(j=1;j<=n;j++)
        {
            if(source[i]==target[j])
            {
                //no operatoin needed if both are source[i]==target[j] equal
                DP[i][j]=DP[i-1][j-1]; 
            }
            else
            {
                //Given three choices, we need to evaluate the minimum one
                //Follow the rule: try them and try them ALL
                num_delete=DP[i-1][j]+1;
                num_insert=DP[i][j-1]+1;
                num_update=DP[i-1][j-1]+1;
                DP[i][j]=min(num_delete,num_insert,num_update);
            }
        }
    }

    return;
}

int min(int num_delete, int num_insert, int num_update)
{
    int min_value;
    
    min_value=(num_delete<num_insert?num_delete:num_insert);
    min_value=(num_update<min_value?num_update:min_value);

    return min_value;
}

#endif

c-1) 测试主函数

/**
 * @file Edit_Distance_Main.c
 * @author your name ([email protected])
 * @brief 
 * @version 0.1
 * @date 2023-02-25
 * 
 * @copyright Copyright (c) 2023
 * 
 */
#ifndef EDIT_DISTANCE_MAIN_C
#define EDIT_DISTANCE_MAIN_C
#include "Edit_Distance.c"

int main(void)
{
    int m;
    int n;
    int DP[M+1][N+1];
    //'#' indicate the string is empty and equals to '∅'
    char source[] = {'#', 'h', 'o', 'r', 's', 'e'};
    char target[] = {'#', 'r', 'o', 's'};
    m=M;
    n=N;

    min_edit_distance(source,target,m,n,DP);

    printf("The minimum edit distance is %d\n",DP[m][n]);

    getchar();
    return EXIT_SUCCESS;
}
#endif

对于递归程序,我们这里仅呈现函数的实现,头文件和测试问题件,我们就不一一罗列了。

/**
 * @file Edit_Distance.c
 * @author your name ([email protected])
 * 采用递归形式,实现MED计算
 * @version 0.1
 * @date 2023-02-25
 *
 * @copyright Copyright (c) 2023
 *
 */
#ifndef EDIT_DISTANCE_C
#define EDIT_DISTANCE_C
#include "Edit_Distance.h"

int min_edit_distance(char *source, char *target, int m, int n, int DP[M + 1][N + 1])
{
    int i;
    int j;

    for(i=0;i<=m;i++)
    {
        for(j=0;j<=n;j++)
        {
            DP[i][j]=-1;
        }
    }

    return min_edit_distance_aux(source, target,m,n,DP);
}

int min_edit_distance_aux(char *source, char *target, int m, int n, int DP[M + 1][N + 1])
{
    int num_delete;
    int num_insert;
    int num_update;
    int dist;

    if (DP[m][n] !=-1)
    {
        return DP[m][n];
    }

   
    if(m==0)
    {
        DP[m][n]=n;
    
    }
    else if(n==0)
    {
        DP[m][n]=m;
    }
    else
    {
        if(source[m]==target[n])
        {
            DP[m][n] = min_edit_distance_aux(source,target,m-1,n-1,DP);
        }
        else
        {
            num_delete = min_edit_distance_aux(source, target, m-1, n, DP)+1;
            num_insert = min_edit_distance_aux(source, target, m, n-1, DP)+1;
            num_update = min_edit_distance_aux(source, target, m-1, n-1, DP)+1;
            DP[m][n]=min(num_delete,num_insert,num_update);
        }
    }
    return DP[m][n];
}


int min(int num_delete, int num_insert, int num_update)
{
    int min_value;
    
    min_value=(num_delete<num_insert?num_delete:num_insert);
    min_value=(num_update<min_value?num_update:min_value);

    return min_value;
}

#endif
  1. 构建最优解的信息

对于如何构建最优解的路径,我们可以设定另外数组 P[i][j]进行记录,其选择值可以为’D’(Delete), ‘U’(Update)和’I’(Insert)三个值之一,从尾部到头部的方法从(递归)遍历,然后打印出操作方法即可,在此略过。

  1. 总结:

对于此动态规划问题,在解决之前将近花了一天时间进行思考,很多时候走入思维的误区,最大的误区是,希望对每一步计算之前,通过word1[i]和word2[j]的先行比较,从而选择Delete, Update或Insert操作之一。其实这个思维过程已经违反了动态遍历的基本思维: Try them and try them all. 后来又复习了《算法导论》中LCS的相关章节,才豁然开朗,打开了思路。

另外碰到的思维难点,在于递归的结束条件或迭代的其实条件的计算,其实过程中没有抽象到word1等于’∅’或word2’∅’的情况,进入死胡同,无法结束递归或开启迭代的相应计算。

其三,通过反思和比较,更进一步体会到了有条件选择和全部选择的不同,主要是MED问题和rod-cutting问题的比较,在rod-cutting问题中,我们需要对{pi+r(n-i)}的所以切割位置进行循环判断,然后才能得到最优的值。而此问题实际上只需要对当前问题的邻接问题进行判断即可,也即: 上方,左方,左上方的值进行判断即可,对于左上方的值,需要通过字符比较,确认选择的代价是0还是1。

DP[i-1][j-1] DP[i-1][j]
DP[i][j-1] DP[i][j]

参考资料:

  1. 《Introduction to algorithm 4ed》

  2. Leecode 问题 #72

你可能感兴趣的:(动态规划,算法,c语言)