AI 刷题 40. 环状 DNA 序列的最小表示法 题解| 豆包 MarsCode AI刷题

96 阅读5分钟

环状 DNA 序列的最小表示法

问题描述:

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

思路解析

本题的重点在于环状二字,如何能体现出环状是本题的关键。本人立马想到数据结构中有许多表示环状的结构,但是并不是所有的结构都适合本题,或者换句话说,能够高质量的解决本题。让我们来一一列举有哪些常见的环状结构,我们做题过程中常见的有单向循环列表、双向循环列表、循环队列这三个。从题目来看,双向循环列表是不必要的,因为他要找的是字典序最小的字符序列,字典序计算的规定是由左向右,所以pass。好了,现在我们已经找到如何构建环状结构的方法了。接下来我将展示实现这道题的伪代码

单向循环列表思路

std::string solution(std::string dna_sequence) {
int n = dna_sequence.size();
Node* head = createCircularLinkedList(dna_sequence);  // 创建循环链表
std::string min_representation = dna_sequence;  // 初始化为原序列

// 遍历每个可能的起始位置
Node* current = head;
for (int i = 0; i < n; ++i) {
    // 生成当前起始位置的子序列
    std::string candidate = "";
    Node* node = current;
    for (int j = 0; j < n; ++j) {
        candidate += node->value;
        node = node->next;
    }
    if (candidate < min_representation) {
        min_representation = candidate;
    }
    current = current->next;  // 移动到下一个节点
}

return min_representation;
}
Node* createCircularLinkedList(std::string dna_sequence) {
Node* head = new Node(dna_sequence[0]);
Node* current = head;
for (int i = 1; i < dna_sequence.size(); ++i) {
    Node* newNode = new Node(dna_sequence[i]);
    current->next = newNode;
    current = newNode;
}
current->next = head;  // 最后一个节点指向头节点,形成循环
return head;
}

循环队列思路

std::string solution(std::string dna_sequence) {
int n = dna_sequence.size();
CircularQueue queue(n);  // 创建循环队列
queue.enqueue(dna_sequence);  // 将 DNA 序列入队
std::string min_representation = dna_sequence;  // 初始化为原序列

// 遍历队列中的每个表示
for (int i = 0; i < n; ++i) {
    std::string candidate = queue.dequeue();  // 出队获取当前表示
    if (candidate < min_representation) {
        min_representation = candidate;
    }
    queue.enqueue(candidate);  // 入队更新队列
}

return min_representation;
}
class CircularQueue {
public:
std::vector<char> queue;  // 队列存储 DNA 序列
int front, rear, size;

CircularQueue(int n) : front(0), rear(0), size(n) {
    queue.resize(n);
}

void enqueue(std::string dna_sequence) {
    for (int i = 0; i < size; ++i) {
        queue[rear] = dna_sequence[i];
        rear = (rear + 1) % size;
    }
}

std::string dequeue() {
    std::string result = "";
    for (int i = 0; i < size; ++i) {
        result += queue[(front + i) % size];
    }
    front = (front + 1) % size;  // 队头指针前移
    return result;
}
};

思路改进

当我顺利完成这道题之后,我一如既往的开始了反思,这道题我为什么解决的这么费劲,但是他的题目难度定位却是简单,很明显,我把题目想复杂了。我开始把矛头指向我思路的出发点,难道一定得用特殊数据结构才能完成这道题嘛,如果我不用特殊数据结构,就无法表示环状结构了嘛。随即,我开始尝试用数组解决,但是单一的数组并不能实现环状的特殊结构。这时我们就需要认识到环状结构的本质是什么,它的本质是按一定规律重组字符串,同时不能改变字符之间的相对位置。在意识到其本质后,我开始大胆创新,终于,我发现了最少两个相同字符串组成的新字符串就能实现环状字符串的需求,即出现环状字符串的所有组合。同时加上滑动窗口的使用便可以顺利获取每个组合。最终,我发现了在不使用复杂数据结构的情况下解决这道问题的方法。完整代码如下。

数组 + 滑动窗口思路

#include <iostream>
#include <string>
std::string solution(std::string dna_sequence) {
int n = dna_sequence.size();
std::string doubled_dna = dna_sequence + dna_sequence;
std::string min_representation = dna_sequence; // 初始化为原序列
for (int i = 1; i < n; ++i) {
std::string candidate = doubled_dna.substr(i, n);
if (candidate < min_representation) {
  min_representation = candidate;
}
}
return min_representation;
}
int main() {
std::cout << (solution("ATCA") == "AATC") << std::endl;
std::cout << (solution("CGAGTC") == "AGTCCG") << std::endl;
std::cout << (solution("TCATGGAGTGCTCCTGGAGGCTGAGTCCATCTCCAGTAG") ==
            "AGGCTGAGTCCATCTCCAGTAGTCATGGAGTGCTCCTGG")
        << std::endl;
return 0;
}

代码解析

首先,我创建了一个由两个原始字符串而组成的新字符串,然后使用滑动窗口进行获取环状字符串的所有组合,同时进行字符串的比较并记录字典序最小字符串,在代码中体现为 “min_representation”。滑动窗口的大小取原始字符串的大小以符合要求,同时循环次数是是n - 1,因为一开始我令一个字符串赋值为原始字符串,当索引为n-1时,此时窗口刚好到达新字符串末尾而不越界免除了边界判断。当循环结束,返回结果,完成题目。

总结与收获

本次解题的最终代码,虽然时间效率上并没有提升,仍然为O(N),但是相比前两个版本的代码,其胜在简单,就算数据结构基础不好的同学也能想到并看懂我这份代码。当然这次解题我还是有不少收获,相比以前,我敢于大胆尝试新解法,不再满足于完成题目即可。当然我还会继续反思,提升自己,以上思路如有不满,敬请开喷。