Min Edit Distance

————两字符串之间的最小距离

PPT原稿参见Stanford;http://www.stanford.edu/class/cs124/lec/med.pdf

Tips:由于本人水平有限,对MED的个人理解可能有纰漏之处,请勿尽信。

Edit:个人理解指编辑之意,也即对于两个字符串,对其中的一个进行各种编辑操作(插入、删除、替换)使其变为另一个字符串。要解决的问题是求出最小的编辑操作次数是多少。

基因系列比对

定义距离:

X,Y是大小分别为n,m的字符串。

定义D(i,j)表示X[1..i],Y[1..j]两子字符串的距离。则:

X与Y的距离为D(n,m)

应用动态规划的方法:

对于D(i,j)的计算结果做成表格(矩阵),D(i,j)的运算结果可以建立在之前的结果之上。

对本算法的一点个人理解:

设 i:输出子字符串长度

    j:输入子字符串长度

D(0,0)=0

D(i,0):输入0个字符转换到i个字符的输出,也即要插入i个字符,代价为insertCost*i

D(0,j):目标长度为0,输入长度为j,所以代价为:deletionCost*j

D(i,j)的上一步可以分为三种情况:

1、上一步输入长度为j,输出长度为i-1,那么现在这一步肯定至少还要插入一个字符才能达到i长度输出:

D(i-1,j)+inseartCost*1

2、上一步输入长度为j-1,输出长度为i-1,那么现在这一步第j个输入只需要做替换处理(如果第j个输入与第i个输出不相等)或者保持不变(如果第j个输入与第i个输出相等):

D(i-1,j-1)+substituteCost*(source[j]==target[i] ? 0 : 1)

3、上一步输入长度为j-1,输出长度为i,那么现在这一步由于又多了一个字符,所以要把多的这个字符删除:

D(i,j-1)+deletionCost*1

由于我们要求的是最小Edit Distance,自然,就是上述三种情况中最小值做为D(i,j)的值。具体算法如下:

最小编辑距离动态算法(Levenshtein):

D(n,m)即为最小距离。

 

字符串对齐:

    每一次计算D(i,j)时记录当前数据是由哪个数据得的。当D(n,m)的计算完成后,即可从D(n,m)出发进行回溯(backtrace).得到和条从D(n,m)到D(0,0)的路径:

上图中从(0,0)到(n,m)的每一个非递减路径即为两字符串的对齐关系。

附加回溯过程的MED:


Weighted Edit Distance:(加权编辑距离)

为什么要加权?

因为有些字符被写错的概率要大些(如搜索引擎中经常能自动搜索到相近的词句)

算法:

 


Levenshtein算法python实现:

#===============================================================================
# Using dynamic programming to realize the MED algorithm(Levenshtein)
# MED: short for Minimum Edit Distance
#=============================================================================== import types
import numpy as np class MED:
def __init__(self):
self.insCost = 1 #insertion cost
self.delCost = 1 #deletion cost
self.subsCost = 1 #substitution cost
self.D = 0 def mDistance(self, word1, word2):
assert type(word1) is types.StringType
assert type(word2) is types.StringType
size1 = len(word1)
size2 = len(word2)
if size1==0 or size2 ==0:
return size1*self.delCost+size2*self.insCost
D_mat = np.zeros((size1+1,size2+1))
D_mat[:,0] = range(size1+1)
D_mat[0,:] = range(size2+1)
for i in range(1,size1+1):
for j in range(1,size2+1):
insert_cost = D_mat[i-1, j] + self.insCost*1
delete_cost = D_mat[i, j-1] + self.delCost*1
if word1[i-1]==word2[j-1]:
substitue_cost = D_mat[i-1, j-1]
else:
substitue_cost = D_mat[i-1, j-1] + self.subsCost*1
D_mat[i,j] = min(insert_cost, delete_cost, substitue_cost)
self.D = D_mat
return D_mat[size1, size2] if __name__ == "__main__":
word1 = "Function"
word2 = "fanctional"
med = MED()
print "MED distance is :" ,med.mDistance(word1, word2)

输出结果:

MED distance is : 4.0

 

扩展应用1:利用回溯做字符对齐

对原ED计算函数做点更改(每次得到MED时记录该值的来源,然后从D(n,m)开始利用记录的来源回溯至起始位置,得到该MED的完整路径):

