题目背景:
小U是一位古生物学家,正在研究不同物种之间的血缘关系。为了分析两种古生物的血缘远近,她需要比较它们的DNA序列。DNA由四种核苷酸A、C、G、T组成,并且可能通过三种方式发生变异:添加一个核苷酸、删除一个核苷酸或替换一个核苷酸。小U认为两条DNA序列之间的最小变异次数可以反映它们之间的血缘关系:变异次数越少,血缘关系越近。
任务: 编写一个算法,帮助小U计算两条DNA序列之间所需的最小变异次数。
思路分析
问题定义:
输入:两条DNA序列 dna1 和 dna2。
输出:两条DNA序列之间的最小变异次数。
变异类型:
- 添加一个核苷酸。
- 删除一个核苷酸。
- 替换一个核苷酸。
算法选择:
- 使用动态规划(Dynamic Programming, DP)来解决这个问题。动态规划是一种通过将问题分解为子问题,并存储子问题的解来避免重复计算的方法。
状态定义:
dp[i][j] 表示dna1的前i个字符和dna2的前j个字符之间的最小变异次数。
状态转移方程:
- 如果
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。
边界条件:
dp[i][0]表示dna1的前i个字符和空字符串之间的最小变异次数,即i。dp[0][j]表示空字符串和dna2的前j个字符之间的最小变异次数,即j。
代码详解
def solution(dna1, dna2):
m, n = len(dna1), len(dna2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化边界条件
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
# 动态规划填充表格
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]
代码解析
初始化:
dp是一个二维数组,用于存储子问题的解。dp[i][0]和dp[0][j]分别初始化为i和j,表示添加或删除字符的次数。
动态规划填充:
- 遍历
dna1和dna2的每个字符。 - 如果字符相同,则
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。
返回结果:
dp[m][n] 即为dna1和dna2之间的最小变异次数。
测试用例
solution("AGT", "AGCT")应返回1。solution("", "ACGT")应返回4。solution("GCTAGCAT", "ACGT")应返回5。
总结
通过动态规划,我们可以高效地计算两条DNA序列之间的最小变异次数,从而帮助小U分析不同物种之间的血缘关系。动态规划不仅解决了这个问题,还为我们提供了一种通用的解决问题的方法,适用于许多其他类似的问题。