DNA序列编辑距离问题的动态规划解法
问题分析
在这个问题中,我们需要将一个可能受损的DNA序列转换为目标序列,允许的编辑操作包括:
- 插入一个碱基
- 删除一个碱基
- 替换一个碱基
这个问题看似简单,但如果通过暴力枚举所有可能的编辑序列,时间复杂度将会呈指数级增长。因此,我们需要寻找一个更优的解法。仔细观察可以发现,这个问题具有最优子结构性质:如果我们知道了较短序列的编辑距离,就可以推导出较长序列的编辑距离。这正是动态规划算法的特征。
动态规划解法
我们可以定义dp[i][j]表示将dna1的前i个碱基转换为dna2的前j个碱基所需的最少编辑步骤。对于每个位置(i,j),我们需要考虑以下几种情况:
- 如果当前位置的碱基相同(dna1[i-1] == dna2[j-1]),则不需要编辑操作,可以直接继承dp[i-1][j-1]的值
- 如果当前位置的碱基不同,我们可以选择:
- 替换操作: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}")