def computingAlignment(self, word1,  word2):
assert type(word1) is types.StringType
assert type(word2) is types.StringType
size1 = len(word1)
size2 = len(word2)
if size1==0 or size2 ==0:
return size1*self.delCost+size2*self.insCost
D_mat = np.zeros((size1+1,size2+1))
D_rec = np.zeros((size1+1, size2+1))
D_mat[:,0] = range(size1+1)
D_mat[0,:] = range(size2+1)
for i in range(1,size1+1):
for j in range(1,size2+1):
insert_cost = D_mat[i-1, j] + self.insCost*1
delete_cost = D_mat[i, j-1] + self.delCost*1
if word1[i-1]==word2[j-1]:
substitue_cost = D_mat[i-1, j-1]
else:
substitue_cost = D_mat[i-1, j-1] + self.subsCost*1
D_mat[i,j] = min(insert_cost, delete_cost, substitue_cost)
if D_mat[i,j] == insert_cost:#Record Where the min val comes from
D_rec[i,j] = 1
elif D_mat[i,j]== delete_cost:
D_rec[i,j] = 2
else:
D_rec[i,j] = 3
self.D = D_mat
self.D_rec = D_rec
# BackTrace
alignRevPath=[]#Get the reverse path
j = size2
i = size1
while i!=0 or j!=0:#Be carefull of this row
alignRevPath.append([i,j,D_rec[i,j]])
if D_rec[i,j]==1:
i -=1
elif D_rec[i,j]==2:
j -=1
elif D_rec[i,j]==3:
i -=1
j-=1
elif D_rec[i,j]==0:
if i>0:
i -= 1
if j>0:
j -= 1
alignStr1 =[]
alignStr2 =[]
if alignRevPath[-1][0]!=0:#process the first postion of the path
alignStr1.append(word1[alignRevPath[-1][0]-1])
else:
alignStr1.append('*')
if alignRevPath[-1][1]!=0:
alignStr2.append(word2[alignRevPath[-1][1]-1])
else:
alignStr2.append('*') for i in range(len(alignRevPath)-1, 0, -1): #process the rest of the path
k = np.subtract(alignRevPath[i-1], alignRevPath[i])
bK = k>0
if bK[0]!=0:
alignStr1.append(word1[alignRevPath[i-1][0]-1])
else:
alignStr1.append('*') if bK[1]!=0:
alignStr2.append(word2[alignRevPath[i-1][1]-1])
else:
alignStr2.append('*')
return (alignStr1, alignStr2)

上面的代码中alignRevPath用来保存路径上的每一个位置,其每个元素都为3元列表,前两维为路上的坐标,第3维取0、1、2、3四种值,0表示路径到达边界了,1表示当前的ED结果由前一个insert操作得到,2表示当前ED结果由前一个Delete得到,3表示当前ED结果由前一个substitue得到。

在主程序中加入如下测试代码:

word1 = "efnction"
word2 = "faunctional"
med = MED()
w1, w2 = med.computingAlignment(word1, word2)
print ' '.join(w1)
print '| '*len(w1)
print ' '.join(w2)

得到的输出:

 


扩展应用2:匹配最长子字符串(不一定连续)

原理:

图示:

从上图可以看出,匹配的并不一定是连续的子串.这是因为我们的惩罚项设置为:

也即迭代过程中的s(xi,yj)取-1(不匹配)或1(匹配)。当子串中有不匹配的字符出现时,将会对之前的匹配计数减1。

如果想匹配最长连续子串,可以令惩罚项F(i,j)为0(不匹配)或F(i-1,j-1)+1(匹配)

Python 实现(对computingAlignment做少许更改):

def longestSubstr(self, word1,  word2):
assert type(word1) is types.StringType
assert type(word2) is types.StringType
size1 = len(word1)
size2 = len(word2)
if size1==0 or size2 ==0:
return size1*self.delCost+size2*self.insCost
D_mat = np.zeros((size1+1,size2+1))
D_rec = np.zeros((size1+1, size2+1))
D_mat[:,0] = 0
D_mat[0,:] = 0
for i in range(1,size1+1):
for j in range(1,size2+1):
insert_cost = D_mat[i-1, j] - self.insCost*1
delete_cost = D_mat[i, j-1] - self.delCost*1
if word1[i-1]==word2[j-1]:
substitue_cost = D_mat[i-1, j-1] +1
else:
substitue_cost = D_mat[i-1, j-1] - self.subsCost*1
# substitue_cost = 0
D_mat[i,j] = max(0,insert_cost, delete_cost, substitue_cost)
if D_mat[i,j] == insert_cost:#Record Where the min val comes from
D_rec[i,j] = 1
elif D_mat[i,j]== delete_cost:
D_rec[i,j] = 2
elif D_mat[i,j]==substitue_cost:
D_rec[i,j] = 3 self.D = D_mat
self.D_rec = D_rec
maxVal = np.max(D_mat)
maxBool = D_mat == maxVal
numMax = np.sum(maxBool)
alignRevPaths=[]
for i in range(numMax):
alignRevPaths.append([])
pathStarts=[]
for i in range(size1+1):
for j in range(size2+1):
if maxBool[i,j]:
pathStarts.append([i,j]) for m in range(numMax):
i = pathStarts[m][0]
j = pathStarts[m][1]
while i!=0 and j!=0:
alignRevPaths[m].append([i,j,D_rec[i,j]])
if D_rec[i,j]==1:
i -=1
elif D_rec[i,j]==2:
j -=1
elif D_rec[i,j]==3:
i -=1
j-=1
elif D_rec[i,j]==0:
break
if D_mat[i,j]==0:
break
str1=[]
str2=[]
for m in range(numMax):
str1.append([])
str2.append([])
p = alignRevPaths[m]
for i in range(len(p)-1, -1,-1):
str1[m].append(word1[p[i][0]-1])
str2[m].append(word2[p[i][1]-1])
return ([''.join(i) for i in str1],[''.join(i) for i in str2])

