Edit Distance(编辑距离)

Edit Distance(编辑距离)

1. 编辑距离的定义

  在计算机科学中,编辑距离用于度量任意两个字符串间不相似的程度,即二者之间的编辑距离越大表示两个字符串之间的差异就越大。

问题描述:

  给定两个字符串x和y,只允许使用三种操作(插入一个字符、删除一个字符、修改一个字符)将x变换为y,求最少需要的操作次数。(更进一步,还需给出变换的具体步骤)【此编辑距离被称作Levenshtein distance】
   P.S. 在Longest common subsequence (LCS) distance中只允许进行插入和删除两种操作。在Hamming Distance 中只允许进行替换操作。这就是这距离的关系,从广义上说它们都可以称作编辑距离,本文所说的编辑距离主要指:Levenshtein distance

举例:

  以”kitten”和”sitting”两个字符串为例,它们之间的编辑距离是3。因为可以通过如下3步将”kitten”变为”sitting”,且至少需要3步才能完成变化,具体步骤如下所示:

1. kitten → sitten (将"k"替换为"s")
2. sitten → sittin (将"e"替换为"i")
3. sittin → sitting (在末尾添加"g").

2. 编辑距离的性质

  编辑距离满足度量公理,具备如下性质:

  • d(a, b) = 0 if and only if a=b
  • d(a, b) > 0 when a ≠ b
  • d(a, b) = d(b, a) # by equality of the cost of each operation and its inverse.
  • Triangle inequality: d(a, c) ≤ d(a, b) + d(b, c).
  • LCS distance 的上限是两个字符串长度之和。
  • LCS distance 是 Levenshtein distance 的上限。
  • 对于等长串, Hamming distance 是 Levenshtein distance 的上限。

3. 编辑距离的应用

  编辑距离在很多领域中都有这广泛的应用。在自然语言处理方面,常见的拼写自动纠错就是通过编辑距离来实现的,即计算用户输入的字符串与候选字符串集合中字符串的编辑距离,来为用户自动推荐最可能的单词或语言片段。在生物信息学中编辑距离经常用于度量两个基因DNA片段序列的相似程度,因为DNA片段可以看成是A、C、G和T碱基组成的序列串。


4. 编辑距离的计算

  编辑距离最早采用 Wagner–Fischer algorithm 来进行求解,该算法采用动态规划的思想,也是各类算法教科书中常见的求解编辑距离的算法,本质上是数学归纳大法。
  定义两个字符串分别为: a=a1a2...an b=b1b2...bm dmn a1a2...an b1b2...bm 之间的编辑距离。
  由于编辑距离具有对称性(删除、插入、替换的三种操作的权重相同,且操作可逆),即 dmn=dnm 。在实际意义上,表示将 a 变成 b 和将 b 变成 a 所需要的操作复杂度是相同的。
  我们假设将 b 变成 a 的复杂度为 dmn ,有如下关系式与分析:
  
   di0=ik=1wdel(bk)for1im
  表示将 b1b2...bi 变为空串需要需要进行 i 次删除操作,将每次删除操作的权重求和即为 di0
  
   d0j=jk=1wins(aj)for1jn
  表示将空串变成 a1a2...aj 需要进行 j 次插入操作,将每次插入操作的权重求和即为 d0j
  前面两个公式相当为动态规划赋迭代初值,接下来的公式表明了该问题符合动态规划的求解思路,以及如何划分子问题。
  

