DNA编辑距离
题目描述
小R正在研究DNA序列,他需要一个函数来计算将一个受损DNA序列(dna1)转换成一个未受损序列(dna2)所需的最少编辑步骤。编辑步骤包括:增加一个碱基、删除一个碱基或替换一个碱基。
思路
本题旨在计算将一个受损的 DNA 序列(dna1)转换为未受损序列(dna2)所需的最少编辑步骤,编辑操作限定为增加一个碱基、删除一个碱基或替换一个碱基,解决这个问题我们采用的是动态规划的思路,下面详细阐述具体的思考过程:
1. 确定状态表示
我们创建了一个二维数组 dp[i][j],其中 i 表示 dna1 序列的长度范围(从 0 到 dna1 的实际长度 m),j 表示 dna2 序列的长度范围(从 0 到 dna2 的实际长度 n)。dp[i][j] 的含义是:将 dna1 的前 i 个字符组成的子序列转换为 dna2 的前 j 个字符组成的子序列所需要的最少编辑步骤数。这样通过这个二维数组,我们就能记录下在不同子序列长度下的编辑距离情况,为后续逐步推导出最终完整序列的编辑距离做准备。
2. 初始化边界情况
在动态规划中,边界情况的初始化很关键,它为后续的递推提供了基础值。
- 当
dna2的长度为 0 时(也就是j = 0),意味着要把dna1的前i个字符都删除才能转换为空序列,所以编辑距离就是i,因此我们通过循环for (int i = 0; i <= m; i++),将dp[i][0]都初始化为i。 - 同理,当
dna1的长度为 0 时(即i = 0),要把空序列通过添加操作变为dna2的前j个字符,编辑距离就是j,所以利用循环for (int j = 0; j <= n; j++),把dp[0][j]都初始化为j。
3. 填充 dp 数组(核心递推过程)
接下来就是根据动态规划的状态转移方程去填充整个 dp 数组,通过两层嵌套的循环 for (int i = 1; i <= m; i++) 和 for (int j = 1; j <= n; j++) 来遍历 dna1 和 dna2 的各个长度下的子序列情况。
-
字符相等情况: 当
dna1的第i个字符(注意数组下标是i - 1,因为下标从 0 开始计数)和dna2的第j个字符相等时,这意味着当前这两个字符不需要进行编辑操作,那么将dna1的前i个字符组成的子序列转换为dna2的前j个字符组成的子序列的最少编辑步骤数,就等于把dna1的前i - 1个字符转换为dna2的前j - 1个字符的编辑步骤数,即dp[i][j] = dp[i - 1][j - 1]。 -
字符不相等情况: 当
dna1的第i个字符和dna2的第j个字符不相等时,此时我们有三种编辑操作可供选择(增加、删除、替换),我们需要选择其中能让编辑距离最小的操作来执行。- 替换操作:可以把
dna1的第i个字符替换成dna2的第j个字符,那么编辑距离就是把dna1的前i - 1个字符转换为dna2的前j - 1个字符的编辑距离再加 1(代表这次替换操作),也就是dp[i - 1][j - 1] + 1。 - 删除操作:选择删除
dna1的第i个字符,那么编辑距离就是把dna1的前i - 1个字符转换为dna2的前j个字符的编辑距离再加 1(代表这次删除操作),即dp[i - 1][j] + 1。 - 增加操作:可以在
dna1的第i个字符位置插入dna2的第j个字符,编辑距离就是把dna1的前i个字符转换为dna2的前j - 1个字符的编辑距离再加 1(代表这次插入操作),也就是dp[i][j - 1] + 1。
- 替换操作:可以把
综合这三种情况,我们要取这三种操作所对应的编辑距离的最小值作为当前 dp[i][j] 的值,即 dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1。
4. 获取最终结果
经过上述步骤,我们完整地填充了 dp 数组,而最终我们要求的是将整个 dna1 序列转换为整个 dna2 序列的最少编辑步骤数,这个结果就存储在 dp[m][n] 中(其中 m 是 dna1 的长度,n 是 dna2 的长度),所以直接返回 dp[m][n] 即可得到答案。
通过这样一套基于动态规划的完整思路和实现流程,我们就能准确计算出两个 DNA 序列之间的编辑距离了。
实现
public class Main {
public static int solution(String dna1, String dna2) {
int m = dna1.length();
int n = dna2.length();
// 创建一个二维数组 dp
int[][] dp = new int[m + 1][n + 1];
// 初始化 dp 数组
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(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
}
}
}
// 返回最终结果
return dp[m][n];
}
public static void main(String[] args) {
// You can add more test cases here
System.out.println(solution("AGCTTAGC", "AGCTAGCT") == 2);
System.out.println(solution("AGCCGAGC", "GCTAGCT") == 4);
}
}