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

85 阅读5分钟

古生物DNA序列血缘分析——最小编辑距离问题

在古生物学的研究中,分析不同物种之间的血缘关系通常需要比较它们的DNA序列。DNA序列由四种核苷酸(A、C、G、T)组成,而不同物种的DNA序列虽然有相似之处,但通常也存在差异。为了衡量这些差异并分析物种之间的血缘关系,我们可以通过计算两条DNA序列之间的“最小变异次数”来实现。

在这篇博客中,我们将解析如何通过最小编辑距离算法来解决这一问题。这个问题的核心是通过三种基本操作(插入、删除、替换)来将一条DNA序列转化为另一条DNA序列。最小编辑距离即为这三种操作所需的最少次数。

说人话

给定两条DNA序列dna1dna2,我们需要计算将dna1转化为dna2所需的最少操作次数。可以进行三种操作:

  • 插入操作:在dna1中插入一个字符;
  • 删除操作:从dna1中删除一个字符;
  • 替换操作:将dna1中的一个字符替换为dna2中的字符。

这三种操作的目标是使得dna1dna2完全相同。

测试用例

  1. 样例1
    输入:dna1 = "AGT", dna2 = "AGCT"
    输出:1
    解释:只需在dna1的末尾插入一个字符C,就可以使两条序列相同。

  2. 样例2
    输入:dna1 = "AACCGGTT", dna2 = "AACCTTGG"
    输出:4
    解释:需要进行四次操作,具体步骤可以是替换、删除、插入等。

  3. 样例3
    输入:dna1 = "ACGT", dna2 = "TGC"
    输出:3
    解释:需要删除A,替换CG,插入T

  4. 样例4
    输入:dna1 = "A", dna2 = "T"
    输出:1
    解释:只需替换AT

  5. 样例5
    输入:dna1 = "GGGG", dna2 = "TTTT"
    输出:4
    解释:需要替换四次GT

思路解析

dp数组的含义

这个问题是经典的“编辑距离”问题,通常通过动态规划来求解。我们可以用一个二维数组dp来表示状态,其中dp[i][j]表示将dna1的前i个字符转换为dna2的前j个字符所需的最少操作次数。

动态规划状态转移

  1. 初始化

    • dp[0][0] = 0,表示两条空字符串之间的编辑距离为0。
    • dp[i][0] = i,表示将dna1的前i个字符转为空字符串,需要进行i次删除操作。
    • dp[0][j] = j,表示将空字符串转为dna2的前j个字符,需要进行j次插入操作。
  2. 状态转移

    • 如果dna1[i-1] == dna2[j-1],那么dp[i][j] = dp[i-1][j-1],即如果当前字符相同,则不需要做任何操作,继承上一步的最小操作数。
    • 否则,dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1,这代表了三种操作:
      • 替换操作:dp[i-1][j-1] + 1
      • 删除操作:dp[i-1][j] + 1
      • 插入操作:dp[i][j-1] + 1

最终,我们的答案就存储在dp[m][n]中,其中mn分别是dna1dna2的长度。

代码详解

def solution(dna1, dna2):
    # 获取两个DNA序列的长度
    m = len(dna1)
    n = len(dna2)

    # dp[i][j] 表示将dna1的前i个字符转换为dna2的前j个字符所需的最小操作次数
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # 初始化第一行和第一列
    # 第一行:将空字符串转换为dna2的前j个字符需要j次添加操作
    for j in range(n + 1):
        dp[0][j] = j
    
    # 第一列:将dna1的前i个字符转换为空字符串需要i次删除操作
    for i in range(m + 1):
        dp[i][0] = i
    
    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], dp[i-1][j], dp[i][j-1]) + 1
    
    # 返回最终结果
    return dp[m][n]

代码解读

  1. 初始化动态规划表

    • 创建一个(m+1) x (n+1)的二维列表dp,表示从dna1的前i个字符到dna2的前j个字符的最小操作数。
  2. 边界条件的设置

    • dp[0][j]表示将空字符串转为dna2的前j个字符,只能通过插入操作来完成。
    • dp[i][0]表示将dna1的前i个字符转为空字符串,只能通过删除操作来完成。
  3. 填充动态规划表

    • 对于每一对字符dna1[i-1]dna2[j-1],我们考虑三种可能的操作,并选择其中的最小操作次数。
  4. 最终答案

    • dp[m][n]即为dna1转换为dna2的最小操作次数。

时间复杂度

  • 时间复杂度:O(m * n),其中mn分别是dna1dna2的长度。我们需要填充一个(m+1) x (n+1)的二维表。
  • 空间复杂度:O(m * n),我们需要一个大小为(m+1) x (n+1)的二维数组来存储中间结果。