编辑距离(Levenshtein)

前言

最近师兄参加招聘笔试的时候,遇到了一道问题,其实就是变种的编辑距离问题。

问题描述

英文单词拼写的时候可能会出现拼写错误的情况(typo)。下面的题目,我们尝试实现拼写纠错推荐的功能。
字串编辑距离是衡量字串间相似度的常见手段。

  1. 字串编辑距离:是指利用字符操作,把字符串A转换成字符串B所需要的最少操作数。
  2. 字串操作包括:删除字符(removal)、插入字符(insertion)、修改字符(substituation)。
  3. 使用以下规则对推荐纠错选项进行相速度排序。得分越高,认为相似度越低

只涉及到26个英文字符、不区分大小写。
删除字符(removal) 3分
插入字符(insertion) 3分
修改字符(substituation):
(q w e r t a s d f g z x c v) (y u i o p h j k l b n m)
以上两个分组内的字符修改1分,两个分组间字符修改2分。

输入

每行一个输入。空格分隔参数。第一个参数是目标单词(可能存在typo),后面若干个空格分割参数(个数不定)是字典单词,作为候选纠错项(也可能和第一个参数重复)。

输出

按照上面的纠错规则字符串相似度最小编辑距离进行排序,给出3个(如有)对应的纠错候选。如得分相同,则按照输入顺序进行排序。

样例

样例输入

slep slap sleep step shoe shop snap slep

样例输出

slep slap step

编辑距离(Levenshtein)算法

算法原理

本算法采用动态规划的思想,计算对于字符串s{1,2,3, ...,n} 到字符串t{1,2,3,...,m}的最短距离(最小代价)。
假设:

  1. 插入一个字符的代价为:a
  2. 删除一个字符的代价为:b
  3. 修改一个字符的代价为:c

对于某个状态:
若字符串t与字符串temp的最小代价为costt,temp = k
且字符串temp与字符串s只需要一次操作(插入、删除、修改字符)
则字符串s与字符串t的最小代价costs,t = k+min(a,b,c)
这其实就是状态转移方程了。

状态转移方程

现在定义 costi,j :长度为i的字符串到长度为j的字符串最小代价。
所以 costn,m = min(①,②,③)
① = costn-1,m + min(a,b)
② = costn,m-1 + min(a,b)
③ = costn-1,m-1 + c
对于① ,从n串到m串,n-1作为temp串,花费代价为 costn-1,m,比较插入和删除操作中较小的代价,实现从n-1串到n串;
对于② ,从n串到m串,m-1作为temp串,花费代价为 costn,m-1 + min(a,b),比较插入和删除操作中较小的代价,实现从m-1串到m串;
对于③,若 n-1串到m-1串代价为k, 则n串到m串代价为 k+c (修改一个字符)

边界条件:
cost0,0 = 0
cost0,1 = min(a,b)
cost1,0 = min(a,b)

求解方法---代价表

步骤 内容
1 构造一张大小为(N+1,M+1)的表,用于储存代价。
2 计算所有边界的代价
3 根据状态转移方程计算整张表

如s="abcd" ,t="acdef" ,插入代价为1,删除代价为1,修改代价为1。
1.构造一张5x6的表,如下所示:

a b c d
a
c
d
e
f

2.计算所有边界条件。

a b c d
0 1 2 3 4
a 1
c 2
d 3
e 4
f 5

3.根据状态转移方程计算整张表.。

a b c d
0 1 2 3 4
a 1 0 1 2 3
c 2 1 1 1 2
d 3 2 2 2 1
e 4 3 3 3 2
f 5 4 4 4 3

最终可以看出,s="abcd" ,t="acdef" 的代价是3,删除b,插入ef。计算结果如表中最后一项。

java代码

import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;

public class WordDistance {

    private static int min(int one, int two) {
        int min;
        if (one <= two) min = one;
        else min = two;
        return min;
    }

    private static int min(int one, int two, int three) {
        return min(min(one, two), three);
    }

    public static int wd(String str1, String str2) {
        int cost_add = 3; //插入字符的代价
        int cost_del = 3; //删除字符的代价
        int[][] d; // 矩阵
        int n = str1.length();
        int m = str2.length();
        int i; // 遍历str1的
        int j; // 遍历str2的

        if (n == 0) {
            // str1是空串
            return m * min(cost_add, cost_del);
        }
        if (m == 0) {
            // str2是空串
            return n * min(cost_add, cost_del);
        }
        d = new int[n + 1][m + 1];
        // 初始化所有边界
        for (i = 0; i <= n; i++) { // 初始化第一列
            d[i][0] = i * min(cost_add, cost_del);
        }
        for (j = 0; j <= m; j++) { // 初始化第一行
            d[0][j] = j * min(cost_add, cost_del);
        }
        for (i = 1; i <= n; i++) { // 计算整张表
            char ch1 = str1.charAt(i - 1);
            for (j = 1; j <= m; j++) {
                char ch2 = str2.charAt(j - 1);
                // 状态转移方程
                d[i][j] = min(d[i - 1][j] + min(cost_add, cost_del),
                        d[i][j - 1] + min(cost_add, cost_del),
                        d[i - 1][j - 1] + get_cost(ch1, ch2));
            }
        }
        return d[n][m];
    }

