问题描述
小C正在研究一种环状的 DNA 结构,它由四种碱基A、C、G、T构成。这种环状结构的特点是可以从任何位置开始读取序列,因此一个长度为 n 的碱基序列可以有 n 种不同的表示方式。小C的任务是从这些表示中找到字典序最小的序列,即该序列的“最小表示”。
问题分析
读题时
初读题目,任务是找出环状 DNA 的字典序最小表示。由于是环状 DNA ,每种序列都有 种循环位移的表示方式,每个碱基序列都可以从任意位置开始,因此一个 DNA 序列的所有循环位移都需要考虑。这是一个显式或隐式生成位移的过程,需要注意避免重复计算。
题目没有明确给出序列的长度限制,但由于碱基序列的每个字符都可能被循环处理,暴力解法对大规模输入不友好,需要考虑更高效的算法。
经典的暴力解法是生成所有位移并排序,这在小规模数据上可以轻松完成。然而,考虑到序列长度较大时的性能问题,我想到 Booth 算法——一个专门用于解决环状字符串最小表示的高效方法,其时间复杂度仅为 。
解题时
我两种方法都试了下,都能过,下面分享一下思考过程。
暴力法
思路
- 将 DNA 序列从每个位置开始重新排列,生成 n 个循环位移的字符串。
- 将所有生成的字符串存入一个数组。
- 对数组排序,选择字典序最小的字符串作为结果。
时间复杂度:每次构造一个位移字符串需要 ,总共需要构造 n 个,时间复杂度为 。对 n 个长度为 n 的字符串排序,时间复杂度为 。总复杂度 。
空间复杂度:额外空间主要用于存储 n 个位移字符串,复杂度为 。
可以看到时空开销都很大。
Booth 算法
-
将字符串自身拼接成两倍长度,用于模拟循环位移。
-
维护两个指针 和 ,分别表示两个候选起点。
-
比较 和 :
- 如果相等,继续比较下一位。
- 如果不等,淘汰字典序较大的起点并调整指针。
-
最后选择较小的指针作为最小表示的起始点。
代码分析及注释
解法一:暴力
#include <bits/stdc++.h>
using namespace std;
string solution(string dna_sequence) {
vector<string> result;
for (int i = 0; i < dna_sequence.size(); i++) {
string res = "";
for (int j = 0; j < dna_sequence.size(); j++) {
result = res + dna_sequence[(i + j) % dna_sequence.size()];
}
result.push_back(res);
}
sort(result.begin(), result.end());
return result[0];
}
解法二:Booth 算法
#include <bits/stdc++.h>
using namespace std;
string solution(string dna_sequence) {
int dna_length = dna_sequence.length();
string dna_extended = dna_sequence + dna_sequence;
int p1 = 0;
int p2 = 1;
int offset = 0;
while (p1 < dna_length && p2 < dna_length && offset < dna_length) {
char c1 = dna_extended[p1 + offset];
char c2 = dna_extended[p2 + offset];
int cmp = c1 - c2;
if (cmp == 0) {
offset += 1;
} else if (cmp > 0) {
p1 = max(p1 + offset + 1, p2 + 1);
offset = 0;
} else {
p2 = max(p1 + 1, p2 + offset + 1);
offset = 0;
}
}
int begin_at = min(p1, p2);
return dna_extended.substr(begin_at, dna_length);
}
总结
这道题目结合了字符串处理和算法优化。暴力法适合简单场景,但在处理长 DNA 序列时效率低。Booth 算法充分利用环状结构的特点,时间复杂度大幅降低,是实际应用中的更优解。我学到了如何平衡时间和空间复杂度,也加深了对字典序、双指针技巧的理解。