代码测试:

word1 = "ATCAT"
word2 = "ATTATC"
med = MED()
str1,str2= med.longestSubstr(word1, word2)
print "Longest match substr:"
print "match substr in word1:", str1
print "match substr in word2:", str2

输出结果:

Longest match substr:

match substr in word1: ['ATC', 'ATCAT']

match substr in word2: ['ATC', 'ATTAT']

如果把longestSubstr中的

substitue_cost = D_mat[i-1, j-1] - self.subsCost*1

改为:

substitue_cost = 0

即可用来求最大连续子字符串,同样运行上述测试代码,可得:

Longest match substr:

match substr in word1: ['ATC']

match substr in word2: ['ATC']

改用另外一组字符串进行实验进行进一步验证:

word1 = "fefnction"
word2 = "faunctional"
med = MED()
str1,str2= med.longestSubstr(word1, word2)
print "Longest match substr:"
print "match substr in word1:", str1
print "match substr in word2:", str2

当求解的是最长子字符串(非连续)时,输出:

Longest match substr:

match substr in word1: ['nction']

match substr in word2: ['nction']

当求解的是最长连续子字符串时,输出:

Longest match substr:

match substr in word1: ['nction']

match substr in word2: ['nction']

 

在以上的算法中,MED的一个最大的特点就是利用了矩阵保存之前处理的结果数据,以做为下一次的输入。对于最长子字符串的查找,同样套用了MED的框架,但仔细一想我们会发现,其实最长子串的查找并不一定需要记录路径alignRevPaths,有了alignRevPaths这个路径,我们编程时是根据这个路径处理字符的。路径的设置主要是为了在computingAlignment中对字符进行对齐,因为字符对齐的情况下,字符不一定是连续,比如会有如下对齐的形式中的“*”号:

所以我们才想到要用路径做记录。

但在最长子串中,子串肯定是连续的,自然路径也就不需要了。

下面对最长子串程序做简化:

def longestSubstr2(self,word1, word2):
assert type(word1) is types.StringType
assert type(word2) is types.StringType
size1 = len(word1)
size2 = len(word2)
if size1==0 or size2 ==0:
return size1*self.delCost+size2*self.insCost
D_mat = np.zeros((size1+1,size2+1))
D_mat[:,0] = 0
D_mat[0,:] = 0
for i in range(1,size1+1):
for j in range(1,size2+1):
insert_cost = D_mat[i-1, j] - self.insCost*1
delete_cost = D_mat[i, j-1] - self.delCost*1
if word1[i-1]==word2[j-1]:
substitue_cost = D_mat[i-1, j-1] +1
else:
substitue_cost = D_mat[i-1, j-1] - self.subsCost*1
# substitue_cost = 0 D_mat[i,j] = max(0,insert_cost, delete_cost, substitue_cost)
self.D = D_mat
maxVal = np.max(D_mat)
maxBool = D_mat == maxVal
numMax = np.sum(maxBool)
alignRevPaths=[]
for i in range(numMax):
alignRevPaths.append([])
pathStarts=[]
for i in range(size1+1):
for j in range(size2+1):
if maxBool[i,j]:
pathStarts.append([i,j]) str1=[]
str2=[]
for m in range(numMax):
str1.append([])
str2.append([])
i = pathStarts[m][0]
j = pathStarts[m][1]
s1Tmp = []
s2Tmp = []
while D_mat[i,j]!=0:
s1Tmp.append(word1[i-1])
s2Tmp.append(word2[j-1])
i -= 1
j -= 1
str1[m]=[s1Tmp[len(s1Tmp)-i-1] for i in range(len(s1Tmp))]
str2[m]=[s2Tmp[len(s2Tmp)-i-1] for i in range(len(s2Tmp))]
return ([''.join(i) for i in str1],[''.join(i) for i in str2])

使用以下字符做实验:

word1 = "ATCAT"

word2 = "ATTATC"

输出结果同样为:

Longest match substr:

match substr in word1: ['ATC', 'ATCAT']

match substr in word2: ['ATC', 'ATTAT']

一点个人感悟:

