经典动态规划问题:最短编辑距离算法的原理及实现

编辑距离的定义

编辑距离(Edit Distance)最常用的定义就是Levenstein距离,是由俄国科学家Vladimir Levenshtein于1965年提出的,所以编辑距离一般又称Levenshtein距离。它主要作用是测量两个字符串的差异化程度,表示字符串a至少要经过多少个操作才能转换为字符串b,这里的操作包括三种:增加、删除、替换。

举个例子:
(1)增加:对于字符串a:abc 和 字符串b:abcde,显然,只需要在字符串a的末尾增加字符’d’和’e’就能变成字符串b了,所以a和b的最短编辑距离为2。
(2)删除:对于字符串a:abcd 和字符串b:abc,显然,只需要在字符串a的末尾删除字符’d’就能变成字符串b了,所以a和b的最短编辑距离为1。
(3)替换:对于字符串a:abcd 和 字符串b:abce,显然,只需要把字符串a的’d’替换成’e’就可以了,此时二者的最短编辑距离是1。

一般字符串都是需要增加、删除、替换三者结合起来一起使用,因为字符串a到b可能存在多种变化的方法,而我们往往最关心的是最短的编辑距离,这样才能得出a和b的相似程度,最短编辑距离越小,表示a到b所需要的操作越少,a和b的相似度也就越高。因此,Levenstein距离的一个应用场景就是判断两个字符串的相似度,可以用在字符串的模糊搜索上面。

Levenshtein 算法原理

先从一个问题谈起:对于字符串"xyz"和"xcz",它们的最短距离是多少?我们从两个字符串的最后一个字符开始比较,它们都是’z’,是相同的,我们可以不用做任何操作,此时二者的距离实际上等于"xy"和"xc"的距离,即d(xyz,xcz) = d(xy,xc)。也即是说,如果在比较的过程中,遇到了相同的字符,那么二者的距离是除了这个相同字符之外剩下字符的距离。即d(i,j) = d(i - 1,j-1)。

接着,我们把问题拓展一下,最后一个字符不相同的情况:字符串A(“xyzab”)和字符串B(“axyzc”),问至少经过多少步操作可以把A变成B。

我们还是从两个字符串的最后一个字符来考察即’b’和’c’。显然二者不相同,那么我们有以下三种处理办法:
(1)增加:在A末尾增加一个’c’,那么A变成了"xyzabc",B仍然是"axyzc",由于此时末尾字符相同了,那么就变成了比较"xyzab"和"axyz"的距离,即d(xyzab,axyzc) = d(xyzab,axyz) + 1。可以写成d(i,j) = d(i,j - 1) + 1。表示下次比较的字符串B的长度减少了1,而加1表示当前进行了一次字符的操作。

(2)删除:删除A末尾的字符’b’,考察A剩下的部分与B的距离。即d(xyzab,axyzc) = d(xyza,axyzc) + 1。可以写成d(i,j) = d(i - 1,j) + 1。表示下次比较的字符串A的长度减少了1。

(3)替换:把A末尾的字符替换成’c’,这样就与B的末尾字符一样了,那么接下来就要考察出了末尾’c’部分的字符,即d(xyzab,axyzc) = d(xyza,axyz) + 1。写成d(i,j) = d(i -1,j-1) + 1表示字符串A和B的长度均减少了1。

由于我们要求的是最短的编辑距离,所以我们取以上三个步骤得出的距离的最小值为最短编辑距离。由上面的步骤可得,这是一个递归的过程,因为除掉最后一个字符之后,剩下的字符串的最后一位仍然是最后一个字符,我们仍然可以按照上面的三种操作来进行,经过这样的不断递归,直到比较到第一个字符为止,递归结束。

按照以上思路,我们很容易写出下面的方程:
最短编辑距离方程
注释:该方程的第一个条件min(i,j) = 0,表示若某一字符串为空,转换成另一个字符串所需的操作次数,显然,就是另一个字符串的长度(添加length个字符就能转换)。这个条件可以看成是递归的出口条件,此时i或j减到了0。

根据以上方程,我们能快速写出递归代码,但由于递归包含了大量的重复计算,并且如果初始字符串过长,会造成递归层次过深,容易造成栈溢出的问题,所以我们这里可以用***动态规划***来实现。**如果说递归是自顶向下的运算过程,那么动态规划就是自底向上的过程。**它从i和j的最小值开始,不断地增大i和j,同时对于一个i和j都会算出当前地最短距离,因为下一个i和j的距离会与当前的有关,所以通过一个数组来保存每一步的运算结果来避免重复的计算过程,当i和j增加到最大值length时,结果也就出来了,即d[length][length]为A、B的最短编辑距离。

动态规划中,i和j的增加需要两层循环来完成,外层循环遍历i,内层循环遍历j,也即是,对于每一行,会扫描行内的每一列的元素进行运算。因此,时间复杂度为o(n²),空间复杂度为o(n²)。

