题目解析:古生物DNA序列血缘分析
小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这个问题本质上是一个 编辑距离(Edit Distance) 或 Levenshtein 距离 问题。编辑距离是用来衡量两条字符串之间的差异的,具体来说,它是将一个字符串转换成另一个字符串所需要的最小操作数,允许的操作包括:插入、删除和替换一个字符。
题目分析
小U需要比较两条DNA序列之间的最小变异次数。对于每个字符,可能有以下几种操作:
- 插入:在一个序列中插入一个字符。
- 删除:从一个序列中删除一个字符。
- 替换:用一个字符替换掉另一个字符。
我们需要求解的是从 dna1 到 dna2 的最小变异次数,或称为编辑距离。
解题思路
编辑距离问题通常使用 动态规划(Dynamic Programming, DP) 来求解。动态规划的思想是通过构造一个二维数组,逐步计算出从 dna1 的前缀到 dna2 的前缀的最小编辑距离。
步骤如下:
-
状态定义:
- 设
dp[i][j]表示将dna1的前i个字符转化为dna2的前j个字符所需的最小变异次数。
- 设
-
初始化:
dp[0][0] = 0,即两个空字符串之间的编辑距离为 0。- 对于任何
i,dp[i][0] = i,表示将dna1的前i个字符转换为空字符串需要i次删除操作。 - 对于任何
j,dp[0][j] = j,表示将空字符串转换为dna2的前j个字符需要j次插入操作。
-
状态转移:
-
如果
dna1[i-1] == dna2[j-1],则dp[i][j] = dp[i-1][j-1],表示当前字符相同,不需要任何操作。 -
如果
dna1[i-1] != dna2[j-1],则我们需要考虑三种操作:- 插入:在
dna1中插入一个字符,dp[i][j] = dp[i][j-1] + 1。 - 删除:从
dna1中删除一个字符,dp[i][j] = dp[i-1][j] + 1。 - 替换:将
dna1[i-1]替换为dna2[j-1],dp[i][j] = dp[i-1][j-1] + 1。
- 插入:在
-
最终,
dp[i][j]的值是以上三种操作中的最小值。
-
-
最终结果:
- 计算完成后,
dp[dna1.length()][dna2.length()]就是从dna1到dna2的最小变异次数。
- 计算完成后,
代码实现
javaCopy Code
public class Main {
public static int solution(String dna1, String dna2) {
int m = dna1.length();
int n = dna2.length();
// 初始化dp数组,dp[i][j]表示将dna1前i个字符转换为dna2前j个字符所需的最小操作数
int[][] dp = new int[m + 1][n + 1];
// 初始化边界条件
for (int i = 0; i <= m; i++) {
dp[i][0] = i; // 删除所有字符
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j; // 插入所有字符
}
// 填充dp表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (dna1.charAt(i - 1) == dna2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // 如果字符相同,不需要操作
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + 1);
// 1. 删除一个字符:dp[i - 1][j] + 1
// 2. 插入一个字符:dp[i][j - 1] + 1
// 3. 替换一个字符:dp[i - 1][j - 1] + 1
}
}
}
// 返回dp[m][n],即从dna1到dna2的最小编辑距离
return dp[m][n];
}
public static void main(String[] args) {
// 测试用例
System.out.println(solution("AGT", "AGCT") == 1); // 应输出 1
System.out.println(solution("", "ACGT") == 4); // 应输出 4
System.out.println(solution("GCTAGCAT", "ACGT") == 5); // 应输出 5
}
}
代码解释
-
初始化
dp数组:dp[i][j]表示将dna1的前i个字符转换为dna2的前j个字符所需的最小操作数。我们需要一个(m+1) x (n+1)的二维数组来保存这些值,其中m是dna1的长度,n是dna2的长度。dp[i][0] = i表示将dna1的前i个字符变为空字符串的操作次数(即删除i次)。dp[0][j] = j表示将空字符串变为dna2的前j个字符的操作次数(即插入j次)。
-
填充
dp数组:- 我们逐步计算
dp[i][j],考虑三种操作:插入、删除和替换,取它们中的最小值。
- 我们逐步计算
-
返回结果:
- 最终
dp[m][n]即为从dna1到dna2的最小编辑距离。
- 最终
时间复杂度
- 时间复杂度:
O(m * n),其中m和n分别是两个字符串的长度。我们需要遍历整个dp数组,填充每个位置。 - 空间复杂度:
O(m * n),需要一个大小为(m+1) x (n+1)的二维数组来存储中间结果。
示例分析
示例 1:
输入:
javaCopy Code
solution("AGT", "AGCT")
dna1 = "AGT",dna2 = "AGCT"。两个字符串的长度分别为 3 和 4。- 删除
'T',插入'C',需要 1 次操作,返回值是1。
示例 2:
输入:
javaCopy Code
solution("", "ACGT")
dna1是空字符串,dna2 = "ACGT"。需要插入 4 个字符'A','C','G','T',返回值是4。
示例 3:
输入:
javaCopy Code
solution("GCTAGCAT", "ACGT")
dna1 = "GCTAGCAT",dna2 = "ACGT",通过替换、删除操作需要 5 次操作,返回值是5。
总结
该问题通过动态规划方法计算编辑距离,通过设计合理的状态转移方程和初始化边界条件,能够高效地解决问题。时间复杂度和空间复杂度都比较符合要求,适用于实际的DNA序列比对问题。
心得:
使用MarsCode AI编写代码让我体验到了编程的便利与高效。AI能够快速生成代码示例,帮助我理解不同编程概念。通过交互式的反馈,我能迅速调整思路,解决问题。同时,MarsCode AI提供的建议让我了解到更多最佳实践,提升了我的编码水平。这种工具不仅节省了时间,还激发了我的创造力,尤其是在处理复杂问题时,AI的支持显得尤为重要。总的来说,MarsCode AI是编程学习和实践中的得力助手。