AI刷题伴学-DNA序列编辑距离 | 豆包MarsCode AI刷题

128 阅读5分钟

问题描述

小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]的最少编辑步骤数,那递归的基本步骤如下:

  1. 基准情况
    • 如果 i==0,返回j,因为需要插入j个字符
    • 如果 j==0,返回i,因为需要删除i个字符
  2. 递归情况
      1. 如果 dna1[i-1] == dna2[j-1]
      • recurEditDis(i,j) = recurEditDis(i-1, j-1)
      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!!! 嗯....

tle.png

唉辛辛苦苦想的Solution居然没过,真恼人呐(╯▔皿▔)╯

不过好在我们有豆包MarsCode智能AI编码助手,不妨让它帮我们找找哪里可以优化的地方吧!

问问它我萌的Solution代码的复杂度和可以优化的地方

Marscode 也是非常迅速的指出了我们Solution的问题所在呀,那就是当子问题重叠时,递归方案的时间复杂度会达到指数级的 O(3n)O(3^n),太可怕了...len过高时会导致大量重复计算,效率非常非常的低

mars_sol.png

好助手Marscode提出了让我们用动态规划(Dynamic Programming)去解决这道题,并给出了用dp的解决的建议^_^

动态规划解决

既然要用dp优化,那就做用dp的基本工作吧:

  1. 初始化一个大小为 (n+1)×(n+1)(n+1) \times (n+1)的二维数组dp
  2. 初始条件如下:
  • dp[0][0] = 0:两个空字符串之间的编辑距离为0。
  • dp[i][0] = i:将dna1的前i个字符转换为空字符串需要i次删除操作。
  • dp[0][j] = j:将空字符串转换为dna2的前j个字符需要j次插入操作
  1. 按照状态转移方程逐步填充dp数组
  2. 返回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]就是我们要找的结果,其中mn分别是dna1dna2的长度。

按照上面 Marscode 小助手提供的建议,修改一下我们原来的代码,时间复杂度也由原来令人发指的O(3n)O(3^n)降到了O(mn)O(m*n)

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解决方案,让我学习到了解决问题时不能一开始就用最简单的解决方案,要考虑尽可能的减少复杂度,一步步从局部最优推敲出全局最优方案。