dij=  di1,j1,foraj=bi  min  di1,j+wdel(bi)  di,j1+wins(aj)  di1,j1+wsub(aj,bi)  forajbi  for1im,1jn

  考虑将 b1b2...bi 变为 a1a2...aj , 已知之前的子问题的解即 di1,j1 di1,j di,j1 。考虑 aj bi
  如果 aj=bi ,在 di1,j1 的基础上不需要进行如何操作就能得到 di,j
  如果 ajbi ,则在之前的基础上只进行一步操作(有三种可选方式:删除、插入、替换)可以得到 di,j
  1) 将 b1b2...bi1 变为 a1a2...aj ,然后删除末尾多余的字符 bi ,即可将问题归结为 di1,j
  2) 将 bi b1b2...bi 变成 a1a2...aj1 ,然后在末尾插入 aj ,即可将问题归结为 di,j1
  3) 将 b1b2...bi1 变为 a1a2...aj1 然后用 aj 替换 bi ,即可将问题归结为 di1,j1
  该动态规划算法的时间复杂度为 Θ(mn) ,空间复杂度为 Θ(mn) ,空间复杂度可进一步降低到 Θ(min(m,n)) ,因为在迭代的过程中我们只需要记录最近一次的子问题的解,不需要记录所有子问题空间中的解。变换的步骤可以通过回溯的方式得到(跟大多数采用动态规划求解的问题类似)。线性空间复杂度的解可以参见Hirschberg’s algorithm,这里我们先给出常规解。

  • python 实现
import random

class Solution:
    def minDistance(self, word1, word2):
        m, n = len(word2), len(word1)
        d = [[0] * (n+1) for k in range(m+1)]
        for i in range(m+1): d[i][0] = i
        for j in range(n+1): d[0][j] = j
        for i in range(1, m+1):
            for j in range(1, n+1):
                if word1[j-1] == word2[i-1]: d[i][j] = d[i-1][j-1]
                else:
                    d[i][j] = min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+1)
        self.backtrace(word1, word2, d)
        return d[m][n]

    def backtrace(self, word1, word2, d):
        m, n, steps = len(word2), len(word1), []
        # generate a random solution
        while m > 0 and n > 0:
            if word2[m-1] == word1[n-1] and d[m][n] == d[m-1][n-1]:
                m -= 1
                n -= 1
            else:
                choices = []
                #0 - delete word2[m]
                if d[m][n] == d[m-1][n] + 1: choices.append(0)
                #1 - insert word1[n]
                if d[m][n] == d[m][n-1] + 1: choices.append(1)
                #2 - substitute word2[m] => word1[n]
                if d[m][n] == d[m-1][n-1] + 1: choices.append(2)
                #randomly choose one possible choices
                rc = random.choice(choices)
                if 0 == rc: 
                    steps.append("delete word2[%d]='%s'" % (m-1, word2[m-1]))
                    m -= 1
                elif 1 == rc:
                    steps.append("insert word1[%d]='%s' at %d " % (n-1, word1[n-1], m-1))
                    n -= 1
                elif 2 == rc: 
                    steps.append("substitute word2[%d]='%s' to word1[%d]='%s'" % (m-1, word2[m-1], n-1, word1[n-1]))
                    m -= 1
                    n -= 1
                else:
                    print ('Error!')
                    return
        while m > 0:
            steps.append("delete word2[%d]='%s'" % (m-1, word2[m-1]))
            m -= 1
        while n > 0:
            steps.append("insert word1[%d]='%s' at 0" % (n-1, word1[n-1]))
            n -= 1
        steps.reverse()
        for i in range(len(steps)):
            print ("Step %d: %s" % (i+1, steps[i]))
  • c++ implementation
int minDistance(string word1, string word2) {
    size_t m = word2.length(), n = word1.length();
    vector prev(n+1), current(n+1, 0);
    iota(prev.begin(), prev.end(), 0);
    for (size_t i=1; i1; i++) {
        current[0] = i;
        for (size_t j=1; j1; j++) {
            if (word2[i-1] == word1[j-1]) {
                current[j] = prev[j-1]; 
            } else {
                current[j] = min(min(prev[j-1]+1, prev[j]+1), current[j-1]+1);
            }
        }
        swap_ranges(prev.begin(), prev.end(), current.begin());
    }
    return (int) prev[n];
}

5. leetcode 刷题

  • 72. Edit Distance

你可能感兴趣的:(学习)