图解动态规划求最短编辑距离过程

在写代码之前,为了让读者对动态规划有一个直观的感受,笔者以表格的形式,列出动态规划是如何一步步地工作的。
下面以字符串"xyzab"和"axyzc"为例来讲解。
在这里插入图片描述
在这里插入图片描述

由上面可以看出,动态规划就是逐行逐列地运算,逐渐填满整个数组,最后得到结果恰好保存在数组的最后一行和最后一列的元素上。

代码实现

一、基本实现

public class LevenshteinDistance {

    private static int minimum(int a,int b,int c){
        return Math.min(Math.min(a,b),c);
    }

    public static int computeLevenshteinDistance(CharSequence src,CharSequence dst){
        int[][] distance = new int[src.length() + 1][dst.length() + 1];

        for (int i = 0;i <= src.length();i++)
            distance[i][0] = i;
        for (int j = 0;j <= dst.length();j++)
            distance[0][j] = j;

        for (int i = 1;i <= src.length();i++){
            for (int j = 1;j <= dst.length();j++){
                int flag = (src.charAt(i - 1) == dst.charAt(j - 1)) ? 0 : 1;
                distance[i][j] = minimum(
                        distance[i - 1][j] + 1,
                        distance[i][j - 1] + 1,
                        distance[i - 1][j - 1] + flag);
            }
        }
        return distance[src.length()][dst.length()];
    }

    //测试方法
    public static void main(String args[]){
        String s1 = "xyzab";
        String s2 = "axyzc";

        String s3 = "等啊高原";
        String s4 = "阿登高原";

        String s5 = "xyz阿登高原";
        String s6 = "1y3等啊高原x";

        System.out.println("字符串(\"" + s1 + "\")和字符串(\"" + s2 + "\")的最小编辑距离为:"+ computeLevenshteinDistance(s1,s2));
        System.out.println("字符串(\"" + s3 + "\")和字符串(\"" + s4 + "\")的最小编辑距离为:"+ computeLevenshteinDistance(s3,s4));
        System.out.println("字符串(\"" + s5 + "\")和字符串(\"" + s6 + "\")的最小编辑距离为:"+ computeLevenshteinDistance(s5,s6));

    }
}

上面的代码是利用了动态规划的思想来实现的最短编辑距离算法,它的实现与原理方程基本上是一致的,都是先对第一行和第一列的数据进行初始化,然后开始逐行逐列进行计算,填充满整个数组,即自底向上的思想,通过这样减少了大量的递归重复计算,实现了运算速度的提升。上面提到,这种实现的时间复杂度和空间复杂度都是n²级别的(实际上是m×n,两个字符串长度的乘积)。实际上,我们可以对代码进行优化,降低空间复杂度。

二、利用滚动数组进行空间复杂度的优化
滚动数组是动态规划中一种常见的优化思想。为了理解滚动数组的思想,我们先来看看如何进行空间复杂度的优化。回到原理方程,我们可以观察到d(i,j)只与上一行的元素d(i-1,j)、d(i,j-1)和d(i-1,j-1)有关,而上一行之前的元素没有关系,也就是说,对于某一行的d(i,j),我们只需要知道上一行的数据就行,别的数据都是无效数据。实际上,我们只需要两行的数组就可以了。

举个例子:还是上面的"xyzab"和"axyzc",当我们计算完第一行和第二行的数据后,到达第三行时,我们以第二行为上一行结果来计算,并把计算结果放到第一行内;到达第四行时,由于第三行的数据实际上保存在第一行,所以我们根据第一行来计算,把结果保存在第二行……以此类推,直到计算到最后一行,即不断交替使用两行数组的空间,“滚动数组”也因此得名。通过使用滚动数组的形式,我们不需要n×m的空间,只需要2×min(n,m)的空间,这样便能把空间复杂度降到线性范围内,节省了大量的空间。

利用滚动数组后的空间复杂度为o(2×n)或者o(2×m),这取决于代码的实现,即取字符串A还是B的长度为数组的列数。(因为无论把哪一个字符串作为src或dst,都是等价的,结果都是一样的。)其实我们可以通过判断A、B的长度,来选取一个最小值作为列数,此时空间复杂度变为o(2×min(n,m))。下面给出基于滚动数组的最小编辑距离的优化版本,由Java实现。

