问题描述
小C正在研究一种环状的 DNA 结构,它由四种碱基A、C、G、T构成。这种环状结构的特点是可以从任何位置开始读取序列,因此一个长度为 n 的碱基序列可以有 n 种不同的表示方式。小C的任务是从这些表示中找到字典序最小的序列,即该序列的“最小表示”。
例如:碱基序列 ATCA 从不同位置读取可能的表示有 ATCA, TCAA, CAAT, AATC,其中 AATC 是字典序最小的表示。
测试样例
样例1:
输入:
dna_sequence = "ATCA"
输出:'AATC'
样例2:
输入:
dna_sequence = "CGAGTC"
输出:'AGTCCG'
样例3:
输入:
dna_sequence = "TTGAC"
输出:'ACTTG'
题目解析
本题要求找到环状 DNA 序列的“最小表示”,即字典序最小的起始序列。环状 DNA 的特点是可以从任何位置开始读取序列,因此我们需要从所有可能的表示中挑选字典序最小的那个。
核心思路
-
构造所有表示:
- 环状 DNA 序列本质上可以通过在字符串后添加其自身来模拟旋转后的所有序列。例如,
ATCA构造为ATCAATCA,然后截取长度为 4 的所有子串。
- 环状 DNA 序列本质上可以通过在字符串后添加其自身来模拟旋转后的所有序列。例如,
-
高效查找最小表示:
- 枚举所有可能的起始位置,找到字典序最小的子串。
- 为了优化性能,使用 Booth’s Algorithm 解决该问题,该算法能在 O(n) 时间复杂度内找到最小表示。
代码实现
def solution(dna_sequence):
def booth_algorithm(s):
"""Booth's Algorithm for finding lexicographically minimal rotation"""
s = s + s # Duplicate string to simulate circular behavior
n = len(s) // 2
f = [-1] * len(s) # Failure function
k = 0 # Least rotation of string found so far
for j in range(1, len(s)):
i = f[j - k - 1]
while i != -1 and s[j] != s[k + i + 1]:
if s[j] < s[k + i + 1]:
k = j - i - 1
i = f[i]
if s[j] != s[k + i + 1]: # Mismatch after failure function exhausted
if s[j] < s[k]:
k = j
f[j - k] = -1
else:
f[j - k] = i + 1
return k
# Find the starting index of the lexicographically smallest rotation
start = booth_algorithm(dna_sequence)
# Return the minimal rotation
return dna_sequence[start:] + dna_sequence[:start]
# Test cases
if __name__ == "__main__":
print(solution("ATCA") == "AATC") # True
print(solution("CGAGTC") == "AGTCCG") # True
print(solution("TTGAC") == "ACTTG") # True
print(solution("TCATGGAGTGCTCCTGGAGGCTGAGTCCATCTCCAGTAG") == "AGGCTGAGTCCATCTCCAGTAGTCATGGAGTGCTCCTGG") # True
代码详解
-
Booth’s Algorithm 的实现:
- 重复字符串以模拟环状行为,例如
ATCA→ATCAATCA。 - 使用 failure function 记录当前子串的匹配情况,并以此加速查找最小字典序。
k记录当前发现的最小起始位置,当发现更小字典序时更新。
- 重复字符串以模拟环状行为,例如
-
时间复杂度:
- Booth’s Algorithm 的时间复杂度为 O(n)O(n)O(n),因为每个字符最多被访问两次。
-
返回最小表示:
- 根据最小起始位置,将字符串重新排列为字典序最小的表示。
图解过程
以 dna_sequence = "ATCA" 为例:
-
重复字符串:
- 构造
ATCAATCA。
- 构造
-
从头开始枚举:
- 比较
ATCA、TCAA、CAAT、AATC。
- 比较
-
找到最小字典序:
AATC是字典序最小的。
此过程通过 Booth's Algorithm 快速定位起始位置,避免逐一比较所有子串。
测试用例分析
样例 1: "ATCA"
- 可能表示:
ATCA,TCAA,CAAT,AATC - 最小字典序:
AATC
样例 2: "CGAGTC"
- 可能表示:
CGAGTC,GAGTCC, ...,AGTCCG - 最小字典序:
AGTCCG
样例 3: "TTGAC"
- 可能表示:
TTGAC,TGACT, ...,ACTTG - 最小字典序:
ACTTG
知识点总结与分析
1. Booth’s Algorithm
梳理:
-
作用:Booth’s Algorithm 是一种线性时间算法,用于在字符串中找到其字典序最小的循环排列。
-
核心思路:
- 将字符串重复一遍(例如
s → s + s),模拟环状行为。 - 维护一个“失败函数”(
failure function),类似于 KMP 算法,用于快速跳过不匹配的字符。 - 通过比较发现的旋转起始位置,逐步更新当前的最小旋转位置。
- 将字符串重复一遍(例如
理解:
- Booth’s Algorithm 的效率高是因为它避免了逐一生成和比较所有旋转的开销,利用了字符串的对称性和失败函数的快速跳转。
- 在环状序列的处理场景中非常有用,例如基因序列分析或循环字符串匹配问题。
2. 失败函数(Failure Function)
梳理:
-
定义:记录当前子串匹配的最长前缀和后缀的长度(类似于 KMP 中的“部分匹配表”)。
-
作用:在发现不匹配时,快速跳过重复计算的部分。
-
构造方法:
- 对于字符串
s,初始化f[0] = -1。 - 遍历每个字符,更新匹配长度
i,同时根据字符是否匹配更新表。
- 对于字符串
理解:
- 失败函数将字符串的重复结构提前计算出来,避免了从头开始匹配的重复工作。
- 在 Booth’s Algorithm 中,它帮助我们在字典序比较中快速跳转到下一可能的最小表示。
3. 循环字符串的最小表示问题
梳理:
- 问题本质:对于一个长度为 nn 的字符串,从所有 nn 种起始位置中找到字典序最小的那个。
- 暴力方法:直接生成所有循环排列,逐一比较,时间复杂度为 O(n2)O(n^2)。
- 优化方法:使用 Booth’s Algorithm 线性查找,时间复杂度为 O(n)O(n)。
理解:
-
循环字符串的最小表示问题与真实场景密切相关,如:
- 基因序列:找最小表示以便于对比或存储。
- 字符串模式匹配:比如寻找回文或子串。
-
Booth’s Algorithm 的最大优势在于无需显式生成所有旋转,仅通过索引操作解决问题。
自己的理解
1. Booth’s Algorithm 的本质
- 本质上是 KMP 算法的扩展,利用了“失败函数”快速定位匹配点。
- 重复字符串是为了模拟环状结构,但算法实际上只需要比较原字符串的长度,节省了存储和计算资源。
2. 环状 DNA 与循环字符串的应用
- 环状 DNA 是循环字符串的一个特例,寻找最小表示可以帮助简化后续的比较和存储。
- 例如,在基因序列分析中,字典序最小表示可以作为一种“归一化”的形式,方便不同序列之间的比对。
3. 算法的应用与推广
-
Booth’s Algorithm 并不仅限于 DNA 序列,还可以推广到其他领域:
- 密码学:寻找最小表示有助于检测循环模式。
- 数据压缩:通过归一化减少冗余存储。
- 图形学:处理循环图形或纹理匹配问题。
知识点之间的联系
- Booth’s Algorithm 是一种解决循环字符串最小表示的工具,而其高效性依赖于 失败函数 的设计。
- 循环字符串问题的优化背后是将数学的组合性质(字符串旋转)映射为字符串算法(字典序比较)的线性解决。
示例帮助理解
示例 1:普通 KMP 和 Booth’s Algorithm 的关系
-
给定字符串
ATCA,通过 Booth’s Algorithm 找到最小旋转。 -
KMP 的部分匹配表(失败函数)如下:
A: 匹配前缀长度 0,f[0] = -1T: 不匹配,f[1] = -1C: 不匹配,f[2] = -1A: 匹配第一个A,f[3] = 0
-
失败函数加速了字典序比较的过程。
示例 2:实际应用
-
输入:DNA 序列
CGAGTC -
输出:
- 重复字符串:
CGAGTCCGAGTC - 枚举旋转:
CGAGTC,GAGTCC,AGTCCG, ... - 最小表示:
AGTCCG
- 重复字符串:
总结
Booth’s Algorithm 是一个优雅的线性算法,解决了循环字符串的最小表示问题,应用场景广泛。理解其核心逻辑可以帮助我们在处理复杂的字符串问题时设计更高效的算法,同时也为基因序列分析等实际问题提供了理论支持。