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

68 阅读4分钟

一、古生物DNA序列血缘分析

问题描述

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

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

  • dna1: 第一条DNA序列。
  • dna2: 第二条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

二、解题思路

(1)理解问题

根据题目的要求可以知道,我们需要计算两条DNA序列之间的最小变异次数。DNA变异可以通过三种方式发生:插入一个核苷酸、删除一个核苷酸或替换一个核苷酸。那么这个问题可以转化为计算两个字符串之间的编辑距离(Levenshtein Distance)。

(2)数据结构的选择

经过思考,选择使用动态规划(Dynamic Programming)来解决这个问题。动态规划的核心思想是将问题分解为子问题,并通过存储子问题的解来避免重复计算。

(3)算法步骤

1.定义状态

设 dp[i][j] 表示 dna1 的前 i 个字符和 dna2 的前 j 个字符之间的最小变异次数。

2.状态转移方程

如果 dna1[i-1] == dna2[j-1],则 dp[i][j] = dp[i-1][j-1],即不需要任何变异。 否则,dp[i][j] 可以通过以下三种操作之一得到:

插入一个字符到 dna1dp[i][j] = dp[i][j-1] + 1

删除一个字符从 dna1dp[i][j] = dp[i-1][j] + 1

替换一个字符在 dna1dp[i][j] = dp[i-1][j-1] + 1

因此,dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1

3.初始条件

dp[0][j] = j,表示将空字符串变成 dna2 的前 j 个字符需要 j 次插入操作。

dp[i][0] = i,表示将 dna1 的前 i 个字符变成空字符串需要 i 次删除操作。

4.最终结果

dp[len(dna1)][len(dna2)] 即为所求的最小变异次数。

三、最终代码

def solution(dna1, dna2):
    m, n = len(dna1), len(dna2)
    # 创建一个 (m+1) x (n+1) 的二维数组 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
    
    # 填充 dp 数组
    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], dp[i][j - 1], dp[i - 1][j - 1]) + 1
    
    # 返回最终结果
    return dp[m][n]

if __name__ == "__main__":
    
    print(solution("AGT", "AGCT") == 1)
    print(solution("", "ACGT") == 4)
    print(solution("GCTAGCAT", "ACGT") == 5)

四、对代码的详细解释

(1)创建二维数组 dp

dp 是一个 (m+1) x (n+1) 的二维数组,其中 m 和 n 分别是 dna1 和 dna2 的长度。

dp[i][j] 表示 dna1 的前 i 个字符和 dna2 的前 j 个字符之间的最小变异次数。

(2)初始化边界条件

当 dna2 为空时,将 dna1 变为空需要删除 dna1 的所有字符,因此 dp[i][0] = i

当 dna1 为空时,将空字符串变为 dna2 需要插入 dna2 的所有字符,因此 dp[0][j] = j

(3)填充 dp 数组

使用两层循环遍历 dna1 和 dna2 的所有字符。如果 dna1[i-1] == dna2[j-1],则 dp[i][j] = dp[i-1][j-1],即不需要任何变异。否则,dp[i][j] 就会通过上面状态转移方程中提到的三个操作中的其中一个操作得到。最终返回 dp[m][n],即 dna1 和 dna2 之间的最小变异次数。

五、总结

通过使用动态规划的方法,我们可以有效地计算出两条DNA序列之间的最小变异次数,实现对这个问题的解答。这个方法的时间复杂度为 O(m * n),其中 m 和 n 分别是两条DNA序列的长度。