基于动态规划的最小编辑距离(Minimum Edit Distance)算法详解
我将为您讲解一个经典的NLP序列比对算法——最小编辑距离。您提供的列表中虽然包含"编辑距离"、"动态规划"等词汇,但并未有题目完全匹配此算法的完整教学讲解,因此我将系统地介绍它。
一、题目描述
最小编辑距离(Minimum Edit Distance,通常指Levenshtein Distance)用于量化两个字符串之间的相似程度。其定义为:将一个字符串(源字符串)通过插入、删除、替换三种基本字符操作,转换为另一个字符串(目标字符串)所需的最少操作次数。这个算法在拼写纠错、基因序列比对、文本相似度计算等领域有广泛应用。
核心问题:给定两个字符串source和target,计算它们之间的最小编辑距离。
例如:
- 字符串A: "intention"
- 字符串B: "execution"
它们的最小编辑距离是5(一种可能操作序列:intention → inention(删除t)→ enention(替换i为e)→ exention(替换n为x)→ exection(替换n为c)→ execution(插入u))。
二、算法原理与解题过程
我们将采用动态规划(Dynamic Programming)来解决此问题,其核心思想是将大问题分解为相互重叠的子问题,并存储子问题的解以避免重复计算。
步骤1:定义状态与子问题
我们定义dp[i][j]为:将源字符串的前i个字符(即source[0:i])转换为目标字符串的前j个字符(即target[0:j])所需的最小操作次数。
i的范围是0到len(source),j的范围是0到len(target)。- 当
i=0时,意味着源字符串为空,要转换为target[0:j],需要进行j次插入操作。因此dp[0][j] = j。 - 当
j=0时,意味着目标字符串为空,要将source[0:i]转换为空,需要进行i次删除操作。因此dp[i][0] = i。
步骤2:建立状态转移方程
这是算法的核心。当我们已知如何转换较小的子串时,如何计算dp[i][j]?考虑对源字符串的最后一个字符source[i-1](注意索引)可以采取的操作:
- 删除:如果我们知道如何将
source[0:i-1]转换为target[0:j](即dp[i-1][j]),那么只需要在完成这个转换后,再删除source[i-1]即可。操作次数为dp[i-1][j] + 1。 - 插入:如果我们知道如何将
source[0:i]转换为target[0:j-1](即dp[i][j-1]),那么只需要在完成这个转换后,再插入字符target[j-1]即可。操作次数为dp[i][j-1] + 1。 - 替换/匹配:
- 如果
source[i-1]等于target[j-1],则这两个字符匹配,不需要额外操作。操作次数等于将source[0:i-1]转换为target[0:j-1]的次数,即dp[i-1][j-1]。 - 如果
source[i-1]不等于target[j-1],则需要将source[i-1]替换为target[j-1]。操作次数等于将source[0:i-1]转换为target[0:j-1]的次数再加1,即dp[i-1][j-1] + 1。
- 如果
dp[i][j]的值应为以上三种可能情况中的最小值。因此,状态转移方程为:
如果 source[i-1] == target[j-1]:
cost = 0
否则:
cost = 1
dp[i][j] = min(
dp[i-1][j] + 1, // 删除
dp[i][j-1] + 1, // 插入
dp[i-1][j-1] + cost // 替换或匹配
)
步骤3:构建动态规划表格(以"horse"和"ros"为例)
我们通过一个表格来可视化计算过程。行i对应源字符串"horse",列j对应目标字符串"ros"。初始行为i=0(源串为空),初始列为j=0(目标串为空)。
初始化表格:
| "" | r | o | s | |
|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 |
| h | 1 | |||
| o | 2 | |||
| r | 3 | |||
| s | 4 | |||
| e | 5 |
计算dp[1][1]("h" -> "r"):
- 删除:
dp[0][1] + 1 = 1 + 1 = 2(删除'h',然后从""到"r") - 插入:
dp[1][0] + 1 = 1 + 1 = 2(从"h"到"",再插入'r') - 替换:
source[0]('h') != target[0]('r'),所以dp[0][0] + 1 = 0 + 1 = 1 min(2, 2, 1) = 1
| "" | r | o | s | |
|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 |
| h | 1 | 1 | ||
| o | 2 | |||
| r | 3 | |||
| s | 4 | |||
| e | 5 |
计算dp[1][2]("h" -> "ro"):
- 删除:
dp[0][2] + 1 = 2 + 1 = 3 - 插入:
dp[1][1] + 1 = 1 + 1 = 2 - 替换:
'h' != 'o',dp[0][1] + 1 = 1 + 1 = 2 min(3, 2, 2) = 2
以此类推,完成整个表格:
| "" | r | o | s | |
|---|---|---|---|---|
| "" | 0 | 1 | 2 | 3 |
| h | 1 | 1 | 2 | 3 |
| o | 2 | 2 | 1 | 2 |
| r | 3 | 2 | 2 | 2 |
| s | 4 | 3 | 3 | 2 |
| e | 5 | 4 | 4 | 3 |
最终结果:dp[5][3] = 3。即"horse"到"ros"的最小编辑距离是3。
一种操作路径是:horse -> rorse(将'h'替换为'r') -> rose(删除'r') -> ros(删除'e')。
步骤4:算法复杂度分析
- 时间复杂度:
O(m * n),其中m和n分别是源字符串和目标字符串的长度。因为我们需要填充一个(m+1) * (n+1)的DP表格。 - 空间复杂度:朴素的DP实现需要
O(m * n)的存储空间。但可以优化到O(min(m, n)),因为计算dp[i][j]时,只需要前一行的dp[i-1][*]和当前行已计算的部分dp[i][*]即可。
三、关键点与扩展
- 操作的代价:基本Levenshtein距离中,插入、删除、替换的代价均为1。可以扩展为加权编辑距离,为不同操作赋予不同代价(如替换的代价可能为2,插入/删除为1)。
- 回溯获取操作序列:在填充DP表时,可以同时记录每一步选择了哪种操作(删除、插入、替换/匹配)。在计算完成后,从
dp[m][n]开始,根据记录反向回溯,即可得到具体的操作序列。 - 与最长公共子序列(LCS)的关系:LCS可以通过编辑距离来理解。从字符串A到B的最小编辑距离
d,与它们的长度|A|、|B|以及LCS长度L满足关系:d = |A| + |B| - 2*L。
最小编辑距离算法是动态规划在NLP中最经典、最直观的应用之一,其思想是许多更复杂序列对齐和相似度计算算法的基础。