首先总结一下动态规划问题
动态规划(Dynamic Programming,简称 DP)是一种将复杂问题分解为简单子问题的算法思想,广泛应用于解决最优化问题。通过将每个子问题的结果保存起来,避免了重复计算,从而提高了效率。以下是动态规划的核心思想和常见问题类型的总结:
1、动态规划的核心思想
问题分解:将问题分解为多个相似的子问题,并通过递归求解这些子问题。
重叠子问题:子问题之间有重叠,意味着多个子问题会计算相同的值,利用存储这些子问题的解来避免重复计算。
最优子结构:问题的最优解可以通过子问题的最优解来推导。例如,最短路径问题的最优解可以通过中间点的最短路径来推导。
状态转移方程:通过定义状态和状态之间的转换关系来递推求解。
- 动态规划的解题步骤
定义状态:找出问题的“状态”,即问题在某一阶段的关键变量,通常是数组或矩阵。
确定状态转移方程:找出从一个状态到另一个状态的转移规则。
初始条件:确定最基本的情况,即递归的终止条件。
计算顺序:通常是从小到大(或从大到小),避免重复计算,逐步推导出最终解。
要解决这个问题,我们可以使用动态规划(Dynamic Programming)来计算两条DNA序列之间的最小变异次数。以下是解题思路:
问题理解
我们需要计算两条DNA序列之间的最小变异次数,变异方式包括:
-
插入一个核苷酸
-
删除一个核苷酸
-
替换一个核苷酸
数据结构选择
我们可以使用一个二维数组 dp,其中 dp[i][j] 表示 dna1 的前 i 个字符和 dna2 的前 j 个字符之间的最小变异次数。
算法步骤
- 初始化:
- 如果 dna1 为空,那么变异次数就是 dna2 的长度(全部插入)。
- 如果 dna2 为空,那么变异次数就是 dna1 的长度(全部删除)。
- 状态转移:
- 如果 dna1[i-1] == dna2[j-1],那么 dp[i][j] = dp[i-1][j-1],因为不需要变异。
- 否则,dp[i][j] 可以通过以下三种方式得到:
- 从 dp[i-1][j] 删除一个字符(dna1 的第 i 个字符)。
- 从 dp[i][j-1] 插入一个字符(dna2 的第 j 个字符)。
- 从 dp[i-1][j-1] 替换一个字符(dna1 的第 i 个字符替换为 dna2 的第 j 个字符)。
- 取这三种方式的最小值,并加上1(表示一次变异)。
- 最终结果:
- dp[len(dna1)][len(dna2)] 就是 dna1 和 dna2 之间的最小变异次数。
代码实现如下
#编写函数
def solution(dna1, dna2):
m, n = len(dna1), len(dna2)
# 创建一个 (m+1) x (n+1) 的二维数组 dp,dp[i][j] 表示将 dna1[:i] 转换为 dna2[:j] 的最小变异次数
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化边界情况
for i in range(m + 1):
dp[i][0] = i # 将 dna1[:i] 转换为空,需要 i 次删除
for j in range(n + 1):
dp[0][j] = j # 将空转换为 dna2[:j],需要 j 次插入
# 填充 dp 表
for i in range(1, m + 1):
for j in range(1, n + 1):
if dna1[i - 1] == dna2[j - 1]: # 如果当前字符相等,不需要额外变异
dp[i][j] = dp[i - 1][j - 1]
else:
# 三种操作的最小值:插入、删除、替换
dp[i][j] = min(
dp[i - 1][j] + 1, # 删除 dna1[i-1]
dp[i][j - 1] + 1, # 插入 dna2[j-1]
dp[i - 1][j - 1] + 1 # 替换 dna1[i-1] 为 dna2[j-1]
)
# 返回 dp[m][n],即将 dna1 转换为 dna2 的最小变异次数
return dp[m][n]
if __name__ == "__main__":
# You can add more test cases here
print(solution("AGT", "AGCT") == 1)
print(solution("", "ACGT") == 4)
print(solution("GCTAGCAT", "ACGT") == 5)