问题描述
小R正在研究DNA序列,他需要一个函数来计算将一个受损DNA序列(dna1)转换成一个未受损序列(dna2)所需的最少编辑步骤。编辑步骤包括:增加一个碱基、删除一个碱基或替换一个碱基。
测试样例
样例1:
输入:
dna1 = "AGT",dna2 = "AGCT"输出:1
样例2:
输入:
dna1 = "AACCGGTT",dna2 = "AACCTTGG"输出:4
样例3:
输入:
dna1 = "ACGT",dna2 = "TGC"输出:3
样例4:
输入:
dna1 = "A",dna2 = "T"输出:1
样例5:
输入:
dna1 = "GGGG",dna2 = "TTTT"输出:4
思考
题目要求计算一个DNA1 通过增、删、替换得到一个新的DNA2所需要的最小步骤,最直观的方法应该是用递归实现:
逐步比较两个序列的当前字符,计算编辑距离
对于每一对字符,我们可以有以下选择:
- 替换:如果当前字符不同,那么可以选择替换一个字符,然后递归计算剩余部分的编辑距离
- 插入:可以在
dna1当前位置插入dna2的当前字符,然后继续递归计算剩余 - 删除:
删除当前字符,继续递归....
递归函数设计
设我们的递归解决函数是recurEditDis(i,j),表示将dna1[0:i]转换为dna2[0:j]的最少编辑步骤数,那递归的基本步骤如下:
- 基准情况
- 如果
i==0,返回j,因为需要插入j个字符 - 如果
j==0,返回i,因为需要删除i个字符
- 如果
- 递归情况
-
- 如果
dna1[i-1] == dna2[j-1]
recurEditDis(i,j) = recurEditDis(i-1, j-1)
- 如果
-
- 如果
dna[i-1] != dna [j-1],进行增删改操作,花销是:
- 插入:
recurEditDis(i,j-1) + 1 - 删除:
recurEditDis(i-1,j) + 1 - 替换:
recurEditDis(i-1,j-1) + 1
- 如果
-
然后我们只要取各操作花销的最小值min(replaceCnt, insertCnt, delCnt)即是本题所要求的答案喵o(=•ェ•=)m
好嘟,贴上我们滴代码:
def recurEditDis(dna1, dna2, i, j):
# 基本情况
if i == 0:
return j
if j == 0:
return i
if dna1[i-1] == dna2[j-1]:
return recurEditDis(dna1, dna2, i-1, j-1)
# 增删改花销
replaceCnt = recurEditDis(dna1, dna2, i-1, j-1) + 1
insertCnt = recurEditDis(dna1, dna2, i, j-1) + 1
delCnt = recurEditDis(dna1, dna2, i-1, j) + 1
return min(replaceCnt, insertCnt, delCnt)
def solution(dna1, dna2):
return recurEditDis(dna1, dna2, len(dna1), len(dna2))
高高兴兴的提交,结果一看:我滴老天TLE!!! 嗯....
唉辛辛苦苦想的Solution居然没过,真恼人呐(╯▔皿▔)╯
不过好在我们有豆包MarsCode智能AI编码助手,不妨让它帮我们找找哪里可以优化的地方吧!
问问它我萌的Solution代码的复杂度和可以优化的地方
Marscode 也是非常迅速的指出了我们Solution的问题所在呀,那就是当子问题重叠时,递归方案的时间复杂度会达到指数级的 ,太可怕了...len过高时会导致大量重复计算,效率非常非常的低
好助手Marscode提出了让我们用动态规划(Dynamic Programming)去解决这道题,并给出了用dp的解决的建议^_^
动态规划解决
既然要用dp优化,那就做用dp的基本工作吧:
- 初始化一个大小为 的二维数组
dp - 初始条件如下:
dp[0][0] = 0:两个空字符串之间的编辑距离为0。dp[i][0] = i:将dna1的前i个字符转换为空字符串需要i次删除操作。dp[0][j] = j:将空字符串转换为dna2的前j个字符需要j次插入操作
- 按照状态转移方程逐步填充
dp数组 - 返回
dp[m][n]作为最终答案
定义状态
- 定义一个二维数组
dp[i][j]表示dna1的前i各字符转换成dna的前j各字符所需的最少编辑步骤。
状态转移方程
-
如果
dna1[i-1] == dna2[j-1],则dp[i][j] = dp[i-1][j-1]。 -
否则,
dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]) + 1,其中:dp[i-1][j-1]表示替换操作。dp[i][j-1]表示插入操作。dp[i-1][j]表示删除操作
初始化
dp[0][j] = j,表示将空字符串转换成dna2的前j个字符需要j次插入操作。dp[i][0] = i,表示将dna1的前i个字符转换成空字符串需要i次删除操作。
最终结果
dp[m][n]就是我们要找的结果,其中m和n分别是dna1和dna2的长度。
按照上面 Marscode 小助手提供的建议,修改一下我们原来的代码,时间复杂度也由原来令人发指的降到了,
Code:
def solution(dna1, dna2):
m, n = len(dna1), len(dna2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
# 状态转移
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], dp[i][j-1], dp[i-1][j]) + 1
return dp[m][n]
完美通过呀,感谢 Marscode AI助手捏,帮我快速找到问题所在而且提供了非常棒的优化方案~
总结
这道题我一开始去逐步对比字符的思想去解决,但这种方法会造成大量的重复计算,导致超时无法通过题目,使用MarscodeAI智能编码助手帮我分析了我的代码的时间复杂度,以及给出了更好的dp解决方案,让我学习到了解决问题时不能一开始就用最简单的解决方案,要考虑尽可能的减少复杂度,一步步从局部最优推敲出全局最优方案。