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

52 阅读7分钟

81.古生物DNA序列血缘分析

一、问题描述

小 U 作为古生物学家,致力于研究不同古生物物种间的血缘关系。在分析两种古生物的血缘远近时,需通过比较它们的 DNA 序列来确定。DNA 序列由四种核苷酸(A、C、G、T)构成,且存在三种可能的变异方式:添加一个核苷酸、删除一个核苷酸或者替换一个核苷酸。小 U 认为两条 DNA 序列之间的最小变异次数能够反映它们之间的血缘关系,即变异次数越少,两种古生物的血缘关系越近。 我们的任务是编写一个算法,能够根据给定的两条 DNA 序列(分别记为 dna1 和 dna2),准确计算出这两条 DNA 序列之间所需的最小变异次数。

二、思路解析

本题采用动态规划的思路来解决计算两条 DNA 序列最小变异次数的问题,具体如下:

  1. 定义状态
    • 创建一个二维数组 dp,其中 dp[i][j] 表示 dna1 的前 i 个核苷酸与 dna2 的前 j 个核苷酸之间的最小变异次数。通过这样的定义,我们可以将原问题逐步分解为子问题,即计算不同长度前缀的 DNA 序列之间的最小变异次数。
  2. 确定边界条件
    • 当 dna1 为空序列(即 i = 0)时,要使它与 dna2 的前 j 个核苷酸匹配,需要进行 j 次添加操作,所以 dp[0][j] = j
    • 同理,当 dna2 为空序列(即 j = 0)时,要使它与 dna1 的前 i 个核苷酸匹配,需要进行 i 次添加操作,所以 dp[i][0] = i
  3. 状态转移方程
    • 对于 dp[i][j]i > 0 且 j > 0),如果 dna1 的第 i 个核苷酸与 dna2 的第 j 个核苷酸相同(即 dna1[i - 1] == dna2[j - 1]),那么此时 dp[i][j] 的值就等于 dp[i - 1][j - 1],因为不需要进行任何变异操作。
    • 若两者不同,则需要考虑三种变异操作中的最优选择:
      • dp[i - 1][j] 表示删除 dna1 的第 i 个核苷酸后的最小变异次数,对应删除操作。
      • dp[i][j - 1] 表示在 dna1 中添加一个与 dna2 的第 j 个核苷酸相同的核苷酸后的最小变异次数,对应添加操作。
      • dp[i - 1][j - 1] 表示替换 dna1 的第 i 个核苷酸为 dna2 的第 j 个核苷酸后的最小变异次数,对应替换操作。
      • 所以此时 dp[i][j] 的值为这三种情况中的最小值再加 1,即 dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1

三、解题步骤

  1. 初始化二维数组
    • 根据输入的两条 DNA 序列 dna1 和 dna2 的长度 m 和 n,创建一个大小为 (m + 1)×(n + 1) 的二维数组 dp,并将其所有元素初始化为 0
    • 按照边界条件,分别初始化 dp 数组的第一行(当 i = 0)和第一列(当 j = 0)的值。第一行 dp[0][j]j 从 0 到 n)的值依次设置为 j,表示 dna1 为空序列时与 dna2 的不同长度前缀匹配所需的添加操作次数;第一列 dp[i][0]i 从 0 到 m)的值依次设置为 i,表示 dna2 为空序列时与 dna1 的不同长度前缀匹配所需的添加操作次数。
  2. 填充二维数组
    • 通过两层嵌套的循环遍历 dp 数组,外层循环控制 i(从 1 到 m),内层循环控制 j(从 1 到 n)。
    • 在每次循环中,比较 dna1 的第 i - 1 个核苷酸与 dna2 的第 j - 1 个核苷酸是否相同。
      • 如果相同,根据状态转移方程,将 dp[i][j] 设置为 dp[i - 1][j - 1]
      • 如果不同,根据状态转移方程,计算 dp[i][j] 为 min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1,即从删除、添加、替换三种操作对应的前序状态的最小变异次数中选取最小值,然后再加 1
  3. 获取最终结果
    • 当完成对整个 dp 数组的填充后,dp[m][n] 的值就代表了两条完整的 DNA 序列 dna1 和 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)]

    # 初始化 dp 数组
    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]
  1. 函数定义与参数接收
    • solution 函数接受两个参数 dna1 和 dna2,分别代表两条需要比较的 DNA 序列。
  2. 二维数组创建与初始化
    • m, n = len(dna1), len(dna2) 获取两条 DNA 序列的长度。
    • dp = [[0] * (n + 1) for _ in range(m + 1)] 创建一个大小为 (m + 1)×(n + 1) 的二维数组 dp,并初始化为全 0
    • 接着通过两个循环分别初始化 dp 数组的第一行和第一列,对应前面提到的边界条件的处理。
  3. 填充二维数组逻辑
    • 两层嵌套的循环 for i in range(1, m + 1) 和 for j in range(1, n + 1) 用于遍历除了第一行和第一列之外的 dp 数组元素。
    • 在循环内部,通过 if dna1[i - 1] == dna2[j - 1] 判断当前位置对应的两个核苷酸是否相同,然后根据不同情况按照状态转移方程更新 dp[i][j] 的值。
  4. 返回最终结果
    • return dp[m][n] 返回 dp 数组右下角的元素,即两条完整 DNA 序列之间的最小变异次数。

五、知识总结

  1. 动态规划
    • 本题核心运用了动态规划的思想。动态规划是一种通过将原问题分解为一系列相互关联的子问题,并通过求解子问题的最优解来构建原问题最优解的算法策略。在本题中,通过定义二维数组 dp 来表示不同长度前缀的 DNA 序列之间的最小变异次数,将计算两条完整 DNA 序列之间最小变异次数的问题转化为逐步计算不同长度前缀的子问题,利用已计算出的子问题的最优解(存储在 dp 数组中)来推导后续子问题的解,最终得到原问题的解。
    • 关键步骤包括确定状态(如本题中的 dp[i][j])、找出边界条件(如 dp[0][j] 和 dp[i][0] 的初始化)以及推导状态转移方程(如根据核苷酸是否相同来确定 dp[i][j] 的更新方式)。
  2. 字符串处理
    • 在处理 DNA 序列时,涉及到对字符串的操作,如通过索引访问字符串中的单个核苷酸(如 dna1[i - 1] 和 dna2[j - 1])。了解如何在程序中正确地处理字符串元素以及根据字符串长度进行循环遍历等操作是解决此类问题的基础。
  3. 数组操作
    • 创建和使用二维数组是本题的重要部分。掌握如何根据问题需求创建合适大小的二维数组(如 dp 数组的创建),以及如何通过循环遍历数组元素并根据特定条件进行更新(如填充 dp 数组的过程)是实现动态规划算法的关键技能。
  4. 比较与选择操作
    • 在代码中多次用到了比较操作(如判断两个核苷酸是否相同)和选择操作(如从三种变异操作对应的状态中选取最小值)。熟练掌握这些操作的实现方式以及它们在算法中的作用,有助于构建高效准确的算法逻辑。