最小编辑距离-动态规划的python实现(附源码)

问题分析

注:仔细本博客,可以保证使你理解最小编辑距离的算法,并对动态规划思想有更深刻的认知。

最小编辑距离是一个经典的动态规划问题,我认为网上很多博客、视频都没有把这个问题讲清楚,至少初学者很难理解他们的讲解,因此我会在问题分析里从我自己的朴素逻辑出发去试图分析清楚这个问题中我遇到的所有细节,希望正在阅读本博客的你不会觉得我写的太多。

“最小编辑距离”这个概念的引入是为了作为判断文本之间相似程度的一种衡量,这个概念是指:一个字符串要经过最少多少次的“插入”、“删除”、”替换“操作才能转化为另一个字符串。

要分析这个问题,从常规算法的逻辑出发,应该先对两个字符串中的第一位字符进行比较,并依次向后扫描。当然有人就会有疑问(比如我自己),人的大脑在比较两个字符串的时候,似乎是一种更加宏观、直接的比较,我们会直接将整个字符串映入脑海,并同时对字符串的不同位置进行修改,比如对于”你爱我“和”我爱你“这两个字符串,我们的大脑可以直接将位于字符串首和字符串尾部的“我”和“你”进行调换,这种转换方式是显而易见的。但是对于机器而言,常常听到一句话说“电脑是很笨的”,确实如此,机器并不具备人脑所具备的抽象思考的能力,因此无法做到上面提到的人脑的思维过程,机器只能接受我们人为赋予它的逻辑,因此最朴素的一种能被机器做到的逻辑就是从前到后依次比较、修改字符串。

接受了上面这一段的说法,我们就应该开始考虑实际操作的算法了,具体算法我将放在下一个模块“算法描述”中,在“问题分析”模块中我将理清算法由何而来。先假设我们有两个字符串str1和str2,str[i]指代该字符串中的第i个字符,我们希望计算将str2转化为str1的最小编辑距离。首先比较str1[1]和str2[1],这时会有两种情况:① str1[1] = str2[1]; ② str1[1] ≠ str2[1] 。

  • 对于情况①,我们可以直接跳过str1[1]和str2[1],然后去比较str1[2]和str2[2];
  • 对于情况②,我们可以有三种选择:
    • 由于str2[1]有可能会和str1后面的字符相等,那么我们就有保留str2[1]的理由,因此我们可以在str2[1]之前插入一个和str1[1]一样的字符,实现str1[1] = str2[1];
    • 将str2[1]修改为和str1[1]一样的字符,简单粗暴地实现str1[1] = str2[1];
    • 由于str2[1]之后地字符有可能等于str1[1],那么既然 str1[1] ≠ str2[1] ,我们就有理由直接删掉str2[1],然后再对str2后面的字符和str1[1]进行比较,此时可以对str2[2]进行和str2[1]一样的操作。

由于上面的步骤,我么有理由确信,我们可以使新的str2[1]与str1[1]相等,那么接下来将目标放在str1[1]和str2[2]上面,str2[2]和str2[1]并无什么不同,甚至可以说具有等价性,因为我们并不关心这个字符具体是什么,故显而易见我们可以对str2[2]施加与我们对str2[1]所施加的相同操作,str2[i]可以以此类推。如果对最后一位str2的字符操作完成后,发现str1长于str2,那么我们可以直接对str2末尾进行添加操作,同样如果str2长于str1,我们也可以删除多余的str2,这一点不难理解。

由于上面的步骤可以保证我们最后所有的str1[i] = str2[i],即我们可以将str2经过有限步转化为str1,接下来我们应当考虑算法的开销问题。最简单的思路是穷举法,既然对str2每个字符都有三种操作,那我们是不是可以进行简单穷举,真的就对每个字符都进行三种操作,那么假如str2有n个字符,我们将可以获得3^n种操作序列,并从中找到操作次数最少的一种。显然这种做法看起来就不太聪明,因为其中存在大量的计算结果冗余,譬如两种操作序列的前n-1步都是相同的操作,只有第n步不同,那我们就没必要对前n-1步进行两次计算,在第一次计算时保留结果,第二次直接使用是一种更好的方式。

