DNA编辑距离

120 阅读5分钟

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++) 来遍历 dna1dna2 的各个长度下的子序列情况。

  • 字符相等情况: 当 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] 中(其中 mdna1 的长度,ndna2 的长度),所以直接返回 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);
     }
 }