/**
     *  利用滚动数组优化过的最小编辑距离算法。空间复杂度为O(2×min(lenSrc,lenDst))
     * @param src 动态规划数组的行元素
     * @param dst 动态规划数组的列元素
     * @return
     */
    public static int computeLevenshteinDistance_Optimized(CharSequence src,CharSequence dst){
        int lenSrc = src.length() + 1;
        int lenDst = dst.length() + 1;

        CharSequence newSrc = src;
        CharSequence newDst = dst;
        //如果src长度比dst的短,表示数组的列数更多,此时我们
        //交换二者的位置,使得数组的列数变为较小的值。
        if (lenSrc < lenDst){
            newSrc = dst;
            newDst = src;
            int temp = lenDst;
            lenDst = lenSrc;
            lenSrc = temp;
        }

        //创建滚动数组,此时列数为lenDst,是最小的
        int[] cost = new int[lenDst];   //当前行依赖的上一行数据
        int[] newCost = new int[lenDst];//当前行正在修改的数据

        //对第一行进行初始化
        for(int i = 0;i < lenDst;i++)
            cost[i] = i;

        for(int i = 1;i < lenSrc;i++){
            //对第一列进行初始化
            newCost[0] = i;

            for(int j = 1;j < lenDst;j++){
                int flag = (newDst.charAt(j - 1) == newSrc.charAt(i - 1)) ? 0 : 1;

                int cost_insert = cost[j] + 1;        //表示“上面”的数据,即对应d(i - 1,j)
                int cost_replace = cost[j - 1] + flag;//表示“左上方的数据”,即对应d(i - 1,j - 1)
                int cost_delete = newCost[j - 1] + 1; //表示“左边的数据”,对应d(i,j - 1)

                newCost[j] = minimum(cost_insert,cost_replace,cost_delete); //对应d(i,j)
            }

            //把当前行的数据交换到上一行内
            int[] temp = cost;
            cost = newCost;
            newCost = temp;
        }

        return cost[lenDst - 1];
    }

把main()方法的方法调用改为上述方法,比较前后两个方法的输出结果,结果一致,符合预期。
输出结果

三、对空间复杂度的进一步优化
实际上,我们还能对这个进行进一步的优化,把空间复杂度减少为o(min(n,m)),即我们只需要一行的数组d加一个额外的临时变量就可以实现。比如说我们要修改d[i]的值时,只需知道它的左边、上边和左上方的元素的值,而左边的值就是d[i-1],上边的值是修改之前的d[i],左上方的值是d[i-1]修改之前的值。每一次需要修改d[i-1]的时候,都用临时变量把他保存起来,这样i位置就能直接获取这三个值进行比较,得到结果之后,先用这个临时变量把d[i]保存起来,然后再写入d[i]内,……以此类推,直到遍历完一行。

其核心思想是:把求得的数据,再次写回这一行数据对应下标元素的位置,而临时变量temp则保存当前位置左上方元素的值,以提供给下一个位置的计算。总的来说,数据的操作只集中在一行之内,所以空间复杂度就是o(n)。

下面以图解的形式表达这一过程,方便读者理解。
单行数组
代码实现也不复杂,有兴趣的同学可以根据上图或者思路来实现,这里就不再实现了。

好了,这篇文章写到这里就结束了,希望能对各位同学有所裨益,谢谢你们的耐心阅读~

  • 48
    点赞
  • 144
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
动态规划是计算最短编辑距离的一种常用方法。根据给定的字符串,我们可以通过构建一个二维数组来表示最短编辑距离。假设字符串str1的长度为m,字符串str2的长度为n,我们可以定义一个大小为(m+1)×(n+1)的二维数组dp。 动态规划的推导过程如下: 1. 初始化dp数组,dp[i]表示将str1的前i个字符转换为空字符串的最小编辑距离,dp[j]表示将空字符串转换为str2的前j个字符的最小编辑距离。因此,dp[i] = i,dp[j] = j。 2. 遍历数组dp,计算dp[i][j]的值。 - 如果str1的第i个字符等于str2的第j个字符(即str1[i-1] == str2[j-1]),则dp[i][j] = dp[i-1][j-1],表示不需要进行编辑操作。 - 否则,dp[i][j]的值可以通过以下三种操作得到: - 如果将str1的前i-1个字符转换为str2的前j个字符的最小编辑距离为dp[i-1][j],则将str1的第i个字符删除,得到str1的前i-1个字符和str2的前j个字符的最小编辑距离为dp[i][j] = dp[i-1][j] + 1。 - 如果将str1的前i个字符转换为str2的前j-1个字符的最小编辑距离为dp[i][j-1],则将str2的第j个字符插入到str1的第i+1个位置,得到str1的前i个字符和str2的前j个字符的最小编辑距离为dp[i][j] = dp[i][j-1] + 1。 - 如果将str1的前i-1个字符转换为str2的前j-1个字符的最小编辑距离为dp[i-1][j-1],则将str1的第i个字符替换为str2的第j个字符,得到str1的前i个字符和str2的前j个字符的最小编辑距离为dp[i][j] = dp[i-1][j-1] + 1。 - 取上述三种操作中的最小值作为dp[i][j]的值。 3. 最终,最小编辑距离为dp[m][n]。 在本例中,示例输入为str1="ABC",str2="DCB",则根据动态规划的推导过程,可以计算得到最小编辑距离为3。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值