青训营-DNA序列编辑距离

138 阅读4分钟

问题描述

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

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

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

输入描述:

输入两段 DNA 碱基序列,每段分一行输入

第一行为第一段受损的 DNA 碱基序列 dna1

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

输出描述:

最小操作步骤数

备注:

0 <= dna1.length, dna2.length <= 500

dna1 和 dna2 由大写英文字母 A、G、C、T 组成

示例 1

输入

AGCTTAGC

AGCTAGCT

输出

2

说明

AGCTTAGC -> AGCTAGC(删除 T)

AGCTAGC -> AGCTAGCT(增加 T)

解题思路(非原创!!!)

为了解决这个问题,我们需要计算一个字符串转换为另一个字符串所需要的最小编辑次数,且每次只允许插入、删除、替换一个字符。即“最小编辑距离问题”。

1.编辑距离

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

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

2.动态规划

简述:动态规划实际上就是将原问题拆解为子问题,知道子问题可以被解决,然后保存子问题答案以便减少重复运算,再根据子问题进行反推,得到原问题解法。

核心思想

动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算

什么样的问题可以考虑使用动态规划解决呢?

如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。

动态规划的解题思路

动态规划一般都是自底向上的

  • 穷举分析
  • 确定边界
  • 找出规律,确定最优子结构
  • 写出状态转移方程

1.穷举分析

2.确定边界

3.找出规律,确定最优子结构

4.写出状态转移方程 设 dp[i][j] 表示将 dna1 的前 i 个字符转换为 dna2 的前 j 个字符所需的最小操作次数。

填充二维数组 dp 是通过两层循环来实现的。

外层的两个循环分别遍历两个字符串的长度。对于每个位置 (i, j) ,如果 dna1 的第 i - 1 个字符和 dna2 的第 j - 1 个字符相同,那么 dp[i][j] 就等于 dp[i - 1][j - 1] 。

如果这两个字符不同,那么 dp[i][j] 就等于以下三个值中的最小值:

  1. dp[i - 1][j] + 1 ,表示删除 dna1 的当前字符。
  2. dp[i][j - 1] + 1 ,表示在 dna1 中增加一个字符使其与 dna2 的当前字符匹配。
  3. dp[i - 1][j - 1] + 1 ,表示将 dna1 的当前字符替换为与 dna2 的当前字符相同。

这样通过逐步比较和计算,最终就能填满整个 dp 数组,得到将 dna1 转换为 dna2 的最少操作步骤数。

5.初始条件

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

算法实现:

#include <iostream>
#include <string>

int solution(std::string dna1, std::string dna2) {
    int m = dna1.size();
    int n = dna2.size();
    // 创建二维数组 dp 来存储中间结果
    int dp[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[i - 1] == dna2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = std::min(dp[i - 1][j] + 1, // 删除操作
                                    std::min(dp[i][j - 1] + 1, // 增加操作
                                             dp[i - 1][j - 1] + 1)); // 替换操作
            }
        }
    }
    return dp[m][n];
}

int main() {
    //  You can add more test cases here
    std::cout << (solution("AGCTTAGC", "AGCTAGCT") == 2) << std::endl;
    std::cout << (solution("AGCCGAGC", "GCTAGCT") == 4) << std::endl;
    return 0;
}
  1. AGCCGAGC -> GCCGAGC(删除 A
  2. GCCGAGC -> GCTGAGC(将 C 替换为 T
  3. GCTGAGC -> GCTAGC(删除 G
  4. GCTAGC -> GCTAGCT(增加 T

作者:热的棒打鲜橙
链接:juejin.cn/post/742678…