青训营AI刷题 DP动态规划 DNA血缘分析 | 豆包MarsCode AI刷题

80 阅读5分钟

题目重述

小U是一位古生物学家,她需要通过比较两种古生物的DNA序列来分析它们的血缘关系。DNA序列由四种核苷酸A、C、G、T组成,可能通过添加、删除或替换核苷酸发生变异。小U认为两条DNA序列之间的最小变异次数可以反映它们之间的血缘关系:即变异次数越少,血缘关系越近。

我们的任务是编写一个算法,计算两条DNA序列之间所需的最小变异次数。

测试样例

样例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

题目分析

这道题目要求我们帮助小U计算两条DNA序列之间的最小变异次数。DNA序列由四种核苷酸A、C、G、T组成,可能通过添加、删除或替换核苷酸发生变异。可以这样认为,每一次遍历都是对下一个字符进行处理,处理的可选操作有三种,即添加、删除、或者替换(实际上有四种,即下一个字符相同,无需操作)。在这里我们重新理清一下三个操作的过程

我用DP并不熟练,以下分析只是我个人的理解方法,可能有误,请大家交流指正。

我的理解是,三个操作并不是其简单的本意,最终的目的是一定要令当前操作的字符及之前的所有字符“完成匹配”。

  • 添加:实际上并非对“下一个字符”本身进行操作,而是在“下一个字符”之前插入一个字符
  • 删除:是把当前字符回删,然后“补字符”,实现与目标字符的匹配
  • 替换:假定当前字符以前的所有字符都已经匹配,因此只把当前的字符换成任意一个

以上三个操作对应三条DP状态转移方程。理解操作的本意对理解动态转移方程至关重要。

我们在这里使用一个二维DP数组来标记最小修改次数,使用 样例1:

输入:dna1 = "AGT", dna2 = "AGCT"
输出:1

先构造一个初始的二维DP,在这里dna2用列表示对应下标j,dna1用行表示,对应下标i

图片.png

这个DP表怎么看呢?很简单,哪一行和哪一列相交的单元格,就表示从“那一行的字符串”到“那一列的字符串”对应的最小距离。注意:这里说的是字符串,例如j=2时,指的是""+"A"+"G"即AG这个字符串。

因此首行和首列就很轻易的填写出来了,从空字符串到 AGCT这几个子串的距离分别就是1 2 3 4,同理AGT就是 1 2 3,OK 咱们这就可以用状态转移方差推DP了

dp[i - 1][j] + 1  # 删除
dp[i][j - 1] + 1  # 插入
dp[i - 1][j - 1] + 1  # 替换

估计看到这里,很多小伙伴一时也不明白为什么这个i j非得是这么操作,为啥删除就是i - 1,插入就是j - 1。咱们一条一条看一下

首先最好理解的替换,无论咱们对前i或前j个字符做了什么操作(反正操作了几次,都已经记录在DP数组里了),经过了这么多次操作之后,dna2的前j个字符和 dna1的前i个字符已经相等了,只差我把当前字符换了这一步,那么就是dp[i-1,j-1]了,替换的这一步就是+1。

然后删除为什么是i - 1呢,咱们以i=1 j=2的格子为例,这个格子表示从A这个字符出发,到AG这个字符串的距离,如果我要删掉当前字符A,是不是相当于得把dna1从A这个先变成""(空字符串),即以空字符串为起点,这个距离是不是就是从i到了i-1?OK 记住: 最终的目的是一定要令当前操作的字符及之前的所有字符“完成匹配”,在这个DP数组中删除,是不能完成匹配的,还得找一下从""到AG的距离,这个距离就是从j=0到j=2的距离,因此删除是dp[i - 1][j] + 1,不要忘了从A到""的这一步,在DP数组里面是没有的,得加上,这才有了+1

既然理解了删除,插入也就不难理解了,删除的起点是当前i的上一行,插入就是不需要回溯到上一行,直接是当前行,j-1表示当前字符到目标字符串的第j-1位时的距离,再+1就好了

那么,我们要的是最短的操作,类似贪心的思路,直接对这三个编辑操作取最短距离就好(只要每一步都是最优,那就是总体最优)

参考代码

def solution(dna1, dna2):
    # 获取两条DNA序列的长度
    len1 = len(dna1)
    len2 = len(dna2)
    
    # 创建一个二维数组dp,大小为(len1+1) x (len2+1)
    dp = [[0] * (len2 + 1) for _ in range(len1 + 1)]
    
    # 初始化dp数组的第一行和第一列
    for i in range(len1 + 1):
        dp[i][0] = i
    for j in range(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("", "ACGT") == 4)
    print(solution("GCTAGCAT", "ACGT") == 5)