青训营刷题-14

213 阅读3分钟

14 DNA序列还原

给定一段受损的 DNA 碱基序列 dna1,在每次只操作一个碱基的情况下,将其以最少的操作步骤将其还原到未受损的 DNA 碱基序列 dna2。

只可以对 DNA 碱基序列中的一个碱基进行三种操作:

  1. 增加一个碱基
  2. 去除一个碱基
  3. 替换一个碱基

输入描述

输入包含两行:

  • 第一行为第一段受损的 DNA 碱基序列 dna1
  • 第二行为第二段未受损的 DNA 碱基序列 dna2

限制条件

  • 0 <= dna1.length, dna2.length <= 500
  • dna1dna2 由大写英文字母 AGCT 组成

输出描述

输出一个整数,表示从 dna1 转换到 dna2 所需的最小操作步骤数。

示例

示例 1

输入

AGCTTAGC
AGCTAGCT

输出

2

说明

操作步骤:

  1. AGCTTAGC -> AGCTAGC(删除 T
  2. AGCTAGC -> AGCTAGCT(增加 T

示例 2

输入

AGCCGAGC
GCTAGCT

输出

4

说明

操作步骤:

  1. AGCCGAGC -> GCCGAGC(删除 A
  2. GCCGAGC -> GCTGAGC(将 C 替换为 T
  3. GCTGAGC -> GCTAGC(删除 G
  4. GCTAGC -> GCTAGCT(增加 T

解题思路

要解决这个问题,我们需要计算将一个字符串转换为另一个字符串所需的最小操作次数,其中每次操作可以是插入、删除或替换一个字符。这实际上是经典的**编辑距离(Levenshtein距离)**问题。

1. 编辑距离概述

编辑距离是衡量两个字符串之间相似度的指标,表示将一个字符串转换成另一个字符串所需的最少编辑操作次数。允许的编辑操作包括:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

2. 动态规划(Dynamic Programming)方法

我们可以使用动态规划来高效地计算编辑距离。具体步骤如下:

2.1. 定义状态

dp[i][j] 表示将 dna1 的前 i 个字符转换为 dna2 的前 j 个字符所需的最小操作次数。

2.2. 状态转移方程
  • 如果当前字符相同(即 dna1[i-1] == dna2[j-1]),则不需要额外的操作:

    dp[i][j] = dp[i-1][j-1]
    
  • 如果当前字符不同,我们考虑三种可能的操作,并选择其中最小的一个加一:

    dp[i][j] = min(
        dp[i-1][j-1] + 1, // 替换
        dp[i-1][j] + 1,   // 删除
        dp[i][j-1] + 1    // 插入
    )
    
2.3. 初始条件
  • 将空字符串转换为长度为 j 的字符串需要 j 次插入操作:

    dp[0][j] = j
    
  • 将长度为 i 的字符串转换为空字符串需要 i 次删除操作:

    dp[i][0] = i
    
2.4. 计算顺序

i = 1mmdna1 的长度),以及从 j = 1nndna2 的长度),依次计算 dp[i][j]

3. 时间与空间复杂度

  • 时间复杂度O(m * n),其中 mn 分别是 dna1dna2 的长度。
  • 空间复杂度O(m * n),用于存储 dp 矩阵。

算法实现

public class Main {
    public static int solution(String dna1, String dna2) {
        int m = dna1.length();
        int n = dna2.length();
        // 创建一个 (m+1) x (n+1) 的DP表
        int[][] dp = new int[m + 1][n + 1];
        
        // 初始化第一列和第一行
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i; // 将dna1的前i个字符转换为空需要i次删除
        }
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j; // 将空转换为dna2的前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); // 输出: true
        System.out.println(solution("AGCCGAGC", "GCTAGCT") == 4);  // 输出: true
        // 其他测试用例
        System.out.println(solution("", "") == 0); // 输出: true
        System.out.println(solution("A", "G") == 1); // 输出: true
        System.out.println(solution("AGT", "AGT") == 0); // 输出: true
        System.out.println(solution("AGT", "AGTC") == 1); // 输出: true
        System.out.println(solution("AGT", "AG") == 1); // 输出: true
        System.out.println(solution("AGT", "CGT") == 1); // 输出: true
    }
}