环状DNA序列的最小表示法
题目分析
本题需要我们找到一个环状 DNA 序列的 字典序最小表示。由于环状结构的特点,从任意位置开始读取序列都是合法的,因此一个长度为 ( n ) 的序列有 ( n ) 种可能的表示方式。我们需要从这些表示方式中找出字典序最小的那个。
以示例序列 "ATCA" 为例,它的所有表示为:
- 从第 1 位开始:ATCA
- 从第 2 位开始:TCAA
- 从第 3 位开始:CAAT
- 从第 4 位开始:AATC
在这些表示中,AATC 是字典序最小的。
解题思路
暴力法
我们可以遍历所有可能的表示方式,然后比较这些表示,选出最小的那个:
- 对于给定的 DNA 序列 ( S ),生成 ( n ) 种可能的表示:分别将 ( S ) 的前 ( i ) 个字符移到末尾。
- 比较这些表示,返回字典序最小的那个。
代码实现如下:
def solution(dna_sequence):
n = len(dna_sequence)
min_representation = dna_sequence # 假设初始序列是最小的
for i in range(n):
# 将前 i 个字符移到末尾
current_representation = dna_sequence[i:] + dna_sequence[:i]
# 更新最小表示
if current_representation < min_representation:
min_representation = current_representation
return min_representation
虽然这种方法能够解决问题,但其时间复杂度是 ( O(n^2) ),因为生成每种表示需要 ( O(n) ) 的时间,而我们需要比较 ( n ) 种表示。
最小表示法
为了优化时间复杂度,可以使用一种更高效的方法——最小表示法 (Minimum Lexicographic Rotation),其核心思想如下:
- 假设给定序列为 ( S ) 的两倍(拼接 ( S+S )),这样可以方便地模拟所有循环移位。
- 通过两个指针 ( i ) 和 ( j ) 来模拟从不同位置开始的循环移位:
- ( i ) 表示当前的候选最小起点。
- ( j ) 表示正在比较的另一个起点。
- 如果发现以 ( j ) 为起点的序列比 ( i ) 为起点的小,则更新 ( i )。
- 遍历一遍即可找到最小表示。
这种方法的时间复杂度是 ( O(n) ),更加高效。
代码实现如下:
def minimal_rotation(dna_sequence):
n = len(dna_sequence)
s = dna_sequence + dna_sequence # 双倍字符串
i, j = 0, 1 # 两个起点指针
while i < n and j < n:
k = 0 # 当前比较的字符偏移量
while k < n and s[i + k] == s[j + k]:
k += 1
if k == n:
break
# 更新较大的起点
if s[i + k] > s[j + k]:
i = i + k + 1
else:
j = j + k + 1
if i == j:
j += 1
# 返回最小表示
start = min(i, j)
return s[start:start + n]
示例运行
以下是对示例的运行结果:
示例 1: "ATCA"
print(minimal_rotation("ATCA")) # 输出: AATC
示例 2: "CGAGTC"
print(minimal_rotation("CGAGTC")) # 输出: AGTCCG
示例 3: "TCATGGAGTGCTCCTGGAGGCTGAGTCCATCTCCAGTAG"
print(minimal_rotation("TCATGGAGTGCTCCTGGAGGCTGAGTCCATCTCCAGTAG"))
# 输出: AGGCTGAGTCCATCTCCAGTAGTCATGGAGTGCTCCTGG
总结
- 暴力法思路清晰,但效率较低,适合理解和验证基本逻辑。
- 最小表示法 利用双指针和双倍字符串的技巧,能够在 ( O(n) ) 时间内高效地找到结果。
- 实际中,如涉及大规模序列数据,推荐使用最小表示法。
通过这个问题,我们学习到了处理环状序列和字典序最小化的技巧,扩展到其他类似问题中也很实用!