DNA序列编辑问题解析 | 豆包MarsCode AI刷题

57 阅读4分钟

题目解析:DNA序列编辑问题

问题理解

给定两段DNA碱基序列 dna1dna2,我们需要通过最少的操作步骤将 dna1 转换为 dna2。允许的操作有三种:增加一个碱基、去除一个碱基、替换一个碱基。这个问题本质上是一个经典的编辑距离问题(Edit Distance),也称为莱文斯坦距离(Levenshtein Distance)。编辑距离是指将一个字符串转换为另一个字符串所需的最少操作次数。

数据结构选择

为了解决这个问题,我们选择使用动态规划(Dynamic Programming, DP)。动态规划的核心思想是将复杂问题分解为一系列子问题,并通过存储子问题的解来避免重复计算。

我们使用一个二维数组 dp,其中 dp[i][j] 表示将 dna1 的前 i 个字符转换为 dna2 的前 j 个字符所需的最小操作数。数组的大小为 (m+1) x (n+1),其中 mn 分别是 dna1dna2 的长度。

算法步骤

  1. 初始化

    • dp[0][j] 表示将空字符串转换为 dna2 的前 j 个字符,显然需要 j 次插入操作。
    • dp[i][0] 表示将 dna1 的前 i 个字符转换为空字符串,显然需要 i 次删除操作。
  2. 状态转移

    • 如果 dna1[i-1] == dna2[j-1],则 dp[i][j] = dp[i-1][j-1],因为不需要任何操作。
    • 否则,dp[i][j] 可以通过以下三种操作之一得到:
      • 插入:dp[i][j-1] + 1
      • 删除:dp[i-1][j] + 1
      • 替换:dp[i-1][j-1] + 1
    • 取这三种操作的最小值作为 dp[i][j]
  3. 最终结果dp[m][n] 即为所求的最小操作步骤数。

代码详解

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) {
        // 你可以添加更多测试用例
        System.out.println(solution("AGCTTAGC", "AGCTAGCT") == 2);
        System.out.println(solution("AGCCGAGC", "GCTAGCT") == 4);
    }
}

代码解析

  1. 初始化

    • dp[i][0] = i:表示将 dna1 的前 i 个字符转换为空字符串,需要 i 次删除操作。
    • dp[0][j] = j:表示将空字符串转换为 dna2 的前 j 个字符,需要 j 次插入操作。
  2. 状态转移

    • if (dna1.charAt(i - 1) == dna2.charAt(j - 1)):如果当前字符相同,则不需要任何操作,直接继承前一个状态的值,即 dp[i][j] = dp[i-1][j-1]
    • else:如果当前字符不同,则需要进行插入、删除或替换操作,取这三种操作的最小值,并加1,即 dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1])) + 1
  3. 最终结果dp[m][n] 即为将 dna1 转换为 dna2 的最小操作步骤数。

测试样例解析

  1. 示例 1

    • 输入:dna1 = "AGCTTAGC"dna2 = "AGCTAGCT"
    • 输出:2
    • 解释:
      • AGCTTAGC -> AGCTAGC(删除 T)
      • AGCTAGC -> AGCTAGCT(增加 T)
  2. 示例 2

    • 输入:dna1 = "AGCCGAGC"dna2 = "GCTAGCT"
    • 输出:4
    • 解释:
      • AGCCGAGC -> GCCGAGC(删除 A)
      • GCCGAGC -> GCTGAGC(将 C 替换为 T)
      • GCTGAGC -> GCTAGC(删除 G)
      • GCTAGC -> GCTAGCT(增加 T)

总结

通过动态规划,我们可以高效地计算出将一个DNA序列转换为另一个DNA序列所需的最小操作步骤数。其时间复杂度为O(m*n)。其中,m 是 dna1 的长度,n 是 dna2 的长度。我们使用了一个二维数组dp,其空间复杂度为O((m+1)*(n-1))。动态规划的核心在于将问题分解为子问题,并通过存储子问题的解来避免重复计算。在本题中,我们使用了一个二维数组 dp 来存储中间结果,并通过状态转移方程来逐步填充数组,最终得到所需的最小操作步骤数。