动态规划问题-最小编辑距离(Minimum Edit Distance)
我们今天要探讨的动态规划问题来源于俄罗斯科学家Levenshtein提出的两个对象之间的不相似度,在音频、语言翻译等领域有广泛的应用。如果用于评估字符串之间的不相似度,那么又称为最小编辑距离MED(Minimum Edit Distance),它规定从string 1到转换成 string 2的最少操作数,最少操作数(MED)越大, 那么两个字符串相似度越低;最少操作数(MED)越小,那么两个字符串的相似度就越高,如果两个字符串完全相同,那么最小编辑距离值为0。
今天要解决的问题来源于Leecode 问题 72, 问题描述:
给你两个单词
word1
和word2
, 请返回将word1
转换成word2
所使用的最少操作数 。你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
这是典型的动态规划问题,因为word 1和word 2可以划分为子问题,而且子问题与原问题有类似的解结构。为了证明其是动态规划问题,借助《算法导论》中的CRCC 分析流程,对此问题进行解决分析。
- 表征最优问题的结构(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, 这个思想类似穷举法,我们对所有的可能都进行判断,然后择优选择。这一点也是动态规划不同于贪心算法的关键,贪心算法每一步只选择某个最优项目,无需对所有选择进行判断,然后基于当前最优前景的选择,进行下一步的相关操作。所以动态规划类似撒网,贪心算法类似垂钓,两类问题在对待子问题的选择上不同。
- 递归定义最优解的值
接下来来到最关键的一步,通过递归来定义最优解的值,假定word1=<x1,x2,x3.xi…xm>, word2=<y1,y2,y3…yj.yn>,我们定义数组dp[i] [j]来定义<x1,x2,x3…xi>和<y1,y2,y3…yj>的最少操作数(MED),对于最少操作数的矩阵dp[i] [j],可以有下面的选择:
a.)如果x[i]==y[j],那么dp[i] [j]=dp[i-1] [j-1], 也就是说,如果二者相等,<x1,x2,x3…xi>和<y1,y2,y3…yj>的最少操作数(MED)等于<x1,x2,x3…xi-1>和<y1,y2,y3…yj-1>的最少操作数(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[i−1][j−1],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[i−1][j]+1,dp[i][j−1]+1,dp[i−1][j−1]+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]的变化过程进行说明;
我们现在假定需要求解DP[1][2]的值(深蓝色单元格),具体可以理解为word1=‘∅h’, word2='∅ro’情况下最小操作数。
由于word1[1]!=word2[2],所以我们需要在删除、替换、更新三个策略中选择结算的最小操作数(MED)选取最小的值,这时候我们有:
DP[1][2]=DP[0][2]+1=2+1=3,我们用图示来说明具体操作路径:
- 删除操作
如橙色箭头所示,首先在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;
- 替换/更新操作
具体过程解析为,在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;
-
插入操作
第一步操作,替换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’二者字符串的最小操作数,我们沿着图示中蓝色方块进行,就可以求得结果。
- 计算最优解的值
完成上述定义后,计算最优解的值过程就相对就比较简单。递归或者迭代都是可以选择的程序构建方法。本例中我们采用两个方法进行说明。
在具体执行程序之前,需要特别提到的是,MED问题属于选择性的子问题,不同于Rod-cutting或者Matrix-Chain-Multiplication问题,不需要对前面所有的子问题进行评估,只需要评估当前以及当前临界的问题即可,所以如果采用递归流程,在程序中就可以省去循环过程,只需要多个不同的判断分支即可解决问题。这是MED问题不同于rod-cutting和Matrix-chain-multiplication的问题的关键地方。我们先采用bottom-up的方法计算求解过程。
a-1) 头文件定义
/**
* @file Edit_Distance.h
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-02-25
*
* @copyright Copyright (c) 2023
*
*/
#ifndef EDIT_DISTANCE_H
#define EDIT_DISTANCE_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 (you@domain.com)
* @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 (you@domain.com)
* @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 (you@domain.com)
* 采用递归形式,实现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
- 构建最优解的信息
对于如何构建最优解的路径,我们可以设定另外数组 P[i][j]进行记录,其选择值可以为’D’(Delete), ‘U’(Update)和’I’(Insert)三个值之一,从尾部到头部的方法从(递归)遍历,然后打印出操作方法即可,在此略过。
- 总结:
对于此动态规划问题,在解决之前将近花了一天时间进行思考,很多时候走入思维的误区,最大的误区是,希望对每一步计算之前,通过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] |
参考资料:
-
《Introduction to algorithm 4ed》文章来源:https://www.toymoban.com/news/detail-492794.html
-
Leecode 问题 #72文章来源地址https://www.toymoban.com/news/detail-492794.html
到了这里,关于动态规划问题-最小编辑距离(Minimum Edit Distance)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!