DNA序列编辑距离
一、题目解析
题目描述
小 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
二、题目思路
1. 问题分析
这道题是一个典型的动态规划(Dynamic Programming)问题。其核心是计算两个字符串之间的编辑距离(Edit Distance),在学术界也被称为莱文斯坦距离(Levenshtein Distance)。
2. 动态规划的解题思路
我们定义一个二维数组 dp,其中 dp[i][j] 表示将 dna1[:i] 转换为 dna2[:j] 所需的最少编辑步骤。
状态转移方程
对于任意位置 (i, j),我们有以下三种编辑操作:
- 增加一个碱基:相当于
dna1[:i]变为dna2[:j-1],然后添加一个字符。因此dp[i][j] = dp[i][j-1] + 1。 - 删除一个碱基:相当于
dna1[:i-1]变为dna2[:j],然后删除一个字符。因此dp[i][j] = dp[i-1][j] + 1。 - 替换一个碱基:如果
dna1[i-1] != dna2[j-1],需要替换。因此dp[i][j] = 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] + cost)
less
复制代码
其中,cost 为替换代价:
- 当
dna1[i-1] == dna2[j-1]时,cost = 0; - 否则,
cost = 1。
边界条件
- 当
dna1或dna2为空时,dp[i][0] = i或dp[0][j] = j。
时间和空间复杂度
- 时间复杂度:二维数组的填充过程为
O(n1 * n2),其中n1和n2分别为dna1和dna2的长度。 - 空间复杂度:二维数组占用
O(n1 * n2)的额外空间。
三、代码实现
def solution(dna1, dna2):
n1 = len(dna1)
n2 = len(dna2)
# 特殊情况:当任意序列为空时,返回另一序列的长度
if n1 * n2 == 0:
return n1 + n2
# 初始化DP数组
dp = [[0 for _ in range(n2 + 1)] for _ in range(n1 + 1)]
# 初始化边界
for i in range(n1 + 1):
dp[i][0] = i
for j in range(n2 + 1):
dp[0][j] = j
# 动态规划填充DP数组
for i in range(1, n1 + 1):
for j in range(1, n2 + 1):
left = dp[i][j - 1] + 1
down = dp[i - 1][j] + 1
leftdown = dp[i - 1][j - 1]
if dna1[i - 1] != dna2[j - 1]:
leftdown += 1
dp[i][j] = min(left, down, leftdown)
return dp[n1][n2]
# 测试用例
if __name__ == "__main__":
print(solution("AGCTTAGC", "AGCTAGCT") == 2)
print(solution("AGCCGAGC", "GCTAGCT") == 4)
print(solution("AGT", "AGCT") == 1)
print(solution("GGGG", "TTTT") == 4)
四、代码详解
1. 初始化
dp = [[0 for _ in range(n2 + 1)] for _ in range(n1 + 1)]
dp[i][0]:表示将dna1[:i]转换为空串的代价。dp[0][j]:表示将空串转换为dna2[:j]的代价。
2. 动态规划过程
for i in range(1, n1 + 1):
for j in range(1, n2 + 1):
left = dp[i][j - 1] + 1
down = dp[i - 1][j] + 1
leftdown = dp[i - 1][j - 1]
if dna1[i - 1] != dna2[j - 1]:
leftdown += 1
dp[i][j] = min(left, down, leftdown)
- 如果当前字符相等:只需要继承左上方的值
dp[i-1][j-1]。 - 如果当前字符不相等:需要加 1,表示一次替换操作。
3. 返回结果
return dp[n1][n2]
dp[n1][n2] 即为将 dna1 转换为 dna2 的最小编辑步骤。
五、知识总结
动态规划关键点
- 定义状态:
dp[i][j]表示将dna1[:i]转换为dna2[:j]所需的最少编辑步骤。 - 转移方程:通过增加、删除或替换操作,选择最小代价路径。
- 边界条件:处理空字符串的情况。
编辑距离的实际应用
- 拼写纠正:计算两个单词之间的编辑距离,判断相似度。
- DNA序列比对:通过编辑距离衡量基因序列的相似性。
- 机器翻译:评估生成的翻译与参考翻译之间的差异。
六、学习心得
-
动态规划解题的套路:
- 明确状态和状态转移方程;
- 定义边界条件;
- 构建 DP 表并填充。
-
抽象数学问题到实际场景:
- 本题从 DNA 比对引入编辑距离的概念,具有很强的实用性。
-
代码调试:
- 在实现过程中容易混淆数组的索引(如
i-1与j-1的对应关系),建议在调试时加入打印语句验证逻辑。
- 在实现过程中容易混淆数组的索引(如
七、学习建议
对初学者的建议
-
熟悉基础算法:
- 动态规划是算法学习的重点之一,建议从简单的题目开始(如斐波那契数列、最小路径和)。
-
通过例子理解代码:
- 在解题过程中,手动模拟 DP 表的构造过程,直观理解算法运行机制。
-
反复练习:
- 编辑距离相关的题目有很多变种,例如“删除操作的最小次数”“两个字符串的公共子序列”等,可以多加练习。
八、总结
通过本题的练习,我们深入理解了动态规划在字符串处理中的应用,尤其是编辑距离的算法设计与实现。在刷题过程中,结合具体问题场景(如 DNA 序列比对)可以更好地感受算法的实际意义。 希望这些总结对其他同学有所帮助,一起进步!