而众所周知,动态规划就是用来解决这种具有相同的子问题结构的算法思路,也就是说,在进行第n步计算时,我们应该已经存储了前n-1步的最优操作序列,怎么保证我们可以拥有前n-1步的最优操作序列?这就要求我们从第一步开始,就对每一步操作进行判优,并存储最优操作,这样我们才能在第n步的时候,在n-1步最优序列的基础上,做出第n个最优操作,接下来我们来看具体模型。

数学模型

定义操作

  1. 增加:在str2当前比较字符前增加一个与str1对应字符相同的字符;
  2. 替换:将str2当前比较字符替换为和str1对应字符相同的字符;
  3. 删除:删掉str2当前比较字符。

具体算法

假设序列str1和str2的长度分别为m和n, 两者的编辑距离表示为D[m][n]. 则对序列进行操作时存在以下几种情况:

  1. 当str1和str2的当前比较字符相等时, 对该字符不需要进行上任何一种操作, 也就是不需要增加操作计数. 则满足条件: D[m][n] = D[m - 1][n - 1];
  2. 当str1和str2的当前比较字符不相等时, 则需要对两者之一进行编辑, 相应的,编辑距离会增加1。
    • 对str2的当前比较字符进行替换, 使之与str1相等, 则此时D[m][n] = D[m - 1][n - 1] + 1;
    • 删除str2当前的元素, 此时D[m][n] = D[m - 1][n] + 1;
    • 在str2的当前字符之前添加str1的当前字符, D[m][n] = D[m][n - 1] + 1;
    • 比较特殊的情况是, 当str2为空时, D[0][n] = n; 而当str1为空时, D[m][0] = m; 这个很好理解, 例如对于序列"“和"abcde”, 则两者的最少操作为5, 即进行5次删除/添加操作。

综上,不难推出本模型的动态规划方程为:
(图片摘自CSDN,dp[i][j]即D[m][n])
图片摘自CSDN,dp[i][j]即D[m][n]

算法示例

最小编辑距离-动态规划的python实现(附源码)_第1张图片
(图片摘自CSDN)

算法实现(程序结构)

  1. 接收输入的字符串
    在这里插入图片描述

  2. 给字符串前连接一个“#”,赋予第一个字符和其他字符同等的处理地位
    在这里插入图片描述

  3. 以处理好的str1和str2作为参数,构造edit_distance_solution函数

    • 构造动态规划矩阵和记录矩阵,并对它们进行初始化。老师的PPT上说使用回退指针,众所周知python没有指针,因此我用了一个自以为聪明的方法,使用记录矩阵,记录矩阵和动态规划矩阵大小相同,每个格子记录的是本格如何由上一格变化而来,同时根据这个记录可以回溯出操作序列,我给记录矩阵的[0][0]赋值100作为回溯终止标志。
      最小编辑距离-动态规划的python实现(附源码)_第2张图片

    • 主要算法部分:判断两个字符是否相等,然后计算替换、添加、删除操作的开销,选择最优值进行记录
      最小编辑距离-动态规划的python实现(附源码)_第3张图片

    • 更新记录矩阵对应值:由于考虑到某些时候对字符进行替换/添加/删除操作可能导致相同的开销,这一点不会影响到对最小编辑距离的计算,但其实这意味着不同的操作序列,优先选择哪种方式会导致不同的操作序列,在我的程序中仅仅计算了两种不同的优先操作顺序,但实际上对于三种操作,不同的优先度有6种。
      最小编辑距离-动态规划的python实现(附源码)_第4张图片

    • 根据记录矩阵,从矩阵的右下角根据格子的值向前进行回溯,我用2和5分别代表替换和保留操作,那么如果格子的值是2或5,就需要向[m-1][n-1]回溯,同理,我用3代表添加,此时需要向[m][n-1]回溯,用4代表删除,需要向[m-1][n]回溯,由此直到回溯到记录矩阵的左上角,此时格子的值为100,退出回溯循环。
      最小编辑距离-动态规划的python实现(附源码)_第5张图片

      注:不同的操作优先度对应了不同的记录矩阵,回溯出不同的操作序列。

程序执行效果

最小编辑距离-动态规划的python实现(附源码)_第6张图片

  1. 同理,本程序也能处理英文字符串,为了篇幅不过于冗余,就不贴图了

完整源码

以下为源码的Github地址,有numpy库就能跑通,希望大家在应付作业的同时可以通过看我的博客把这个算法彻底搞懂。
另外,记得star!!!!
https://github.com/CiAurora/Minimum-editing-distance-python

你可能感兴趣的:(python,动态规划,算法)