问题描述
小R正在研究DNA序列,他需要一个函数来计算将一个受损DNA序列(dna1)转换成一个未受损序列(dna2)所需的最少编辑步骤。编辑步骤包括:增加一个碱基、删除一个碱基或替换一个碱基。
这个问题是经典的 编辑距离问题,要求计算将一个字符串 dna1 转换成另一个字符串 dna2 所需的最少编辑步骤。
每一步操作可以是:
- 插入:在
dna1中插入一个字符,使其与dna2匹配。 - 删除:从
dna1中删除一个字符,使其与dna2匹配。 - 替换:替换
dna1中的一个字符,使其与dna2匹配。
问题分析
这个问题可以通过动态规划来解决。具体方法是构建一个二维的DP表,其中 dp[i][j] 表示将 dna1 的前 i 个字符转换成 dna2 的前 j 个字符所需要的最小编辑距离。
-
定义状态:
dp[i][j]表示将dna1[0..i-1]转换成dna2[0..j-1]的最小编辑距离。
-
状态转移:
如果
dna1[i-1] == dna2[j-1],那么dp[i][j] = dp[i-1][j-1](即不需要操作,因为字符相同)。如果dna1[i-1] != dna2[j-1],则:(1) 插入:将
dna1[0..i-1]转换成dna2[0..j-2],然后插入一个字符,dp[i][j] = dp[i][j-1] + 1。(2) 删除:将
dna1[0..i-2]转换成dna2[0..j-1],然后删除一个字符,dp[i][j] = dp[i-1][j] + 1。(3) 替换:将
dna1[0..i-2]转换成dna2[0..j-2],然后替换dna1[i-1]为dna2[j-1],dp[i][j] = dp[i-1][j-1] + 1。 -
初始化:
dp[0][0] = 0:两个空字符串的编辑距离是0。dp[i][0] = i:将dna1[0..i-1]转换为一个空字符串需要i次删除操作。dp[0][j] = j:将一个空字符串转换为dna2[0..j-1]需要j次插入操作。
-
目标:
- 当达到
dp[len(dna1)][len(dna2)]时完成。
- 当达到
代码解释
-
初始化:
dp[i][0] = i表示将dna1的前i个字符转换为一个空字符串需要i次删除操作。dp[0][j] = j表示将一个空字符串转换为dna2的前j个字符需要j次插入操作。 -
填充 DP 表:
遍历每一个
dp[i][j],对于每对字符dna1[i-1]和dna2[j-1],我们根据它们是否相等来决定是进行替换、插入、还是删除操作。如果字符相等,则不需要编辑操作,继承自dp[i-1][j-1];否则,选择三种操作中的最小值。
完整代码
def solution(dna1, dna2):
# 获取 dna1 和 dna2 的长度
len1 = len(dna1)
len2 = len(dna2)
# 创建 dp 数组,dp[i][j] 表示 dna1[0..i-1] 和 dna2[0..j-1] 的最小编辑距离
dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
# 初始化 dp 数组
for i in range(1, len1 + 1):
dp[i][0] = i # 删除所有字符
for j in range(1, len2 + 1):
dp[0][j] = j # 插入所有字符
# 填充 dp 数组
for i in range(1, len1 + 1):
for j in range(1, len2 + 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, # 删除字符
dp[i][j - 1] + 1, # 插入字符
dp[i - 1][j - 1] + 1 # 替换字符
)
# 返回最终的编辑距离
return dp[len1][len2]
# 测试用例
if __name__ == "__main__":
print(solution("AGT", "AGCT")) # 输出 1
print(solution("AACCGGTT", "AACCTTGG")) # 输出 4
print(solution("ACGT", "TGC")) # 输出 3
print(solution("A", "T")) # 输出 1
print(solution("GGGG", "TTTT")) # 输出 4