[python]环状DNA序列的最小表示法(Booth's算法) | MarsCode AI刷题

144 阅读4分钟

一、问题描述

小C正在研究一种环状 DNA 结构,它由四种碱基 (A)、(C)、(G)、(T) 构成。这种环状结构的特点是可以从任何位置开始读取序列,因此一个长度为 (n) 的碱基序列可以有 (n) 种不同的表示方式。小C的任务是从这些表示中找到字典序最小的序列,即该序列的“最小表示”。

示例

  • 输入:dna_sequence = "ATCA"

    • 环状旋转可能的表示有:ATCATCAACAATAATC
    • 字典序最小的是 AATC
    • 输出:"AATC"
  • 输入:dna_sequence = "CGAGTC"

    • 环状旋转可能的表示有:CGAGTCGAGTCCAGTCCGGTCCGATCCGAGCCGAGT
    • 字典序最小的是 AGTCCG
    • 输出:"AGTCCG"

二、解决思路

由于这个序列是环状的,所以可以从任意位置开始读取,产生不同的旋转序列。我们需要从这些旋转中找到字典序最小的表示。

假设输入序列长度为 (n),我们可以通过以下方法来寻找其最小字典序的表示。

三、解题方法

1. 暴力法(不推荐)

我们可以遍历每一个可能的旋转,将其字典序与当前最小的旋转进行比较。尽管这种方法能够找到最小表示,但复杂度为 (O(n^2)),对长序列效率较低。

2. 双倍字符串法

将原始字符串与自己拼接起来形成一个双倍字符串。双倍字符串的特点是我们可以方便地在其中查找所有旋转情况。

对于 DNA 序列 (S) 的长度为 (n),将 (S) 与自身拼接得到 (S+S),形成长度为 (2n) 的新字符串。在这个新字符串中,从 (0) 到 (n-1) 的每一个位置作为起始位置的 (n) 长度子串,都是原字符串的一种旋转。通过遍历这些旋转,寻找字典序最小的旋转位置,就可以得到答案。

双倍字符串的复杂度是 (O(n^2)),通过直接比较不同子串效率不够高,尤其在面对长序列时。因此,我们引入更高效的算法——Booth’s 算法

3. Booth's 算法(推荐)

Booth's 算法是一种能够在线性时间 (O(n)) 内找到字符串最小表示的方法。其核心思想是通过维护一个失败数组,快速跳过不符合的前缀,从而确定最小字典序的起始位置。

Booth’s 算法最常用在字符串旋转相关的问题中,比如找到旋转字符串中的最小表示。

四、Booth's 算法实现步骤

  1. 构造双倍字符串:将原 DNA 序列 (S) 拼接为 (S+S),并定义长度 (2n) 的新字符串 s
  2. 初始化变量
    • f:失败数组,初始化为 ([-1] * 2n)
    • k:当前找到的最小表示的起始位置,初始为 0
  3. 遍历字符串
    • 从位置 (j = 1) 开始,逐个字符与最小表示位置进行比较。
    • 使用失败数组来记录匹配失败的位置,跳过重复的比较。
  4. 返回结果
    • 从最小表示的起始位置 (k) 提取长度为 (n) 的子串,即为所求字典序最小表示。

五、代码实现

以下是使用 Booth’s 算法求解最小表示的 Python 实现。

def solution(dna_sequence):
    # 使用 Booth's 算法来找到字典序最小的旋转起始位置
    s = dna_sequence * 2  # 双倍字符串
    n = len(dna_sequence)
    f = [-1] * (2 * n)  # 失败数组
    k = 0  # 最小表示的起始位置
    
    for j in range(1, 2 * n):
        sj = s[j]
        i = f[j - k - 1]
        
        while i != -1 and sj != s[k + i + 1]:
            if sj < s[k + i + 1]:
                k = j - i - 1
            i = f[i]
        
        if sj != s[k + i + 1]:  # 更新 k
            if sj < s[k]:
                k = j
            f[j - k] = -1
        else:
            f[j - k] = i + 1
    
    # 提取从最小起始位置开始的 n 长度的子串,即最小字典序表示
    return s[k:k + n]

if __name__ == "__main__":
    #  You can add more test cases here
    print(solution("ATCA") == "AATC")
    print(solution("CGAGTC") == "AGTCCG")
    print(solution("TCATGGAGTGCTCCTGGAGGCTGAGTCCATCTCCAGTAG") == "AGGCTGAGTCCATCTCCAGTAGTCATGGAGTGCTCCTGG")

六、复杂度分析

  • 时间复杂度:(O(n)),Booth's 算法的效率来源于线性遍历字符串,并通过失败数组跳过多余的比较。
  • 空间复杂度:(O(n)),只需额外存储双倍字符串和失败数组。

七、总结

Booth's 算法为我们提供了一种高效且简单的方法来解决最小表示问题。它通过对字符串的双倍扩展和失败数组的维护,能够在 (O(n)) 时间内找到字符串的字典序最小表示。