简单题:环状 DNA 序列的最小表示法| 豆包MarsCode AI刷题
环状 DNA 序列的最小表示法解析
问题概述
在这个问题中,我们面对的是一个关于环状 DNA 序列的问题。由于 DNA 序列是环状的,所以可以从任何位置开始读取序列,这导致了同一个序列有多种不同的表示方式。我们的任务是找出这些表示方式中字典序最小的一个,即所谓的“最小表示”。
思路分析
-
生成所有可能的表示:最直接的方法是生成所有可能的序列表示,并比较它们的字典序。然而,对于较长的序列,这种方法的时间复杂度会非常高,因为它需要生成和比较大量的字符串。
-
优化策略:考虑到 DNA 序列的循环特性,我们可以利用字符串的旋转和比较操作来优化这个问题。具体来说,我们可以固定一个起始位置,然后通过比较不同起始位置得到的旋转字符串的字典序,来找到最小的表示。
-
字典序比较:在比较两个字符串时,我们只需要比较它们的前缀,直到找到第一个不同的字符。这个字符的 ASCII 值较小的字符串,其字典序也更小。
-
避免重复计算:由于 DNA 序列是环状的,所以某些旋转字符串的后缀和前缀是重叠的。我们可以利用这个特性来减少不必要的比较。
代码详解
public class MinimumRepresentationOfCircularDNA {// 方法:找到环状 DNA 序列的最小表示法public static String findMinimumRepresentation(String dnaSequence) {int n = dnaSequence.length();int i = 0, j = 1, k = 0;// 使用 Floyd 的循环字符串匹配算法(也称为“乌龟与兔子”算法)来找到最小表示while (i < n && j < n) {// 比较从 i 和 j 开始的两个子串while (k < n && dnaSequence.charAt((i + k) % n) == dnaSequence.charAt((j + k) % n)) {k++;}// 如果 k 已经遍历完整个字符串,说明两个子串完全相同,此时可以随意选择一个作为起点(但实际上不会进入这个分支,因为后续会有 i 或 j 的移动)if (k == n) {break;}// 根据字典序大小移动指针if (dnaSequence.charAt((i + k) % n) > dnaSequence.charAt((j + k) % n)) {i = (i + k + 1) % n; // i 移动到下一个不同字符的下一位(考虑循环)} else {j = (j + k + 1) % n; // j 移动到下一个不同字符的下一位(考虑循环)// 如果 i 和 j 相遇,说明找到了一个最小表示法的起点(但此时 i 和 j 相等,所以选择 i(或 j)并加上 k+1(即最小循环的起始位置到当前位置的偏移量,但这里不需要真的加,因为后续直接构建字符串)来构建最小表示)// 注意:由于 i 和 j 相遇时,我们已经知道从 i(或 j)开始的后缀是字典序最小的,所以我们只需要从 i(或 j)开始构建字符串即可// 同时,由于字符串是循环的,我们可以将 i(或 j)之后的部分与前面的部分拼接起来形成最小表示if (i == j) {j = (j + 1) % n; // 为了避免死循环(虽然在这个特定问题中不会发生,但加上这行代码可以使算法更加健壮)// 但实际上,在这个问题中,当 i == j 时,我们已经可以构建最小表示了,不需要再移动 j// 这行代码在这里是多余的,但为了保持与一些标准实现的一致性,我还是保留了它(并注释说明了其多余性)}// 注意:这里的 j 移动实际上是为了下一个循环做准备,而不是为了构建当前的最小表示// 构建最小表示是在 while 循环外部进行的}// 重置 k,为下一轮比较做准备k = 0;}// 由于 i 和 j 最终会指向同一个位置(或者相差 n 的整数倍,但在这个问题中,我们可以忽略这个细节),我们选择 i(或 j)作为起点来构建最小表示// 由于字符串是循环的,我们将从 i 开始的后缀与前面的部分拼接起来形成最小表示int startIndex = Math.min(i, j); // 实际上,i 和 j 最终会相等,所以这里取哪个都可以return dnaSequence.substring(startIndex) + dnaSequence.substring(0, startIndex);}// 主方法:测试 findMinimumRepresentation 方法public static void main(String[] args) {// 测试样例String dnaSequence1 = "ATCA";String dnaSequence2 = "CGAGTC";String dnaSequence3 = "TTGAC";String dnaSequence4 = "TCATGGAGTGCTCCTGGAGGCTGAGTCCATCTCCAGTAG";// 输出最小表示System.out.println("Minimum representation of \"" + dnaSequence1 + "\": " + findMinimumRepresentation(dnaSequence1)); // 应该输出 "AATC"System.out.println("Minimum representation of \"" + dnaSequence2 + "\": " + findMinimumRepresentation(dnaSequence2)); // 应该输出 "AGTCCG"System.out.println("Minimum representation of \"" + dnaSequence3 + "\": " + findMinimumRepresentation(dnaSequence3)); // 应该输出 "ACTTG"System.out.println("Minimum representation of \"" + dnaSequence4 + "\": " + findMinimumRepresentation(dnaSequence4)); // 应该输出 "AGGCTGAGTCCATCTCCAGTAGTCATGGAGTGCTCCTGG"(或循环等价的其他形式,但考虑到输出长度和可读性,这里给出的是完整形式)}
}
代码详解:
- 我们使用三个指针
i
,j
,k
来遍历和比较字符串。 i
和j
分别表示两个不同起始位置的索引。k
用于比较从i
和j
起始的字符串的前缀。- 当
k
位置上的字符相同时,我们增加k
的值,继续向后比较。 - 当找到第一个不同的字符时,我们根据字典序大小移动
i
或j
。 - 如果
i
和j
相遇,说明我们已经遍历了一圈,并且找到了一个可能的最小表示起点。但由于i
和j
此时相等,我们只需要选择其中一个(这里选择i
)作为起点,并加上k+1
个字符(即最小循环的起始位置到当前位置的字符数)来构成最小表示。 - 最后,我们返回从最小表示起点开始的子串,这个子串就是我们要找的最小表示。
个人思考与分析
这个问题考察的是对字符串处理和比较算法的理解。通过生成所有可能的表示并比较它们的字典序,我们可以直观地解决这个问题,但这种方法的时间复杂度较高。通过优化策略,我们可以避免生成所有可能的表示,而是利用字符串的循环特性和字典序比较来找到最小表示。这种方法不仅降低了时间复杂度,还提高了代码的可读性和效率。
此外,这个问题还提醒我们,在处理具有循环特性的数据结构时,要善于利用这种特性来优化算法。通过固定一个起始位置,并比较不同起始位置得到的旋转字符串的字典序,我们可以找到满足条件的最小表示。这种方法在字符串处理、数组处理等领域都有广泛的应用。