Edit Distance问题在两种编程范式下的求解

本文已授权 [Coding博客](https://blog.coding.net) 转载

前言

Edit Distance,中文叫做编辑距离,在文本处理等领域是一个重要的问题,以下是摘自于百度百科的定义

编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

分别用R(replace),I(insert),D(delete),M(Match)代表替换、插入、删除、匹配操作,vintnerwriters直接的转换过程,可以如图所示

从图中可以看出,vintnerwriteres的最短编辑距离为5。所谓的编辑距离的问题即求解两个字符串之间的最小距离,用函数名称定义如下

int minDistance(String word1, String word2) {}

指令式编程范式下使用动态递归求解

状态定义

这一类问题,可以通过动态递归的方式来解决。

定义 对于两个字符串S1和S2,D(i,j)表示两个字符子串S1[1..i]和S2[1..j]之间的最短编辑距离。

对于两个字符串,长度分别为n和m,那么为了D(n,m)就是他们的之间的最短编辑距离,为了计算D(n,m)需要计算所有的D(i,j),其中 0<=i<=M, 0<=j<=N。对于基本情况,可以得到
D(i,0)=0,D(0,j)=0

状态转移方程及其证明

动态递归的中,确定了初始状态以后,就需要推倒出问题的状态转移方程,编辑距离问题的状态转移方程是:
D(i,j)= min[D(i-1,j)+1,D(i,j-1)+1,D(i-1,j-1)+t(i,j)]
其中

t(i,j) = S1.charAt(i) == S2.charAt(j) ? 0 : 1

 

以下是证明

命题1 D(i,j)的取值,只可能为 D(i-1,j)+1, D(i,j-1)+1, D(i-1,j-1)+t(i,j) 这三者中的一个,没有其他可能。

考虑把S1[1..i]转换到S2[1..j]这个过程,对于最后一步编辑,这一步的动作只可能是R,I,D,M中的一种。现假设最后一步编辑的动作是I,即表示把S2[j]插入到正在转化的S1的末尾。那么对于这个动作之前的连续的编辑动作,表示的是把S1[1..i]转换到S2[1..j-1]这个过程,这个过程的最短编辑距离根据之前的定义表示为D(i,j-1),那么由此可以得出D(i,j)=D(i,j-1)+1
同理,对于最后一步动作是D的情况,D(i,j)=D(i-1,j)+1。而对于最后一步东西是M或者R的情况,$D(i,j)=D(i-1,j-1)+t(i,j)$。结合命题1和D(i,j)的定义,状态转移方程的正确性得正。

LeetCode https://leetcode.com/problems/edit-distance/ 上,就有一道这样的题目,以下是用上述动态规划的思路的解法,已经AC

public int minDistance(String word1, String word2) {
        int w1length = word1.length();
        int w2length = word2.length();
        int[][] dp = new int[w1length+1][w2length+1];
        for(int l = 0; l<= w2length; l++ ){
            dp[0][l] = l;
        }
        for(int l = 0; l<= w1length; l++ ){
            dp[l][0] = l;
        }
        for(int i = 1; i<=w1length; i++){
            for(int j=1; j<=w2length;j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j]+1,dp[i][j-1]+1));
                }else{
                   dp[i][j] = Math.min(dp[i-1][j-1]+1,Math.min(dp[i-1][j]+1,dp[i][j-1]+1));

                }
            }
        }
        return dp[w1length][w2length];
    }

函数式编程范式下的求解

之前说到的动态递归的算法,都在指令式编程即imperative programming范式下讨论的,但是在程序开发中,还有一种范式称为函数式编程即functional programming。scala作为一种在jvm上运行的语句,既有函数式又有面对对象的特点,下面我们用函数式的方式,用scala来求解最小编辑距离问题。

 

对问题建立模型

这里的模型,指的是如何将这个问题model成程序中的类型,这里的类型包括scala中内置的数据类型,也包括自定义的类型,以下是我根据几种编辑步骤定义的类型:

 

trait Edit
case class Replace(val from: Char, val to: Char) extends Edit
case class Match(val c: Char) extends Edit
case class Delete(val c: Char) extends Edit
case class Insert(val c: Char) extends Edit

每个case class对应一种编辑动作,所有的case class都继承自Edit这个特质,为了求解两个字符串之间的最小编辑距离,先求解一个字符串到另外一个字符串需要经过多少步的编辑步骤,函数的定义如下:

def transform(s1: List[Char], s2: List[Char]): List[Edit]

 

