古生物DNA序列血缘分析| 豆包MarsCode AI刷题

45 阅读5分钟

小U是一位古生物学家,正在研究不同物种之间的血缘关系。为了分析两种古生物的血缘远近,她需要比较它们的DNA序列。DNA由四种核苷酸A、C、G、T组成,并且可能通过三种方式发生变异:添加一个核苷酸、删除一个核苷酸或替换一个核苷酸。小U认为两条DNA序列之间的最小变异次数可以反映它们之间的血缘关系:变异次数越少,血缘关系越近。

你的任务是编写一个算法,帮助小U计算两条DNA序列之间所需的最小变异次数。

输入:

  • dna1: 第一条DNA序列。
  • dna2: 第二条DNA序列。

输出:

  • 返回两条DNA序列之间的最小变异次数。

测试样例:

  • 样例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

这个问题可以用经典的**编辑距离(Edit Distance)**算法解决,具体来说是动态规划(Dynamic Programming, DP)。编辑距离算法计算将一个字符串转换为另一个字符串所需的最小操作次数,这些操作包括插入、删除和替换。

下面是解法的完整步骤和代码实现:

算法步骤

  1. 初始化动态规划表:创建一个大小为 (m+1)×(n+1)(m+1) \times (n+1)(m+1)×(n+1) 的二维数组 dp,其中 mmm 和 nnn 分别是两条DNA序列的长度。

    • dp[i][j]dp[i][j]dp[i][j] 表示将 dna1[0..i-1] 转换为 dna2[0..j-1] 的最小变异次数。
  2. 边界条件

    • dna1 为空,则需要插入所有字符才能变成 dna2,即 dp[0][j]=jdp[0][j] = jdp[0][j]=j。
    • dna2 为空,则需要删除所有字符才能变成 dna1,即 dp[i][0]=idp[i][0] = idp[i][0]=i。
  3. 状态转移方程

    • dna1[i-1] == dna2[j-1],则无需操作,延续前状态:
      dp[i][j]=dp[i−1][j−1]dp[i][j] = dp[i-1][j-1]dp[i][j]=dp[i−1][j−1]

    • 否则需要考虑三种操作:

      • 替换:dp[i−1][j−1]+1dp[i-1][j-1] + 1dp[i−1][j−1]+1
      • 插入:dp[i][j−1]+1dp[i][j-1] + 1dp[i][j−1]+1
      • 删除:dp[i−1][j]+1dp[i-1][j] + 1dp[i−1][j]+1
      • 取三者的最小值:
        dp[i][j]=min⁡(dp[i−1][j−1]+1,dp[i][j−1]+1,dp[i−1][j]+1)dp[i][j] = \min(dp[i-1][j-1] + 1, dp[i][j-1] + 1, dp[i-1][j] + 1)dp[i][j]=min(dp[i−1][j−1]+1,dp[i][j−1]+1,dp[i−1][j]+1)。
  4. 最终结果dp[m][n] 即为所需的最小变异次数。


Python代码实现

python
Copy code
def min_mutation_distance(dna1, dna2):
    m, n = len(dna1), len(dna2)
    # 初始化 dp 数组
    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][j - 1] + 1,      # 插入
                    dp[i - 1][j] + 1       # 删除
                )

    return dp[m][n]

# 测试样例
print(min_mutation_distance("AGT", "AGCT"))       # 输出 1
print(min_mutation_distance("AACCGGTT", "AACCTTGG"))  # 输出 4
print(min_mutation_distance("ACGT", "TGC"))       # 输出 3
print(min_mutation_distance("A", "T"))           # 输出 1
print(min_mutation_distance("GGGG", "TTTT"))     # 输出 4

代码解析

  1. 时间复杂度:O(m×n)O(m \times n)O(m×n),因为需要计算整个 dp表。
  2. 空间复杂度:O(m×n)O(m \times n)O(m×n),存储整个 dp 表。

我们可以通过滚动数组优化动态规划的空间复杂度。因为在动态规划的计算过程中,每次仅依赖当前行和上一行的数据。因此,我们只需要维护两行(或一列)数据即可,而不需要存储整个 dp表。

优化后的算法

思路

  1. 用两个一维数组 prevcurr 表示当前行和上一行。
  2. 初始化 prev 为边界条件(对应 dp[0][j])。
  3. 在迭代中,逐行更新 curr,并在每次循环结束后将 curr 赋值给 prev
  4. 最终,结果保存在最后一次更新的 currprev 中。

Python代码

python
Copy code
def min_mutation_distance(dna1, dna2):
    m, n = len(dna1), len(dna2)
    
    # 用于存储当前行和上一行的数据
    prev = list(range(n + 1))
    curr = [0] * (n + 1)

    # 动态规划迭代
    for i in range(1, m + 1):
        curr[0] = i  # 边界条件:对应 dp[i][0]
        for j in range(1, n + 1):
            if dna1[i - 1] == dna2[j - 1]:
                curr[j] = prev[j - 1]  # 无需操作
            else:
                curr[j] = min(
                    prev[j - 1] + 1,  # 替换
                    curr[j - 1] + 1,  # 插入
                    prev[j] + 1       # 删除
                )
        # 将当前行变为上一行
        prev, curr = curr, prev

    # 返回最终结果
    return prev[n]

# 测试样例
print(min_mutation_distance("AGT", "AGCT"))       # 输出 1
print(min_mutation_distance("AACCGGTT", "AACCTTGG"))  # 输出 4
print(min_mutation_distance("ACGT", "TGC"))       # 输出 3
print(min_mutation_distance("A", "T"))           # 输出 1
print(min_mutation_distance("GGGG", "TTTT"))     # 输出 4

优化分析

  1. 时间复杂度
    O(m×n)O(m \times n)O(m×n),与未优化版本相同,遍历了所有状态。
  2. 空间复杂度
    O(n)O(n)O(n),只需要存储两行长度为 nnn 的数组,相比未优化的 O(m×n)O(m \times n)O(m×n) 大幅降低。

输出结果

运行优化后的代码,所有测试样例输出结果与未优化版本一致。

  • 样例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

这个优化版本在处理长序列时可以显著节省内存,非常适合大规模 DNA 序列比较的场景。