计算字符的相似度【转】
许多程序会大量运用 字符串。对于不同的字符串,我们希望能够有办法判断其相似程序。我们定义一套操作要领 来把两个不相同的字符串变得相同,具体的操作要领 为:
1.修改一个字符(如把“a”替换为“b”);
2.添加 一个字符(如把“abdd”变为“aebdd”);
3.删除一个字符(如把“travelling”变为“traveling”);
比如,对于“abcdefg”和“abcdef”两个字符串来说,我们认为可以通过添加 /减少一个“g”的方式来达到目的。上面的两种方案,都仅须要 一 次 。把这个操作所须要 的次数定义为两个字符串的距离,而相似度等于“距离+1”的倒数。也就是说,“abcdefg”和“abcdef”的距离为1,相似度 为1/2=0.5。
给定任意两个字符串,你能不能 能写出一个算法来计算它们的相似度呢?
原文的分析与解法
不难看出,两个字符串的距离肯定不超过它们的长度之和(我们可以通过删除操作把两个串都转化为空串)。虽然这个结论对结果没有帮助,但至少可以知道,任意两个字符串的距离都是有限的。
我们还是就住集中考虑如何 才能把这个疑问 转化成规模较小的同样的子疑问 。如果有两个串A=xabcdae和B=xfdfa,它们的第一个字符是相同的,只要计算A[2,...,7]=abcdae和B[2,...,5]=fdfa的距离就可以了。但是如果两个串的第一个字符不相同,那么可以执行 如下的操作(lenA和lenB分别是A串和B串的长度)。
1.删除A串的第一个字符,然后计算A[2,...,lenA]和B[1,...,lenB]的距离。
2.删除B串的第一个字符,然后计算A[1,...,lenA]和B[2,...,lenB]的距离。
3.修改A串的第一个字符为B串的第一个字符,然后计算A[2,...,lenA]和B[2,...,lenB]的距离。
4.修改B串的第一个字符为A串的第一个字符,然后计算A[2,...,lenA]和B[2,...,lenB]的距离。
5.添加 B串的第一个字符到A串的第一个字符之前,然后计算A[1,...,lenA]和B[2,...,lenB]的距离。
6.添加 A串的第一个字符到B串的第一个字符之前,然后计算A[2,...,lenA]和B[1,...,lenB]的距离。
在这个题目中,我们并不在乎两个字符串变得相等之后的字符串是怎样的。所以,可以将上面的6个操作合并为:
1.一步操作之后,再将A[2,...,lenA]和B[1,...,lenB]变成相字符串。
2.一步操作之后,再将A[2,...,lenA]和B[2,...,lenB]变成相字符串。
3.一步操作之后,再将A[1,...,lenA]和B[2,...,lenB]变成相字符串。
这样,很快就可以完成一个递归程序。
原文算法代码
int calculateStringDistance(string strA, int pABegin, int pAEnd, string strB, int pBBegin, int pBEnd) { if(pABegin > pAEnd) { if(pBBegin > pBEnd) return 0; else return pBEnd - pBBegin + 1; } if(pBBegin > pBEnd) { if(pABegin > pAEnd) return 0; else return pAEnd - pABegin + 1; } if(strA[pABegin] == strB[pBBegin]) { return calculateStringDistance(strA, pABegin+1, pAEnd, strB, pBBegin+1, pBEnd); } else { int t1 = calculateStringDistance(strA, pABegin, pAEnd, strB, pBBegin+1, pBEnd); int t2 = calculateStringDistance(strA, pABegin+1, pAEnd, strB, pBBegin, pBEnd); int t3 = calculateStringDistance(strA, pABegin+1, pAEnd, strB, pBBegin+1, pBEnd); return minValue(t1, t2, t3) + 1; } }
上面的递归程序,有什么地点 须要 改良 呢?疑问 在于:在递归的流程 中,有些数据被重复计算了。
我们知道适合采用动态筹划 要领 的最优化疑问 中的两个要素:最优子结构和重叠子疑问 。另外,还有一种要领 称为备忘录(memoization),可以充分运用 重叠子疑问 的性质。
下面简述一下动态筹划 的基本思想。和分治法一样,动态筹划 是通过组合子疑问 的解而处理 整个疑问 的。我们知道,分治算法是指将疑问 划分 成一睦独立的子疑问 ,递归 地求解各子疑问 ,然后合并子疑问 的解而得到原疑问 的解。与此不同,动态筹划 适用于子疑问 不是独立 的情况,也就是各子疑问 包含公共的子子疑问 。在这种情况 下,若用分治法则会做许多不必要的工作,即重复地求解公共的子子疑问 。动态筹划 算法对每个子子疑问 只求解一次,将其结果保存在一张表中,从而防止 每次遇到各个子疑问 时重新计算答案。
动态筹划 通常运用 于最优化疑问 。此类疑问 可能有很多种可行解,每个解有一个值,而我们希望找出一个具有最优(最大或最小)值的解。称这样的解为该疑问 的“一个”最优解(而不是“确定的”最优解),因为可能存在多个取最优值的解。
动态筹划 算法的设计可以分为如下4个步骤:
1)描述最优解的结构。
2)递归定义最优解的值。
3)按自底向上的方式计算最优解的值。
4)由计算出的结果构造一个最优解。
第1~3步构成疑问 的动态筹划 解的基础。第4步在只要求计算最优解的值时可以略去。如果的确做了第4步,则有时要在第3步的计算中记录一些附加信息,使构造一个最优解变得容易。
该疑问 明显完全符合动态筹划 的两个要素,即最优子结构和重叠子疑问 特征 。该疑问 的最优指的是两个字符串的最短距离,子疑问 的重叠性可以从原书中的那个递归算法中看出。
下面再来细致 说说什么是重叠子疑问 。适用于动态筹划 求解的最优化疑问 必须具有的第二个要素是子疑问 的空间要“很小”,也就是用来解原疑问 的递归算法可以反复地解同样的子疑问 ,而不是总在产生新的子疑问 。典型地,不同的子疑问 数是输入规模的一个多项式。当一个递归算法不断地调用同一疑问 时,我们说该最优疑问 包含重叠子疑问 。相反地,适合用分治法处理 的疑问 只往往在递归的每一步都产生全新的疑问 。动态筹划 算法总是充分运用 重叠子疑问 ,即通过每个子疑问 只解一次,把解保存在一个须要 时就可以查看的表中,而每次查表的时间为常数。
根据以上的分析,我写了如下的动态筹划 算法:
上面的递归程序,有什么地方需要改进呢?在递归的过程中,有些数据被重复计算了。为了避免重复计算,我们可以将计算后的结果存储起来。
如下表所示
我们注意到,当我们要计算distance(A[1,i],B[1,j])时,该值仅与红色部分的值相关。因此,我们只需要保存这些值即可。
int distance(const char *A, int n,const char *B, int m)
{
int C[n+1][m+1];
int i,j;
for(i=0; i<=n; i++)
C[i][0] = i;
for(i=1; i<=m; i++)
C[0][i] = i;
for(i=1; i<=n; i++)
for(j=1; j<=m; j++)
{
x = C[i-1][j]+1;
y = C[i][j-1]+1;
if(A[i] == B[j])
z = C[i-1][j-1];
else
z = C[i-1][j-1]+1;
C[i][j] = min(x,y,z);
}
return C[n][m];
}
posted on 2011-09-05 17:52 Lovell Liu 阅读(236) 评论(0) 编辑 收藏 举报