注:在scala中,String可以很方便的转化成List[Char]。

问题求解

对于base case,当s2为空字符串的时候,s1只需要把每个字符给删掉,就能转换成s2;当s1为空字符串的时候,只需要把s2中所有的字符按次序插入s1,就能转化成s1。把这段话语直白的翻译成代码,可以得到函数的部分实现:

 

def transform(s1: List[Char], s2: List[Char]): List[Edit] = {
  (s1, s2) match {
    case (s1, Nil) => s1 map (x => Delete(x))
    case (Nil, s2) => s2 map (x => Insert(x))
  }
}

  

处理了base case,需要处理更加一般的情况,当s1和s2的第一个字符相等的时候,则生成一个Match动作,接着处理剩下的字符子串。如果s1和s2的第一个字符串不相等的时候,则有三种情况:

  1. 删掉s1的第一个字符,将剩下字符子串s1[2..n]转化成s2。
  2. 插入s2的第一个字符,将s1转化成s2剩下的字符子串s2[2..n]。
  3. 将s1的第一个字符替换成s2的第一个字符,将字符子串s1[2..n]转化成s2[2..n]。

综上,我们可以得到tranform函数的完整实现

def transform(s1: List[Char], s2: List[Char]): List[Edit] = {
  (s1, s2) match {
    case (s1, Nil) => s1 map (x => Delete(x))
    case (Nil, s2) => s2 map (x => Insert(x))
    case (x :: xs, y :: ys) => if (x == y) {
      Match(y) :: transform(xs, ys) //s1和s2的第一个字符相等的时候,则生成一个Match动作
    }
    else {
      best(List(Delete(x) :: transform(xs, s2), Insert(y) :: transform(s1, ys), Replace(x, y) :: transform(xs, ys)))
    }
  }
}

其中best函数求解多个List[Edit]中的最优值,函数的实现为:

 

def best(edits: List[List[Edit]]): List[Edit] = edits match {
  case (x :: Nil) => x
  case (x :: xs) => {
    val b = best(xs)
    if (cost(x) <= cost(b)) x else b
  }
}

cost函数求解一个List[Edit]中所需要的编辑步骤,因为Match步骤其实是不需要做任何动作的,那么计算cost的时候,只需要把Match步骤给过滤掉就行,在scala中可以用一行函数来实现:

 

def cost(edits: List[Edit]): Int = edits.filterNot(x => x.isInstanceOf[Match]).length

我们可以测试一下,edits记录的是转换过程。

 

val s1 = "vintner"
val s2 = "writers"
val edits = transform(s1.toList, s2.toList)
val result = s"edit distance between 

----------
$s1 and $s2 is" + cost(edits)
//result
edits: List[Edit] = List(Insert(w), Replace(v,r), Match(i), Delete(n), Match(t), Delete(n), Match(e), Match(r), Insert(s))
result: String = edit distance between vintner and writers is 5

总结

至此,这道问题用scala就解决了,其实所做的无非就是把建立模型和解决问题的思考过程非常直白的翻译成代码,因为函数式语言描述的更多是数学,和人类的思考过程更加相近,而过程式编程描述的更多的是机器语言,对于一些问题的求解没有那么的直观。从Edit Distance这道题目的两种解法就能够清楚的看出来。至于两种方法的效率比较,则已经超出我的能力范围了。函数式编程的算法分析,算是比较偏的一门课程了,我也没有涉猎过。

 

参考书籍

  1. Algorithms on Strings, Trees and Sequences: Computer Science and Computational Biology
  2. Haskell: The Craft of Functional Programming

此问题的Haskell解法,来自于Haskell: The Craft of Functional Programming一书

data Edit = Change Char|
            Copy|
            Delete|
            Insert Char|
            Kill
            deriving(Eq,Show)
transform :: String->String->[Edit]
transform [] [] = []
transform xs [] = [Kill]
transfrom [] ys = map Insert ys
transform (x:xs)(y:ys)
    | x == y = Copy: : transform xs ys
    | otherwise = best [Delete : transform xs (y:ys), Insert y : transform (x:xs) ys, Change y : transform xs ys]
best :: [[Edit]] ->[Edit]
best [x] = x
best (x:xs)
    | cost x <= cost b = x
    | otherwise = b
        where 
        b = best xs
cost :: [Edit] -> Int
cost = length . filter(/=Copy)

 

 

 

 

 

 

 

 

 

 


 

posted @ 2017-01-08 17:08  JavaNerd  阅读(562)  评论(0编辑  收藏  举报