DNA序列编辑距离 | 豆包MarsCode AI刷题

67 阅读2分钟

DNA序列编辑距离问题的动态规划解法

问题分析

在这个问题中,我们需要将一个可能受损的DNA序列转换为目标序列,允许的编辑操作包括:

  1. 插入一个碱基
  2. 删除一个碱基
  3. 替换一个碱基

这个问题看似简单,但如果通过暴力枚举所有可能的编辑序列,时间复杂度将会呈指数级增长。因此,我们需要寻找一个更优的解法。仔细观察可以发现,这个问题具有最优子结构性质:如果我们知道了较短序列的编辑距离,就可以推导出较长序列的编辑距离。这正是动态规划算法的特征。

动态规划解法

我们可以定义dp[i][j]表示将dna1的前i个碱基转换为dna2的前j个碱基所需的最少编辑步骤。对于每个位置(i,j),我们需要考虑以下几种情况:

  1. 如果当前位置的碱基相同(dna1[i-1] == dna2[j-1]),则不需要编辑操作,可以直接继承dp[i-1][j-1]的值
  2. 如果当前位置的碱基不同,我们可以选择:
    • 替换操作:dp[i-1][j-1] + 1
    • 删除操作:dp[i-1][j] + 1
    • 插入操作:dp[i][j-1] + 1

在这三种操作中,我们选择产生最小编辑距离的操作。这样,通过填充动态规划表,最终dp[len(dna1)][len(dna2)]就是我们要求的最小编辑距离。

代码实现

def minDistance(dna1: str, dna2: str) -> int:
    m, n = len(dna1), len(dna2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j
    
    for i in range(1, m + 1):
        for j in range(1, n + 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] + 1,  # 替换
                    dp[i-1][j] + 1,    # 删除
                    dp[i][j-1] + 1     # 插入
                )
    
    return dp[m][n]

算法分析

这个解法的时间复杂度是O(mn),其中m和n分别是两个DNA序列的长度。空间复杂度也是O(mn),因为我们需要一个二维数组来存储中间状态。

让我们通过一个具体例子来理解算法的执行过程。假设我们有输入:dna1 = "AGT", dna2 = "AGCT"。算法会构建一个4x5的动态规划表(包含空字符串情况)。最终我们得到答案1,表示只需要插入一个碱基'C'就可以将"AGT"转换为"AGCT"。

代码验证

让我们验证一下所有测试用例:

test_cases = [
    ("AGT", "AGCT"),           # 输出: 1
    ("AACCGGTT", "AACCTTGG"),  # 输出: 4
    ("ACGT", "TGC"),           # 输出: 3
    ("A", "T"),                # 输出: 1
    ("GGGG", "TTTT")           # 输出: 4
]

for dna1, dna2 in test_cases:
    result = minDistance(dna1, dna2)
    print(f"dna1: {dna1}, dna2: {dna2}, 最小编辑距离: {result}")