← 返回题目列表

重复的 DNA 序列如何用位编码优化?为什么 10 个字符只需要 20 位?

中等 第 21 / 26 题 更新于 2026/08/01
位运算滑动窗口位编码DNA序列

简化版

DNA 字符只有 A/C/G/T 四种,可以用 2 位编码一个字符。长度为 10 的 DNA 片段只需要 20 位,用滚动位掩码维护最近 10 个字符的编码,把出现过一次和重复出现的编码分别放入集合即可。

详细版

映射可设为 A=0,C=1,G=2,T=3。遍历字符串时,执行 mask = ((mask << 2) | code) & ((1 << 20) - 1),表示加入新字符并保留最近 10 个字符。第 10 个字符开始,每个 mask 对应一个长度 10 的子串。

seen 记录出现过的 mask,用 duplicated 或结果集合避免重复加入答案。时间复杂度 O(n),空间复杂度 O(n)

完整版教学

一、为什么 DNA 可以压成位编码

DNA 题只包含 4 种字符:A,C,G,T。4 种状态刚好能用 2 个 bit 表示:

字符编码
A00
C01
G10
T11

因此长度 10 的字符串需要:

10 * 2 = 20 bit

一个 int 足够存下这个片段。

二、滚动窗口如何更新 mask

每读入一个新字符,就把旧 mask 左移 2 位,并把新字符编码放到最低 2 位:

mask = (mask << 2) | code

但我们只需要最近 10 个字符,也就是 20 位,所以要用掩码截断:

mask &= (1 << 20) - 1

记忆钩子:左移是窗口向右滑,低位塞新字符,高位用掩码裁掉。

三、为什么第 10 个字符后才开始统计

长度不足 10 时,mask 还不是一个完整 DNA 片段。只有当下标 i >= 9 时,最近 10 个字符才构成一个有效窗口。

i = 0..8  => 不足 10 个字符
i = 9     => 第一个长度 10 子串

从这个时刻开始,当前 mask 和 s[i-9..i] 一一对应。

四、如何判断重复

使用两个集合:

seen:已经出现过一次的编码
added:已经加入答案的重复编码

如果当前 mask 不在 seen,加入 seen;如果已经在 seen 且不在 added,说明这是第二次出现,把对应子串加入答案,同时加入 added 防止答案重复。

这样不会因为某个片段出现 3 次而把答案加 2 次。

五、代码模板

List<String> findRepeatedDnaSequences(String s) {
    Map<Character, Integer> map = Map.of('A', 0, 'C', 1, 'G', 2, 'T', 3);
    Set<Integer> seen = new HashSet<>();
    Set<Integer> added = new HashSet<>();
    List<String> ans = new ArrayList<>();
    int mask = 0;
    int keep = (1 << 20) - 1;
    for (int i = 0; i < s.length(); i++) {
        mask = ((mask << 2) | map.get(s.charAt(i))) & keep;
        if (i >= 9) {
            if (!seen.add(mask) && added.add(mask)) {
                ans.add(s.substring(i - 9, i + 1));
            }
        }
    }
    return ans;
}

这里 substring(i-9, i+1) 正好取最近 10 个字符。

六、用小例子理解窗口

假设读到序列 "ACGTACGTAA"

A C G T A C G T A A
00 01 10 11 00 01 10 11 00 00

这 10 个字符压成 20 位。下一步再读一个 C,左移 2 位后,最早的 A 被高位裁掉,低位加入 C

这就是字符串滑动窗口在 bit 层面的表现。

七、常见误区与追问

  • 误区:用 4 位编码一个字符。 4 种字符只需要 2 位,4 位会浪费且掩码长度错。
  • 误区:忘记保留低 20 位。 不截断会把更早字符也混进编码。
  • 误区:重复出现多次就多次加入答案。 需要额外集合避免答案重复。
  • 追问:为什么 1<<20 安全? 20 位远小于 int 31 位有效正数范围。
  • 追问:不用位编码可以吗? 可以直接用字符串 Set,但会多一些 substring 和哈希成本。
  • 追问:窗口长度变成 L 怎么办? 保留 2*L 位,掩码是 (1 << (2*L)) - 1,注意溢出。

八、加强记忆

重复 DNA 序列的关键是“四个字符两位够”。每次左移 2 位放入新字符,再用低 20 位掩码保留最近 10 个字符。seen 判断是否出现过,added 防止重复加入答案。它本质是滑动窗口,只是窗口内容用整数压缩了。