虽然这里的动态规划算法看上去有点不可思议,但联想起线性代数中的矩阵运算也就不难理解了。

就拿最长子串的程序来说,其实际过程仍可看做:对每word1中的每一个字符在word2中进行查找,当匹配第一个字符后继续匹配第二个字符,然后第三、第四……个,直到有字符不匹配时,记录该匹配成功的串长度及字串始末位置。

而这里的Smith-waterman算法将这个匹配记录在一个2维矩阵(数组)中。我们都知道,线性代数中的矩阵乘法同样是可以展开为各元素的乘与累加操作的。而这里,我认为,发明者正是利用了这一思想。

Min Edit Distance的更多相关文章

  1. 动态规划 求解 Minimum Edit Distance

    http://blog.csdn.net/abcjennifer/article/details/7735272 自然语言处理(NLP)中,有一个基本问题就是求两个字符串的minimal Edit D ...

  2. 72. Edit Distance

    题目: Given two words word1 and word2, find the minimum number of steps required to convert word1 to w ...

  3. [LeetCode] One Edit Distance 一个编辑距离

    Given two strings S and T, determine if they are both one edit distance apart. 这道题是之前那道Edit Distance ...

  4. [LeetCode] Edit Distance 编辑距离

    Given two words word1 and word2, find the minimum number of steps required to convert word1 to word2 ...

  5. Edit Distance

    Edit Distance Given two words word1 and word2, find the minimum number of steps required to convert  ...

  6. 编辑距离——Edit Distance

    编辑距离 在计算机科学中,编辑距离是一种量化两个字符串差异程度的方法,也就是计算从一个字符串转换成另外一个字符串所需要的最少操作步骤.不同的编辑距离中定义了不同操作的集合.比较常用的莱温斯坦距离(Le ...

  7. LintCode Edit Distance

    LintCode Edit Distance Given two words word1 and word2, find the minimum number of steps required to ...

  8. 【leetcode】Edit Distance

    Edit Distance Given two words word1 and word2, find the minimum number of steps required to convert  ...

  9. ✡ leetcode 161. One Edit Distance 判断两个字符串是否是一步变换 --------- java

    Given two strings S and T, determine if they are both one edit distance apart. 给定两个字符串,判断他们是否是一步变换得到 ...

随机推荐

  1. 基于QT的换肤整体解决方案(QSkinStyle)(提供Linux的XP风格)

    基于QT的换肤整体解决方案(QSkinStyle) 对QT这个成功的跨平台GUI库,本身内置了对换肤功能的实现,比如cleanlooks.plastique等跨平台风格:还有一些是和平台相关的风格,比 ...

  2. Android:修改版本

    修改AndroidManifest.xml下的Version即可 <uses-sdk android:minSdkVersion="14" android:targetSdk ...

  3. C# Java间进行RSA加密解密交互(三)

    原文:C# Java间进行RSA加密解密交互(三) 接着前面一篇C# Java间进行RSA加密解密交互(二)说吧,在上篇中为了实现 /** * RSA加密 * @param text--待加密的明文 ...

  4. 微信开发之——Php批量生成带参数的二维码

    带参数的二维码对于渠道营销推广来说是很有用的,可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送,可喜的是微信开通了这个接口,那下面就来研究一下吧. 具体接口说明请参见,微信公众平 ...

  5. SPOJ 1108 Card Trick 暴力模拟

    解释一下样例,因为我觉得这个题意表述的不是很清楚.以第二组样例为例. 牌序为:3 1 4 5 2 第一轮:把 3 放到末尾:1 4 5 2 3,最顶上的牌是1,把1拿走.剩余 4 5 2 3 第二轮: ...

  6. Vim 命令笔记

    给指定行添加序号 let la = 行a let lb = 行b +1 let lc = lb - la for i in range(lc) let cl = la + i call setline ...

  7. php获取apk包信息的方法

    /*解析安卓apk包中的压缩XML文件,还原和读取XML内容 依赖功能:需要PHP的ZIP包函数支持.*/ include('./Apkparser.php'); $appObj = new Apkp ...

  8. JNIEnv解析

    1.关于JNIEnv和JavaVM JNIEnv:线程相关的变量 JavaVM:是虚拟机在JNI层的代表, JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立.JavaVM是虚拟机在J ...

  9. kafka的环境搭建

    kafka是一个高吞吐量的消息系统.隔离消息接收和处理过程(可理解为一个缓存) 1.kafka伪分布的部署 1.1.下载并解压 1.2.启动zk bin/zookeeper-server-start. ...

  10. Android Activity形象描述

    Activity就是形象的说就是一个容器,在里面放置各种控件(按钮,文本,复选框等),就形成了软件的界面~ Activity是可见的,如果不加任何控件的话,那么就像Windows中的空白窗体一样 通过 ...