古生物 DNA 序列血缘分析算法解析 | 豆包 MarsCode AI 刷题
在古生物学研究中,分析不同物种之间的血缘关系是一项极具挑战性但又意义重大的工作。其中,通过比较古生物的 DNA 序列来确定血缘远近是一种重要的方法。本次笔记将对给定的计算两条 DNA 序列之间最小变异次数的算法进行详细解析,以帮助我们更好地理解这种分析方法的原理。
问题背景
我们知道,DNA 由四种核苷酸 A、C、G、T 组成,而在基因的演变过程中,可能会通过添加一个核苷酸、删除一个核苷酸或替换一个核苷酸这三种方式发生变异。古生物学家小 U 认为,两条 DNA 序列之间的最小变异次数能够反映它们之间的血缘关系,变异次数越少,意味着血缘关系越近。 代码:C++
// 创建二维数组用于保存中间结果
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
// 初始化第一行和第一列
for (int i = 0; i <= m; ++i) {
dp[i][0] = i;
}
for (int j = 0; j <= n; ++j) {
dp[0][j] = j;
}
// 动态规划计算最小变异次数
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (dna1[i - 1] == dna2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = std::min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
}
}
return dp[m][n];
}
算法解析
我们来看给定的 solution 函数,它使用了动态规划的方法来解决这个问题。
- 初始化二维数组
首先,创建了一个二维数组dp,其大小为(m + 1) x (n + 1),这里的m和n分别是输入的两条 DNA 序列dna1和dna2的长度。这个二维数组用于保存中间结果。然后,对dp数组的第一行和第一列进行初始化。对于第一行,dp[i][0] = i,这表示将空序列转换为dna1的前i个字符组成的序列所需的操作次数,也就是直接添加i个核苷酸,因为每添加一个核苷酸算一次操作。同理,对于第一列,dp[0][j] = j,表示将空序列转换为dna2的前j个字符组成的序列所需的操作次数,即添加j个核苷酸。 - 动态规划计算过程
通过两层嵌套的for循环来遍历dna1和dna2的每个位置。当dna1[i - 1] == dna2[j - 1]时,说明当前位置的核苷酸相同,此时dp[i][j] = dp[i - 1][j - 1]。这是因为如果当前位置的核苷酸相同,那么从空序列转换到当前位置的dna1和dna2的子序列的最小变异次数,就等于转换到它们上一个位置的子序列的最小变异次数。
然而,当dna1[i - 1]!= dna2[j - 1]时,需要考虑三种操作:替换、删除和添加。这里通过取dp[i - 1][j - 1](替换操作,将dna1的第i个核苷酸替换为dna2的第j个核苷酸)、dp[i - 1][j](删除操作,删除dna1的第i个核苷酸)和dp[i][j - 1](添加操作,在dna1的第i个核苷酸位置添加一个与dna2的第j个核苷酸相同的核苷酸)这三个位置的最小值并加 1,来更新dp[i][j]。这里的加 1 表示进行了一次变异操作。 - 返回结果
最后,函数返回dp[m][n],这个值就是两条完整的 DNA 序列dna1和dna2之间的最小变异次数。
测试案例分析
在 main 函数中给出了几个测试案例。比如对于 dna1 = "AGT", dna2 = "AGCT",通过算法计算得到的最小变异次数为 1。这是因为只需要在 dna1 的末尾添加一个 C 即可,符合我们对这个问题的直观理解。再看 dna1 = "AACCGGTT", dna2 = "AACCTTGG",最小变异次数为 4。这说明这两条序列之间需要进行 4 次变异操作才能相互转换,它们的血缘关系相对较远。
通过对这个算法的学习和分析,我们不仅掌握了一种计算古生物 DNA 序列最小变异次数的方法,更深入理解了动态规划在解决这类问题中的应用。在实际的学习中,我们可以将这种方法应用到其他类似的序列比较和优化问题中,同时也可以思考如何进一步改进算法以提高效率或处理更复杂的情况。而且,在代码实现过程中,要注意对边界条件的处理,就像这里对 dp 数组第一行和第一列的初始化一样,这对于保证算法的正确性至关重要。