我们把两个字符串的相似度定义为:将一个字符串转换成另外一个字符串的代价(转换的方法可能不唯一),转换的代价越高则说明两个字符串的相似度越低。比如两个字符串:“SNOWY”和“SUNNY”,下面给出两种将“SNOWY”转换成“SUNNY”的方法:
变换1:
S - N O W Y
S U N N - Y
Cost = 3 (插入U、替换O、删除W)
变换2:
- S N O W - Y
S U N - - N Y
Cost = 5 (插入S、替换S、删除O、删除W、插入N)
分析问题
我们可以把这种相似度理解为:把一个字符串(source)通过“插入、删除和替换”这样的编辑操作变成另外一个字符串(target)所需要的最少编辑次数,也就是两个字符串之间的编辑距离(edit distance)。能否给出一个算法,求解任意两个字符串之间的编辑距离?
从题目给出的例子可知,将一个字符串经由插入、删除或替换等操作转换成另外一个字符串的方法不止一种,所需要做的编辑次数也不相同,如果有某种方法能够用最小的修改次数完成转换,这种方法的编辑次数就是我们要求的编辑距离。很显然,这是个求最优解的问题。
提到最优解问题,首先可以考虑使用贪婪法,但是本题显然是一个多阶段决策类型的最优解问题,对source字符串做最小的修改变换到target字符串,需要在处理过程中的每个阶段都选择修改最小的方式,但是本题中每个阶段之间都不是孤立的,受到前面已经确定的决策和后面可选的决策共同影响,无法通过对每一次决策的最优决策简单堆叠出最后的最优结果,因此,排除贪婪法。
动态规划法(Dynamic Programming)
对于多阶段决策类型的问题,应该优先考虑动态规划法(Dynamic Programming, DP)。动态规划法是解决多阶段决策最优化问题常用的一种思想方法[1],也是所有解题方法中最抽象的一种方法。使用动态规划法解决问题的关键有两点,一点是定义子问题的最优子结构【注解1】,另一点是确定子问题最优解的堆叠方式。定义最优子结构就是分解子问题,可以用递推的方式,也可以用递归的方式,基本原则就是将问题分成M个子问题,同时确定每个子问题的最优解与其它N(N小于M)个子问题之间的关系。子问题最优解的堆叠方式是指最优决策序列和它的子序列的递推关系,包括子问题最优解的递推关系和边界值两部分。对于一个问题,如果能够找最优子结构的定义方式(包括子问题之间的关系)和子问题最优解的堆叠方式,并且每个子问题最优解都满足无后效性【注解2】,则该问题就可以尝试用动态规划法解决这个问题。
以本题为例,假设source字符串有n个字符,target字符串有m个字符,如果将问题定义为求解将source的1-n个字符转换为target的1-m个字符所需要的最少编辑次数(最小编辑距离),则其子问题就可以定义为将source的1-i个字符转换为target的1-j个字符所需要的最少编辑次数,这就是本问题的最优子结构。我们用d[i, j]表示source[1..i]到target[1..j]之间的最小编辑距离,则计算d[i, j]的递推关系可以这样计算出来:
如果source[i] 等于target[j],则:
d[i, j] = d[i, j] + 0 (递推式 1)
如果source[i] 不等于target[j],则根据插入、删除和替换三个策略,分别计算出使用三种策略得到的编辑距离,然后取最小的一个:
d[i, j] = min(d[i, j - 1] + 1,d[i - 1, j] + 1,d[i - 1, j - 1] + 1 ) (递推式 2)
d[i, j - 1] + 1 表示对source[i]执行插入操作后计算最小编辑距离
d[i - 1, j] + 1 表示对source[i]执行删除操作后计算最小编辑距离
d[i - 1, j - 1] + 1表示对source[i]替换成target[i]操作后计算最小编辑距离
d[i, j]的边界值就是当target为空字符串(m = 0)或source为空字符串(n = 0)时所计算出的编辑距离:
m = 0,对于所有 i:d[i, 0] = i
n = 0,对于所有 j:d[0, j] = j
注解:
【1】最优子结构:对于多阶段决策问题,如果每一个阶段的最优决策序列的子序列也是最优的,且决策序列具有“无后效性”,就可以将此决策方法理解为最优子结构。
【2】无后效性:动态规划法的最优解通常是由一系列最优决策组成的决策序列,最优子结构就是这些最优决策序列中的一个子序列,对于每个子序列再做最优决策会产生新的最优决策(子)序列,如果某个决策只受当前最优决策子序列的影响,而不受当前决策可能产生的新的最优决策子序列的影响,则可以理解这个最优决策具有无后效性。
根据前面分析的最优子结构、最优解的递推关系以及边界值,写出用动态规划法求解最小编辑距离的算法就很容易了,以下代码就是计算两个字符串的最小编辑距离的算法实现:
public class MinDistance {
/*注意:source和target字符串的长度不能超过d矩阵的限制*/
static int EditDistance(String source, String target)
{
int maxLen=Math.max(source.length(), target.length());
int i,j;
int d[][] = new int[maxLen+1][maxLen+1];
/* d[i, j]的边界值就是当target为空字符串(m = 0)或source为空字符串(n = 0)时所计算出的编辑距离:
m = 0,对于所有 i:d[i, 0] = i
n = 0,对于所有 j:d[0, j] = j
*/
for(i = 0; i <= source.length(); i++)
d[i][0] = i;
for(j = 0; j <= target.length(); j++)
d[0][j] = j;
for(i = 1; i <= source.length(); i++)
{
for(j = 1; j <= target.length(); j++)
{
if((source.charAt(i-1) == target.charAt(j-1)))
{
d[i][j] = d[i - 1][j - 1]; //不需要编辑操作
}
else
{
int edIns = d[i][j - 1] + 1; //source 插入字符
int edDel = d[i - 1][j] + 1; //source 删除字符
int edRep = d[i - 1][j - 1] + 1; //source 替换字符
d[i][j] =Math.min(Math.min(edIns, edDel), edRep);
}
}
}
return d[source.length()][target.length()];
}
public static void main(String[] args) {
// TODO Auto-generated method stub
String source="abc";
String target="bcfed";
int dis=EditDistance(source, target);
System.out.println(dis);
}
}
测试结果: