好好看看这个问题,如果我们定义一个成本函数来告诉我们两个字符串之间的距离,我们就有两个考虑三种自然类型的变化:
替换 - 将模式“s”中的单个字符更改为文本“t”中的不同字符,例如将“shot”更改为“spot”。
插入 - 将单个字符插入模式“s”以帮助它匹配文本“t”,例如将“ago”更改为“agog”。
删除 - 从模式“s”中删除单个字符以帮助其匹配文本“t”,例如将“hour”更改为“our”。
当我们将每个操作设置为花费一步时,我们定义了两个字符串之间的编辑距离。那么我们如何计算呢?
我们可以通过观察字符串中的最后一个字符必须匹配、替换、插入或删除来定义递归算法。在最后一次编辑操作中删除字符会留下一对操作留下一对更小的字符串。令 i 和 j 分别是 和 t 的相关前缀的最后一个字符。最后一次操作后有三对较短的字符串,对应匹配/替换、插入或删除后的字符串。如果我们知道编辑三对较小字符串的成本,我们可以决定哪个选项会导致最佳解决方案并相应地选择该选项。我们可以通过递归这个很棒的东西来了解这个成本:
#define MATCH 0 /* enumerated type symbol for match */
#define INSERT 1 /* enumerated type symbol for insert */
#define DELETE 2 /* enumerated type symbol for delete */
int string_compare(char *s, char *t, int i, int j)
{
int k; /* counter */
int opt[3]; /* cost of the three options */
int lowest_cost; /* lowest cost */
if (i == 0) return(j * indel(’ ’));
if (j == 0) return(i * indel(’ ’));
opt[MATCH] = string_compare(s,t,i-1,j-1) +
match(s[i],t[j]);
opt[INSERT] = string_compare(s,t,i,j-1) +
indel(t[j]);
opt[DELETE] = string_compare(s,t,i-1,j) +
indel(s[i]);
lowest_cost = opt[MATCH];
for (k=INSERT; k<=DELETE; k++)
if (opt[k] < lowest_cost) lowest_cost = opt[k];
return( lowest_cost );
}
这个算法是正确的,但也慢得不可思议。
在我们的计算机上运行时,比较两个 11 个字符的字符串需要几秒钟,然后计算就消失了,再也不会出现在任何事情上。
为什么算法这么慢?它需要指数级的时间,因为它一次又一次地重新计算值。在字符串中的每个位置,递归都以三种方式分支,这意味着它以至少 3^n 的速度增长——实际上,甚至更快,因为大多数调用只减少两个索引中的一个,而不是两个索引。
那么我们怎样才能使算法实用呢? 重要的观察是,这些递归调用中的大多数都在计算之前已经计算过的东西。我们怎么知道?好吧,只能有|s| · |t|可能的唯一递归调用,因为只有那么多不同的 (i, j) 对作为递归调用的参数。
通过将每个 (i, j) 对的值存储在一个表中,我们可以
避免重新计算它们,只看
根据需要添加它们。
该表是一个二维矩阵 m,其中每个 |s|·|t|单元格包含该子问题的最优解的成本,以及解释我们如何到达该位置的父指针:
typedef struct {
int cost; /* cost of reaching this cell */
int parent; /* parent cell */
} cell;
cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */
动态编程版本与递归版本有三个不同。
首先,它使用表查找而不是递归调用来获取中间值。
**第二,**它更新每个单元格的父字段,这将使我们能够在以后重建编辑序列。
**Third,**Third,它使用更通用的目标cell() 函数进行检测,而不仅仅是返回 m[|s|][|t|].cost。这将使我们能够将此例程应用于更广泛的问题。