81.古生物DNA序列血缘分析
一、问题描述
小 U 作为古生物学家,致力于研究不同古生物物种间的血缘关系。在分析两种古生物的血缘远近时,需通过比较它们的 DNA 序列来确定。DNA 序列由四种核苷酸(A、C、G、T)构成,且存在三种可能的变异方式:添加一个核苷酸、删除一个核苷酸或者替换一个核苷酸。小 U 认为两条 DNA 序列之间的最小变异次数能够反映它们之间的血缘关系,即变异次数越少,两种古生物的血缘关系越近。 我们的任务是编写一个算法,能够根据给定的两条 DNA 序列(分别记为 dna1 和 dna2),准确计算出这两条 DNA 序列之间所需的最小变异次数。
二、思路解析
本题采用动态规划的思路来解决计算两条 DNA 序列最小变异次数的问题,具体如下:
- 定义状态:
- 创建一个二维数组
dp,其中dp[i][j]表示dna1的前i个核苷酸与dna2的前j个核苷酸之间的最小变异次数。通过这样的定义,我们可以将原问题逐步分解为子问题,即计算不同长度前缀的 DNA 序列之间的最小变异次数。
- 创建一个二维数组
- 确定边界条件:
- 当
dna1为空序列(即i = 0)时,要使它与dna2的前j个核苷酸匹配,需要进行j次添加操作,所以dp[0][j] = j。 - 同理,当
dna2为空序列(即j = 0)时,要使它与dna1的前i个核苷酸匹配,需要进行i次添加操作,所以dp[i][0] = i。
- 当
- 状态转移方程:
- 对于
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。
- 对于
三、解题步骤
- 初始化二维数组:
- 根据输入的两条 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的不同长度前缀匹配所需的添加操作次数。
- 根据输入的两条 DNA 序列
- 填充二维数组:
- 通过两层嵌套的循环遍历
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。
- 如果相同,根据状态转移方程,将
- 通过两层嵌套的循环遍历
- 获取最终结果:
- 当完成对整个
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]
- 函数定义与参数接收:
solution函数接受两个参数dna1和dna2,分别代表两条需要比较的 DNA 序列。
- 二维数组创建与初始化:
m, n = len(dna1), len(dna2)获取两条 DNA 序列的长度。dp = [[0] * (n + 1) for _ in range(m + 1)]创建一个大小为(m + 1)×(n + 1)的二维数组dp,并初始化为全0。- 接着通过两个循环分别初始化
dp数组的第一行和第一列,对应前面提到的边界条件的处理。
- 填充二维数组逻辑:
- 两层嵌套的循环
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]的值。
- 两层嵌套的循环
- 返回最终结果:
return dp[m][n]返回dp数组右下角的元素,即两条完整 DNA 序列之间的最小变异次数。
五、知识总结
- 动态规划:
- 本题核心运用了动态规划的思想。动态规划是一种通过将原问题分解为一系列相互关联的子问题,并通过求解子问题的最优解来构建原问题最优解的算法策略。在本题中,通过定义二维数组
dp来表示不同长度前缀的 DNA 序列之间的最小变异次数,将计算两条完整 DNA 序列之间最小变异次数的问题转化为逐步计算不同长度前缀的子问题,利用已计算出的子问题的最优解(存储在dp数组中)来推导后续子问题的解,最终得到原问题的解。 - 关键步骤包括确定状态(如本题中的
dp[i][j])、找出边界条件(如dp[0][j]和dp[i][0]的初始化)以及推导状态转移方程(如根据核苷酸是否相同来确定dp[i][j]的更新方式)。
- 本题核心运用了动态规划的思想。动态规划是一种通过将原问题分解为一系列相互关联的子问题,并通过求解子问题的最优解来构建原问题最优解的算法策略。在本题中,通过定义二维数组
- 字符串处理:
- 在处理 DNA 序列时,涉及到对字符串的操作,如通过索引访问字符串中的单个核苷酸(如
dna1[i - 1]和dna2[j - 1])。了解如何在程序中正确地处理字符串元素以及根据字符串长度进行循环遍历等操作是解决此类问题的基础。
- 在处理 DNA 序列时,涉及到对字符串的操作,如通过索引访问字符串中的单个核苷酸(如
- 数组操作:
- 创建和使用二维数组是本题的重要部分。掌握如何根据问题需求创建合适大小的二维数组(如
dp数组的创建),以及如何通过循环遍历数组元素并根据特定条件进行更新(如填充dp数组的过程)是实现动态规划算法的关键技能。
- 创建和使用二维数组是本题的重要部分。掌握如何根据问题需求创建合适大小的二维数组(如
- 比较与选择操作:
- 在代码中多次用到了比较操作(如判断两个核苷酸是否相同)和选择操作(如从三种变异操作对应的状态中选取最小值)。熟练掌握这些操作的实现方式以及它们在算法中的作用,有助于构建高效准确的算法逻辑。