这道题虽然标的是简单,但是是一道很经典的DP问题,接下来将详细讲解。
问题描述
小R正在研究DNA序列,他需要一个函数来计算将一个受损DNA序列(dna1)转换成一个未受损序列(dna2)所需的最少编辑步骤。编辑步骤包括:增加一个碱基、删除一个碱基或替换一个碱基。
测试样例
样例1:
输入:
dna1 = "AGT",dna2 = "AGCT"
输出:1
样例2:
输入:
dna1 = "AACCGGTT",dna2 = "AACCTTGG"
输出:4
样例3:
输入:
dna1 = "ACGT",dna2 = "TGC"
输出:3
样例4:
输入:
dna1 = "A",dna2 = "T"
输出:1
样例5:
输入:
dna1 = "GGGG",dna2 = "TTTT"
输出:4
问题分析
小R的DNA序列编辑问题是一个典型的编辑距离问题,也称为Levenshtein距离问题。编辑距离是指将一个字符串转换成另一个字符串所需的最少编辑操作步骤,包括增加一个字符、删除一个字符或替换一个字符。
解题思路
-
定义状态:
dp[i][j]
表示将字符串dna1
的前i
个字符转换成字符串dna2
的前j
个字符所需的最少编辑步骤。
-
状态转移:
-
对于字符串
dna1
的第i
个字符和字符串dna2
的第j
个字符,我们有以下几种情况:- 如果
dna1[i-1] == dna2[j-1]
,则不需要编辑操作,dp[i][j] = dp[i-1][j-1]
。 - 如果
dna1[i-1] != dna2[j-1]
,则需要进行替换操作,dp[i][j] = dp[i-1][j-1] + 1
。 - 如果我们删除
dna1
的第i
个字符,则dp[i][j] = dp[i-1][j] + 1
。 - 如果我们在
dna1
的第i
个字符后增加一个字符,则dp[i][j] = dp[i][j-1] + 1
。
- 如果
-
因此,
dp[i][j]
的值是上述几种情况的最小值。
-
-
初始化:
dp[0][j] = j
,因为将一个空字符串转换成长度为j
的字符串需要进行j
次增加操作。dp[i][0] = i
,因为将长度为i
的字符串转换成一个空字符串需要进行i
次删除操作。
-
计算顺序:
- 我们从左到右、从上到下遍历动态规划表,确保在计算
dp[i][j]
时,其左侧、上方和左上方的值已经被计算过。
- 我们从左到右、从上到下遍历动态规划表,确保在计算
-
返回结果:
- 最终结果为
dp[l1][l2]
,即将整个字符串dna1
转换成整个字符串dna2
所需的最少编辑步骤。
- 最终结果为
解题代码
def solution(dna1, dna2):
l1 = len(dna1)
l2 = len(dna2)
# 初始化动态规划表
dp = [[0] * (l2 + 1) for _ in range(l1 + 1)]
# 初始化第一行和第一列
for i in range(l1 + 1):
dp[i][0] = i
for j in range(l2 + 1):
dp[0][j] = j
# 填充动态规划表
for i in range(1, l1 + 1):
for j in range(1, l2 + 1):
dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1)
if dna1[i-1] == dna2[j-1]:
dp[i][j] = min(dp[i][j], dp[i-1][j-1])
else:
dp[i][j] = min(dp[i][j], dp[i-1][j-1] + 1)
return dp[l1][l2]
if __name__ == "__main__":
# 你可以在这里添加更多的测试用例
print(solution("AGCTTAGC", "AGCTAGCT") == 2)
print(solution("AGCCGAGC", "GCTAGCT") == 4)
总结
-
时间复杂度:
- 我们需要遍历整个动态规划表,其大小为 (l1 + 1) * (l2 + 1)。
- 因此,总的时间复杂度是 O(l1 * l2)。
-
空间复杂度:
- 我们需要一个二维数组
dp
来存储状态,其大小为 (l1 + 1) * (l2 + 1)。 - 因此,总的空间复杂度是 O(l1 * l2)。
- 我们需要一个二维数组
这个问题在生物信息学、自然语言处理和计算机科学等领域有着广泛的应用。