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

67 阅读5分钟

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),我们有以下三种编辑操作:

  1. 增加一个碱基:相当于 dna1[:i] 变为 dna2[:j-1],然后添加一个字符。因此 dp[i][j] = dp[i][j-1] + 1
  2. 删除一个碱基:相当于 dna1[:i-1] 变为 dna2[:j],然后删除一个字符。因此 dp[i][j] = dp[i-1][j] + 1
  3. 替换一个碱基:如果 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
边界条件
  • dna1dna2 为空时,dp[i][0] = idp[0][j] = j
时间和空间复杂度
  • 时间复杂度:二维数组的填充过程为 O(n1 * n2),其中 n1n2 分别为 dna1dna2 的长度。
  • 空间复杂度:二维数组占用 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 的最小编辑步骤。


五、知识总结

动态规划关键点

  1. 定义状态dp[i][j] 表示将 dna1[:i] 转换为 dna2[:j] 所需的最少编辑步骤。
  2. 转移方程:通过增加、删除或替换操作,选择最小代价路径。
  3. 边界条件:处理空字符串的情况。

编辑距离的实际应用

  • 拼写纠正:计算两个单词之间的编辑距离,判断相似度。
  • DNA序列比对:通过编辑距离衡量基因序列的相似性。
  • 机器翻译:评估生成的翻译与参考翻译之间的差异。

六、学习心得

  1. 动态规划解题的套路

    • 明确状态和状态转移方程;
    • 定义边界条件;
    • 构建 DP 表并填充。
  2. 抽象数学问题到实际场景

    • 本题从 DNA 比对引入编辑距离的概念,具有很强的实用性。
  3. 代码调试

    • 在实现过程中容易混淆数组的索引(如 i-1j-1 的对应关系),建议在调试时加入打印语句验证逻辑。

七、学习建议

对初学者的建议

  1. 熟悉基础算法

    • 动态规划是算法学习的重点之一,建议从简单的题目开始(如斐波那契数列、最小路径和)。
  2. 通过例子理解代码

    • 在解题过程中,手动模拟 DP 表的构造过程,直观理解算法运行机制。
  3. 反复练习

    • 编辑距离相关的题目有很多变种,例如“删除操作的最小次数”“两个字符串的公共子序列”等,可以多加练习。

八、总结

通过本题的练习,我们深入理解了动态规划在字符串处理中的应用,尤其是编辑距离的算法设计与实现。在刷题过程中,结合具体问题场景(如 DNA 序列比对)可以更好地感受算法的实际意义。 希望这些总结对其他同学有所帮助,一起进步!