DNA序列编辑距离|Marscode AI刷题

37 阅读4分钟

最小编辑距离(Levenshtein 距离)

在解决本题的过程中,我们要实现一个函数来计算将一个受损的DNA序列转换为一个未受损的DNA序列所需的最少编辑步骤。这是一个经典的 字符串编辑距离 问题,通常称为 Levenshtein距离。通过这道题,我重新梳理了编辑距离问题的常见解法——动态规划(Dynamic Programming,简称DP),并理解了其应用过程。接下来,我将详细讲解该问题的思路与解法。

问题分析

最小编辑距离问题的本质是给定两个字符串,我们需要通过最少的操作将一个字符串转换成另一个字符串。操作包括:

  • 插入字符:向字符串中添加一个字符。
  • 删除字符:删除字符串中的一个字符。
  • 替换字符:将字符串中的一个字符替换为另一个字符。

例如,给定两个DNA序列:

  • dna1 = "AGT"
  • dna2 = "AGCT"

我们需要通过最少的操作将 dna1 转换成 dna2。从 AGT 转换为 AGCT 的最优路径是 插入字符 "C" ,因此最少编辑步骤为 1。

动态规划思想

为了解决这个问题,我们采用动态规划(DP)的方法。DP的核心思想是将一个大问题转化为小问题,通过小问题的解构建最终的解。

  1. 定义状态: 设 dp[i][j] 表示将 dna1[0...i-1] 转换为 dna2[0...j-1] 所需的最小编辑步骤。

    • dp[i][0] 表示将 dna1 的前 i 个字符转换为空字符串,这需要 i 步删除操作。
    • dp[0][j] 表示将空字符串转换为 dna2 的前 j 个字符,这需要 j 步插入操作。
  2. 状态转移方程

    • 如果 dna1[i-1] == dna2[j-1],则 dp[i][j] = dp[i-1][j-1],即不需要任何操作。

    • 如果 dna1[i-1] != dna2[j-1],则有三种可能的操作:

      1. 删除:将 dna1[i-1] 删除,得到 dp[i-1][j] + 1
      2. 插入:向 dna1 中插入 dna2[j-1],得到 dp[i][j-1] + 1
      3. 替换:将 dna1[i-1] 替换为 dna2[j-1],得到 dp[i-1][j-1] + 1

    因此,状态转移方程为:

    dp[i][j] = min(dp[i-1][j] + 1,  # 删除
                   dp[i][j-1] + 1,  # 插入
                   dp[i-1][j-1] + 1)  # 替换
    
  3. 边界条件

    • dp[i][0] = i,表示将 dna1 的前 i 个字符转化为空字符串需要 i 步删除操作。
    • dp[0][j] = j,表示将空字符串转换为 dna2 的前 j 个字符需要 j 步插入操作。

实现步骤

通过上述分析,我们可以使用一个二维数组 dp 来实现动态规划,逐步计算出最小编辑距离。具体实现步骤如下:

  1. 创建一个二维数组 dp,其大小为 (len1+1) x (len2+1),其中 len1len2 分别是 dna1dna2 的长度。
  2. 初始化数组的第一行和第一列,表示将一个字符串转换为空字符串所需的步数。
  3. 遍历整个二维数组,计算出每个位置的最小编辑距离。
  4. 最终结果存储在 dp[len1][len2] 中,表示将 dna1 完全转换为 dna2 所需的最小编辑步数。

代码实现

def solution(dna1, dna2):
    len1, len2 = len(dna1), len(dna2)

    # 创建DP数组,并初始化
    dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
    
    # 初始化边界条件
    for i in range(len1 + 1):
        dp[i][0] = i  # 删除所有字符
    for j in range(len2 + 1):
        dp[0][j] = j  # 添加所有字符
    
    # 填充DP表
    for i in range(1, len1 + 1):
        for j in range(1, len2 + 1):
            if dna1[i - 1] == dna2[j - 1]:  # 字符相同,不需要操作
                dp[i][j] = dp[i - 1][j - 1]
            else:  # 需要替换、插入或删除
                dp[i][j] = min(dp[i - 1][j] + 1,  # 删除
                               dp[i][j - 1] + 1,  # 插入
                               dp[i - 1][j - 1] + 1)  # 替换

    # 返回最小编辑步骤
    return dp[len1][len2]

# 测试
if __name__ == "__main__":
    print(solution("AGT", "AGCT") == 1)
    print(solution("AACCGGTT", "AACCTTGG") == 4)
    print(solution("ACGT", "TGC") == 3)
    print(solution("A", "T") == 1)
    print(solution("GGGG", "TTTT") == 4)

时间与空间复杂度分析

  • 时间复杂度O(m * n),其中 mn 分别是 dna1dna2 的长度。我们需要遍历整个二维数组 dp,每个位置的计算时间为常数时间。
  • 空间复杂度O(m * n),我们使用了一个大小为 (m+1) x (n+1) 的二维数组来存储中间结果。

总结

通过本题,我们深入理解了最小编辑距离问题的动态规划解法。通过构建一个二维DP表来记录每一步操作的最小代价,最终得出将一个字符串转换为另一个字符串的最少编辑操作数。此题不仅考察了基本的动态规划技术,还加深了对字符串操作的理解,对于字符串处理问题的求解具有广泛的应用价值。