古生物DNA序列血缘分析 最长公共子序列 | 豆包MarsCode AI 刷题

100 阅读4分钟

问题描述

小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

问题分析

在算法中,我们经常遇到处理子数组/字串,或子序列的问题,它们的区别在于前二者可以是不连续的,而子序列是连续的。对于这个问题表面上看是处理字串,但在解决时其实是对于求最大公共子序列的一个变形,为什么这么处理我后面会提到。

我们先处理它们的最大子序列,我们设dna1的序列长度为n,dna2的序列长度为m。和背包问题一样,子序列本质上也是考虑每个字母选或者不选。假设最后一对字母为x和y,那么就会出现4种情况,即选x和y,选x不选y,选y不选x,不选x不选y。假设我们都不选,那么问题就变成前n-1和前m-1个字母的子问题了。再一般化,考虑dna1[i],dna2[j]选或者不选,就确定了递归参数中的i和j表示的子问题就是dna1的前i个字母和dna2的前j个字母的最长公共子序列的长度。

现在就出现了两个情况,即dna1[i]和dna2[j]是否相同,如果它们相同,那么我们可以直接进入dfs(i-1,j-1),如果不同,那我们就需要递归计算dfs(i-1,j),dfs(i,j-1),dfs(i-1,j-1)的最小值并加上1作为返回值,即进行了一次增删改的操作。

回到刚才的问题,为什么可以近似看作求最大子序列长度呢?删除一个字母,相当于是去掉dna1[i],插入一个字母,相当于是去掉dna2[j],如果它们相同,就可以都去掉,并且如果他们两个不相同,还可以进行替换操作,等价于把他们两个都去掉。

需要注意的是,如果其中一个dna链已经到了尽头,如i<0,那么相当于dna2链中剩下的字母都需要去掉,即需要j+1步操作完成。

代码实现

-   时间复杂度O(nm)
-   空间复杂度O(nm)

本题总结

  1. 递归与记忆化

    • 使用了递归函数 f(i, j) 来计算 dna1 和 dna2 之间的最小变异次数。
    • 通过 @lru_cache 装饰器实现了记忆化,避免了重复计算,提高了效率。
  2. 边界条件

    • 当 i < 0 时,表示 dna1 已经处理完毕,剩下的 dna2 的长度即为需要插入的次数。
    • 当 j < 0 时,表示 dna2 已经处理完毕,剩下的 dna1 的长度即为需要删除的次数。
  3. 状态转移

    • 如果 dna1[i] == dna2[j],则不需要任何操作,直接返回 f(i-1, j-1)
    • 否则,返回三种操作(插入、删除、替换)的最小值加一。
  4. 最终结果

    • 通过 f(n-1, m-1) 计算出 dna1 和 dna2 之间的最小变异次数,其中 n 和 m 分别是 dna1 和 dna2 的长度。