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

17 阅读4分钟

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

环状 DNA 序列的最小表示法 - MarsCode

接触了一个新的算法,布斯算法,但其实是贪心+双指针。但是双倍字符串这个思路还是很新奇的,记录一下。


摘要

小C正在研究一种环状 DNA 序列结构。由于环状结构的特性,可以从任意位置读取序列。任务是确定字典序最小的表示形式。本文采用 布斯算法(Booth's Algorithm) 来高效地解决这一问题。


问题描述

环状 DNA 序列最小表示

给定一个字符串 dna_sequence,表示环状 DNA 序列,可从任意位置开始读取。目标是找到所有可能表示形式中的 字典序最小 的表示。

示例:

  • 输入:dna_sequence = "ATCA"
  • 环状序列所有可能表示:
    • "ATCA"
    • "TCAA"
    • "CAAT"
    • "AATC"

字典序最小的表示为:"AATC"


测试样例

示例 1

输入:

dna_sequence = "ATCA"

输出:

"AATC"
示例 2

输入:

dna_sequence = "CGAGTC"

输出:

"AGTCCG"
示例 3

输入:

dna_sequence = "TTGAC"

输出:

"ACTTG"

算法原理

布斯算法的核心思路

布斯算法专门用于求解环状字符串的 字典序最小表示。其主要原理是利用双倍字符串和贪心策略,避免了暴力枚举所有旋转字符串的高成本。

  1. 构造双倍字符串
    • 为模拟环状结构,将字符串重复一次。例如:
      • 输入:"ATCA"
      • 双倍字符串:"ATCAATCA"
  2. 双指针比较
    • 使用两个指针 ij 分别指向两个候选起点。
    • 使用变量 k 表示目前比对字符串的长度
    • 从两指针位置开始,逐字符比较对应字母的大小。
  3. 贪心更新
    • 如果 T[i + k] < T[j + k],则保留 i 位置作为较优的起点,跳过 j,更新j=j+k+1,如果此时i==j,则j++(确保不是同一起点)。
    • 如果 T[i + k] > T[j + k],则保留 j 位置作为较优的起点,跳过 i,更新i=i+k+1,如果此时i==j,则i++,理由同上。
    • 如果字符相等,继续比较下一字符,即k++
  4. 终止条件
    • 当某一指针超出原字符串长度时,选择另一个指针作为起点。

算法步骤

  1. 初始化

    • 构造双倍字符串 T=S+ST = S + S
    • 设置两个指针 i=0i = 0j=1j = 1,以及一个匹配长度 k=0k = 0
  2. 指针比较

    • 比较 T[i+k]T[i + k]T[j+k]T[j + k]
      • 如果 T[i+k]=T[j+k]T[i + k] = T[j + k],继续比较下一位,令 k=k+1k = k + 1
      • 如果 T[i+k]<T[j+k]T[i + k] < T[j + k],说明 i 是更优的起点,跳过 j:
        • 更新 j=j+k+1j = j + k + 1,并重置 k=0k = 0
      • 如果 T[i+k]>T[j+k]T[i + k] > T[j + k],说明 jj 是更优的起点,跳过 ii
        • 更新 i=i+k+1i = i + k + 1,并重置 k=0k = 0
    • 如果 i=ji = j,则将 j=i+1j = i + 1
  3. 输出结果

    • 最终的最小起点是 iijj 中较小的一个,返回从该起点开始的长度为 nn 的子串。

时间复杂度

  1. 字符串比较:每个字符最多比较两次,总体复杂度为 O(n)O(n)
  2. 总时间复杂度O(n)O(n)

空间复杂度

  1. 使用了双倍字符串和常量辅助变量。
  2. 总空间复杂度O(n)O(n)

Go 实现

package main

import "fmt"

// 布斯算法解决环状字符串的最小字典序表示
func solution(dna_sequence string) string {
	n := len(dna_sequence)
	// 构造双倍字符串
	doubleS := dna_sequence + dna_sequence
	// 初始化双指针
	i, j := 0, 1
	k := 0

	// 双指针扫描
	for i < n && j < n && k < n {
		if doubleS[i+k] == doubleS[j+k] {
			k++
		} else if doubleS[i+k] > doubleS[j+k] {
			// 如果 i 开始的字典序更大,移动 i 指针
			i = i + k + 1
			k = 0
			if i == j {
				i++
			}
		} else {
			// 如果 j 开始的字典序更大,移动 j 指针
			j = j + k + 1
			k = 0
			if i == j {
				j++
			}
		}
	}

	// 最小起点
	pos := min(i, j)
	return doubleS[pos : pos+n]
}

// 辅助函数:返回较小值
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func main() {
	// 测试用例
	fmt.Println(solution("ATCA") == "AATC")                              // true
	fmt.Println(solution("CGAGTC") == "AGTCCG")                          // true
	fmt.Println(solution("TTGAC") == "ACTTG")                            // true
}

Python 实现

def solution(dna_sequence: str) -> str:
    """
    使用布斯算法解决环状字符串的最小字典序表示问题。
    :param dna_sequence: 待处理的 DNA 序列字符串
    :return: DNA 序列的最小字典序表示
    """
    n = len(dna_sequence)
    # 构造双倍字符串
    double_s = dna_sequence + dna_sequence
    # 初始化双指针
    i, j, k = 0, 1, 0

    # 双指针扫描
    while i < n and j < n and k < n:
        if double_s[i + k] == double_s[j + k]:
            k += 1
        elif double_s[i + k] > double_s[j + k]:
            # 如果 i 开始的字典序更大,移动 i 指针
            i = i + k + 1
            k = 0
            if i == j:
                i += 1
        else:
            # 如果 j 开始的字典序更大,移动 j 指针
            j = j + k + 1
            k = 0
            if i == j:
                j += 1

    # 最小起点
    pos = min(i, j)
    return double_s[pos:pos + n]


if __name__ == "__main__":
    # 测试用例
    print(solution("ATCA") == "AATC")                              # 应输出 True
    print(solution("CGAGTC") == "AGTCCG")                          # 应输出 True
    print(solution("TTGAC") == "ACTTG")                            # 应输出 True

输出解释

示例 1:

输入:

dna_sequence = "ATCA"

所有可能表示:

  • "ATCA"
  • "TCAA"
  • "CAAT"
  • "AATC"

字典序最小为:"AATC"

示例 2:

输入:

dna_sequence = "CGAGTC"

所有可能表示:

  • "CGAGTC"
  • "GAGTCC"
  • "AGTCCG"
  • ...

字典序最小为:"AGTCCG"

示例 3:

输入:

dna_sequence = "TTGAC"

所有可能表示:

  • "TTGAC"
  • "TGACT"
  • "GACTT"
  • "ACTTG"
  • ...

字典序最小为:"ACTTG"