问题描述
小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
def solution(dna1, dna2):
len1, len2 = len(dna1), len(dna2)
dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
for i in range(len1 + 1):
for j in range(len2 + 1):
if i == 0:
dp[i][j] = j
elif j == 0:
dp[i][j] = i
else:
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] + 1, # 替换
dp[i - 1][j] + 1, # 删除
dp[i][j - 1] + 1) # 添加
def solution(dna1, dna2):
len1, len2 = len(dna1), len(dna2)
dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
for i in range(len1 + 1):
for j in range(len2 + 1):
if i == 0:
dp[i][j] = j
elif j == 0:
dp[i][j] = i
else:
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] + 1, # 替换
dp[i - 1][j] + 1, # 删除
dp[i][j - 1] + 1) # 添加
return dp[len1][len2]
代码实现原理
这段代码的核心是通过动态规划解决两个 DNA 序列之间的编辑距离问题。以下是代码实现的详细原理解析:
1. 动态规划概述
动态规划是一种通过分解问题为子问题并逐步求解来解决复杂问题的方法。在这段代码中,定义了一个二维数组 dp,用于存储子问题的最优解。每个 dp[i][j] 表示:
将
dna1的前i个字符转换为dna2的前j个字符所需的最小编辑操作数。
允许的操作
- 插入操作:在
dna1中插入一个字符,使其更接近dna2。 - 删除操作:从
dna1删除一个字符,使其更接近dna2。 - 替换操作:将
dna1中的一个字符替换为另一个字符,使其与dna2的对应字符匹配。
2. 状态转移方程
状态转移方程描述如何通过已知的子问题解推导更大的子问题解。对于每个位置 (i, j):
-
如果
dna1[i - 1] == dna2[j - 1],则两个字符相等,不需要额外操作: dp[i][j]=dp[i−1][j−1]dp[i][j] = dp[i - 1][j - 1]dp[i][j]=dp[i−1][j−1] -
如果
dna1[i - 1] \neq dna2[j - 1],则需要从以下三种操作中选择最小的:- 替换字符:
使
dna1[i - 1]与dna2[j - 1]匹配, dp[i][j]=dp[i−1][j−1]+1dp[i][j] = dp[i - 1][j - 1] + 1dp[i][j]=dp[i−1][j−1]+1 - 删除字符:删除
dna1[i - 1], dp[i][j]=dp[i−1][j]+1dp[i][j] = dp[i - 1][j] + 1dp[i][j]=dp[i−1][j]+1 - 插入字符:在
dna1末尾插入一个字符,使其与dna2[j - 1]匹配, dp[i][j]=dp[i][j−1]+1dp[i][j] = dp[i][j - 1] + 1dp[i][j]=dp[i][j−1]+1
- 替换字符:
使
3. 初始化
为了使动态规划的计算过程有基准点,需要对二维数组 dp 进行初始化:
dp[i][0]:表示将dna1的前i个字符转换为空字符串所需的操作数,显然需要 删除i次字符。 dp[i][0]=idp[i][0] = idp[i][0]=idp[0][j]:表示将空字符串转换为dna2的前j个字符所需的操作数,显然需要 插入j次字符。 dp[0][j]=jdp[0][j] = jdp[0][j]=j
4. 填充 DP 表
通过双重循环遍历 dp 表的每个位置 (i, j),依次计算最优解:
- 第一层循环遍历
dna1的字符索引i。 - 第二层循环遍历
dna2的字符索引j。 - 每次根据状态转移方程更新
dp[i][j]的值。
5. 返回最终结果
最终的答案是 dp[len1][len2],即将整个 dna1 转换为 dna2 所需的最小编辑操作数。
算法设计分析
代码功能
这段代码实现的是一个计算两个 DNA 序列之间编辑距离的算法,使用了动态规划的思想。编辑距离指的是将一个字符串变成另一个字符串所需的最小操作次数,操作包括插入、删除和替换。
时间复杂度分析
-
初始化 dp 数组
dp是一个二维数组,其大小为(len1 + 1) x (len2 + 1),其中len1和len2分别是dna1和dna2的长度。- 初始化时,使用了双重列表推导,时间复杂度为 O(len1×len2)O(len1 \times len2)O(len1×len2)。
-
填充 dp 数组
- 遍历
i和j,分别从0到len1和len2,总共有 O(len1×len2)O(len1 \times len2)O(len1×len2) 次迭代。 - 每次迭代中,根据当前字符是否相等,进行插入、删除和替换的操作。
- 每次操作是 O(1)O(1)O(1) 的常数时间。
- 整体填充 dp 数组的时间复杂度为 O(len1×len2)O(len1 \times len2)O(len1×len2)。
- 遍历
-
结果返回
- 返回 dp 数组的最后一个元素
dp[len1][len2],时间复杂度为 O(1)O(1)O(1)。
- 返回 dp 数组的最后一个元素
总时间复杂度
初始化和填充 dp 数组是主要耗时步骤,因此总时间复杂度为:
O(len1×len2)O(len1 \times len2)O(len1×len2)
空间复杂度分析
-
dp 数组
- 需要一个大小为
(len1 + 1) x (len2 + 1)的二维数组。 - 空间复杂度为 O(len1×len2)O(len1 \times len2)O(len1×len2)。
- 需要一个大小为
-
其他变量
- 使用了
len1、len2、i和j作为辅助变量。 - 这些变量所占空间为 O(1)O(1)O(1)。
- 使用了
总空间复杂度
由于 dp 数组占用主要空间,因此总体空间复杂度为:
O(len1×len2)O(len1 \times len2)O(len1×len2)
算法设计总结
-
核心思想
- 利用动态规划,逐步构造两个字符串之间的最小编辑操作数。
- 每个 dp 元素的值依赖于前一步的状态,因此通过表格法逐层递推,最终得到结果。
-
步骤
- 初始化 dp 数组,用于存储中间结果。
- 双重循环填充 dp 数组,计算当前的编辑距离。
- 根据三个操作(插入、删除、替换)的最小值更新当前状态。
-
时间与空间优化
- 时间复杂度为 O(len1×len2)O(len1 \times len2)O(len1×len2),较优。
- 空间复杂度为 O(len1×len2)O(len1 \times len2)O(len1×len2)。可以通过优化为一维数组将空间复杂度降到 O(min(len1,len2))O(\min(len1, len2))O(min(len1,len2))。
适用场景与扩展
-
场景
- 文本相似性比较。
- DNA 序列比对与基因分析。
- 拼写纠正和自然语言处理中的字符串距离计算。
-
扩展
- 使用递归加记忆化的方式实现。
- 通过滚动数组优化空间复杂度到 O(min(len1,len2))O(\min(len1, len2))O(min(len1,len2))。