    public static int get_cost(char a, char b) {
        // 题目中,修改字符的代价不同
        String s1 = "qwertasdfgzxcv";
        String s2 = "yuiophjklbnm";
        int cost = 0;
        if (a == b) return cost;
        if (s1.contains(a + "")) {
            if (s1.contains(b + "")) {
                // a b 都在s1组
                cost = 1;
            } else {
                // a b 分别在s1 s2组
                cost = 2;
            }
        } else {
            if (s2.contains(b + "")) {
                // a b 分别在s2 s1组
                cost = 2;
            } else {
                // a b 都在s2组
                cost = 1;
            }
        }
        return cost;
    }

    static class node implements Comparable<Integer> {
        // 绑定单词和距离代价
        int dis;
        String word;

        node(int dis, String word) {
            this.dis = dis;
            this.word = word;
        }

        @Override
        public int compareTo(Integer o) {
            return this.dis - o;
        }
    }

    public static void main(String[] args) {
        // 从system.in接收字符串
//        Scanner sc = new Scanner(System.in);
//        String s_in = sc.nextLine();
        String s_in = "slep slap sleep step shoe shop snap slep";
        String[] s_splits = s_in.split(" ");
        String target = s_splits[0];
        String[] dictionary = Arrays.copyOfRange(s_splits, 1, s_splits.length);
        int[] distance = new int[dictionary.length];
        node[] node_list = new node[dictionary.length];
        for (int i = 0; i < distance.length; i++) {
            distance[i] = wd(target, dictionary[i]);
            node_list[i] = new node(distance[i], dictionary[i]);
        }
        Arrays.sort(node_list, new Comparator<node>() {
            @Override
            public int compare(node o1, node o2) {
                return o1.dis - o2.dis;
            }
        });
        for (int i = 0; i < node_list.length; i++) {
            if (i > 2) break; //限制输出3个
            System.out.print(node_list[i].word + " ");
        }
    }
}

本人java水平很菜,输出格式是师兄随手写的,我贴来用用。

python代码

class WordDistance:
    cost_add = 3
    cost_del = 3

    @staticmethod
    def get_cost(a, b):
        if a == b:
            return 0
        if a in "qwertasdfgzxcv":
            if b in "qwertasdfgzxcv":
                cost = 1
            else:
                cost = 2
        else:
            if b in "qwertasdfgzxcv":
                cost = 2
            else:
                cost = 1
        return cost

    def __init__(self, ):
        pass

    def wd(self, str1, str2):
        n = len(str1)
        m = len(str2)
        # 空串情况
        if n == 0:
            return m * min(self.cost_add, self.cost_del)
        if m == 0:
            return n * min(self.cost_add, self.cost_del)
        d = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
        for i in range(n + 1):
            # 第一列
            d[i][0] = i * min(self.cost_add, self.cost_del)
        for j in range(m + 1):
            d[0][j] = j * min(self.cost_add, self.cost_del)
        for i in range(1, n + 1):
            ch1 = str1[i - 1]
            for j in range(1, m + 1):
                ch2 = str2[j - 1]
                d[i][j] = min(
                    d[i - 1][j] + min(self.cost_add, self.cost_del),
                    d[i][j - 1] + min(self.cost_add, self.cost_del),
                    d[i - 1][j - 1] + self.get_cost(ch1, ch2)
                )
        return d[n][m]


if __name__ == '__main__':
    s_in = "slep slap sleep step shoe shop snap slep"
    target = s_in.split(" ", 1)[0]
    dictionary = s_in.split(" ")[1:]
    wd = WordDistance()

    wd_dict = {word: wd.wd(target, word) for word in dictionary}
    wd_sort_list = sorted(wd_dict.items(), key=lambda x: x[1])

    for i in range(len(wd_sort_list)):
        if i > 2: break
        print(wd_sort_list[i][0], end=" ")

参考博客

java文本相似度计算(Levenshtein Distance算法(中文翻译:编辑距离算法))----代码和详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容