题目
给定一段受损的 DNA 碱基序列 dna1,在每次只操作一个碱基的情况下,将其以最少的操作步骤将其还原到未受损的 DNA 碱基序列 dna2。
只可以对 DNA 碱基序列中的一个碱基进行三种操作:
- 增加一个碱基
- 去除一个碱基
- 替换一个碱基
题目解析
如果不懂如何开始,我们可先让ai给我们思路
问题理解
我们需要将一个受损的 DNA 碱基序列 dna1 转换为未受损的 DNA 碱基序列 dna2,并且要找到最少的操作步骤数。允许的操作有三种:
- 增加一个碱基
- 去除一个碱基
- 替换一个碱基
数据结构的选择
我们可以使用动态规划(Dynamic Programming, DP)来解决这个问题。动态规划是一种通过将问题分解为子问题并存储子问题的解来解决复杂问题的方法
动态规划的应用范围
动态规划不仅适用于DNA序列编辑距离问题,它还广泛应用于许多其他问题,如背包问题、最短路径问题、最长公共子序列问题等。这些问题都具有重叠子问题和最优子结构的特性,使得动态规划成为解决这些问题的理想工具。
解题思路
这题是属于动态规划,动态规模本来是比较难的,但是这题会稍微容易一些,
- 定义状态:
dp[i][j]表示将dna1的前i个字符变成dna2的前j个字符所需的最小操作数。 - 初始化:
- 当
i=0时,只能通过插入字符将空字符串变为dna2的前j个字符,因此dp[0][j] = j。 - 当
j=0时,只能通过删除字符将dna1的前i个字符变为空字符串,因此dp[i][0] = i。
- 当
- 状态转移方程:
- 如果
dna1[i-1] == dna2[j-1],即当前字符相同,则不需要操作:dp[i][j] = dp[i-1][j-1]。 - 如果
dna1[i-1] != dna2[j-1],则选择插入、删除或替换操作中的最小值:dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1。
- 如果
时间复杂度:O(m * n),其中 m 和 n 分别是 dna1 和 dna2 的长度。
代码详解
以下是我的代码的逐步解析: 我是用java写的
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;
}
}
}
// 返回最终结果,即 dna1 到 dna2 的最小操作数
return dp[m][n];
}
}
-
定义状态:
- 我们使用一个二维数组
dp,其中dp[i][j]表示将dna1的前i个字符转换为dna2的前j个字符所需的最小操作步骤数。
- 我们使用一个二维数组
int[][] dp = new int[m + 1][n + 1];
-
状态转移方程:
-
如果
dna1[i-1] == dna2[j-1],那么dp[i][j] = dp[i-1][j-1],因为不需要任何操作。 -
否则,我们有三种操作可以选择:
- 插入:
dp[i][j] = dp[i][j-1] + 1 - 删除:
dp[i][j] = dp[i-1][j] + 1 - 替换:
dp[i][j] = dp[i-1][j-1] + 1
- 插入:
-
我们需要取这三种操作中的最小值。
-
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;
}
}
}
-
初始化:
dp[0][j] = j,表示将空字符串转换为dna2的前j个字符需要j次插入操作。dp[i][0] = i,表示将dna1的前i个字符转换为空字符串需要i次删除操作。
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
-
最终结果:
dp[dna1.length()][dna2.length()]就是将dna1转换为dna2所需的最小操作步骤数。
知识总结
- 编辑距离:这个问题是编辑距离问题的变种,通过动态规划可以高效求解。
- 动态规划应用:在这里我们定义了一个二维数组来存储每个子问题的最小操作数。通过构建二维状态表,逐步填充并获得最终答案。
- 状态转移方程设计:插入、删除、替换的最小值的计算是编辑距离类问题的核心。
学习计划与心得
- 学习计划:动态规划一般是建议先学到其他的算法,再学习动态规划。学习动态规划时,建议先理解动态规划的基本思想,再逐步解决类似的字符串问题,如最长公共子序列、字符串相似度等。可以先从简单的二维动态规划开始理解状态转移方程的构建。
- 学习建议:刚开始可以手动填充几行
dp表格,加深对状态转移方程的理解。学习动态规划的核心在于理解“重叠子问题”和“最优子结构”。如果不懂算法原理,可以问一下ai。
个人思考与分析
在解决DNA序列编辑距离问题时,我深刻体会到了动态规划的强大之处。通过将问题分解为更小的子问题,并利用已有的子问题解来构建原问题的解,可以高效地解决问题。这种方法不仅减少了计算量,还提高了算法的效率。
此外,我也意识到了在设计状态转移方程时的挑战。正确的状态转移方程是解决问题的关键,它需要对问题有深入的理解。在实际应用中,我们可能需要尝试不同的状态定义和转移方式,直到找到最合适的解决方案。
最后,我认为动态规划是一种强大的算法策略,但它也需要我们投入时间去理解和掌握。通过不断练习和思考,我们可以更好地应用动态规划来解决复杂问题。