Skip to content

🤖 Daily Challenge: Problem #762#53

Closed
github-actions[bot] wants to merge 2 commits intomainfrom
daily-challenge/762
Closed

🤖 Daily Challenge: Problem #762#53
github-actions[bot] wants to merge 2 commits intomainfrom
daily-challenge/762

Conversation

@github-actions
Copy link

LeetCode 762. Prime Number of Set Bits in Binary Representation

题目信息

属性 内容
题目链接 https://leetcode.com/problems/prime-number-of-set-bits-in-binary-representation/
难度 Easy
标签 Math, Bit Manipulation

文件位置

  • 头文件:include/leetcode/problems/prime-number-of-set-bits-in-binary-representation.h
  • 源文件:src/leetcode/problems/prime-number-of-set-bits-in-binary-representation.cpp
  • 测试文件:test/leetcode/problems/prime-number-of-set-bits-in-binary-representation.cpp

刚看到这道题的时候,我愣了一下。LeetCode 762,标题长得像个论文题目——"Prime Number of Set Bits in Binary Representation"。听起来很吓人,但让我把它翻译成人话:

我们要干的只有一件事:给你两个数 leftright,在这个区间里,有多少个整数,它的二进制形式里包含素数个的 1?

比如数字 21,二进制是 10101,有三个 1。3 是素数,所以 21 算一个。就这么简单。

第一直觉:这也太直接了吧?

我的第一反应是:"这题是不是太简单了?" 毕竟思路清晰可见:

  1. left 遍历到 right
  2. 对每个数字,数一下二进制里有多少个 1(这就是 popcount,population count)
  3. 判断这个数量是不是素数
  4. 统计总数

代码大概长这样:

int countPrimeSetBits(int left, int right) {
    int count = 0;
    for (int i = left; i <= right; i++) {
        int bits = __builtin_popcount(i);  // GCC 内置函数,数 1 的个数
        if (isPrime(bits)) count++;
    }
    return count;
}

等等,isPrime 怎么实现?如果我写一个通用的素数判断,比如试除法到 sqrt(n),那对于每个数字都要跑一遍。区间长度可能是 10^5 级别,每个数字都要做 sqrt(20) 次除法... 虽然能过,但总觉得哪里不对劲。

我盯着约束条件看:right <= 10^6

10^6 的二进制是什么? 大概是 11110100001001000000,20 位左右。这意味着什么?

任何不超过 10^6 的数字,它的二进制表示里,1 的个数最多只有 20 个。

这是个巨大的发现。我们不是在判断一个可能很大的数是不是素数——我们只需要判断 0 到 20 这些微小的数字里,哪些是素数。

那个"Ah-ha!"时刻

既然范围这么小,为什么还要写 isPrime 函数?为什么还要计算?我们可以硬编码所有可能的答案。

0 到 20 之间的素数有哪些?
2, 3, 5, 7, 11, 13, 17, 19。

就这些。没有别的了。1 不是素数,0 显然也不是。

所以问题变成了:统计 popcount 结果是否在这个 tiny 的集合里。

最直接的方法是搞一个 boolean 数组:

bool isPrime[21] = {false};  // 索引 0-20
isPrime[2] = isPrime[3] = isPrime[5] = isPrime[7] = true;
isPrime[11] = isPrime[13] = isPrime[17] = isPrime[19] = true;

但等等,作为程序员,看到"集合里是否存在"这种操作,我们的 DNA 应该动起来——位掩码(Bitmask)

我们可以把这 20 个可能性压缩到一个整数里:

位置:19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
      |  |  |  |  |  |  |  |  |  |  | | | | | | | | | |
值:  1  0  1  0  0  0  1  0  1  0  0 0 1 0 1 0 1 1 0 0

如果某个位置是素数,就把对应的 bit 设为 1。这个 mask 等于:

(1 << 2) | (1 << 3) | (1 << 5) | (1 << 7) | 
(1 << 11) | (1 << 13) | (1 << 17) | (1 << 19)

算一下:4 + 8 + 32 + 128 + 2048 + 8192 + 131072 + 524288 = 665772。

所以 const int PRIME_MASK = 0xA28AC; (或者十进制 665772)。

现在,素数判断变成了一次位运算:

if (PRIME_MASK & (1 << bits)) {
    // 是素数个 1!
}

这太美了。O(1) 的素数判断,没有分支,没有循环,就是一次位与操作。

构建最终解法

让我们把思路串起来,像搭积木一样。

首先,我们需要计算 popcount。虽然 GCC 提供了 __builtin_popcount,但为了理解原理,我们也应该知道怎么手动算。Brian Kernighan 的算法很优雅:

int popcount(int x) {
    int count = 0;
    while (x) {
        x &= (x - 1);  // 消除最低位的 1
        count++;
    }
    return count;
}

这个操作的原理是:x - 1 会把最低位的 1 变成 0,并把右边所有的 0 变成 1。所以 x & (x-1) 就消掉了最低位的 1。

但既然我们在写实际代码,而且编译器优化得很好,直接用内置函数更清爽:

class Solution {
public:
    int countPrimeSetBits(int left, int right) {
        // 0-20 的素数掩码:2,3,5,7,11,13,17,19
        const int mask = 0b10100010100010101100;  // 二进制看着更清楚
        
        int ans = 0;
        for (int i = left; i <= right; i++) {
            int bits = __builtin_popcount(i);
            if (mask & (1 << bits)) {
                ans++;
            }
        }
        return ans;
    }
};

等等,这里有个微妙的 bug。如果 bits 是 0 或 1,1 << bits 分别是 1 和 2。我们的 mask 在位置 0 和 1 上是 0(因为 0 和 1 不是素数),所以 (mask & (1 << bits)) 会是 0,判断正确。

但让我检查一下 mask 的构造:

  • 位置 2: 1 (是素数)
  • 位置 3: 1 (是素数)
  • 位置 4: 0 (不是)
  • 位置 5: 1 (是素数)
    ...

看起来没问题。但等等,1 << bits 当 bits 是 31 时会有符号问题吗?不会,因为 right <= 10^6,bits 最大 20,远小于 31。

魔鬼在细节里

让我们走一遍具体的 case,确保没踩坑。

假设 left = 6, right = 10

  • 6 = 110b, popcount = 2, 2 是素数 ✓
  • 7 = 111b, popcount = 3, 3 是素数 ✓
  • 8 = 1000b, popcount = 1, 1 不是素数 ✗
  • 9 = 1001b, popcount = 2, 是素数 ✓
  • 10 = 1010b, popcount = 2, 是素数 ✓

总共 4 个。

我们的 mask 包含 2 和 3,所以 2 和 3 对应的 bit 是置位的。1 对应的 bit 是 0。完美。

但是,我注意到一个陷阱:如果题目约束更大呢?比如 right 到 10^9?那 popcount 最大是 30(因为 2^30 > 10^9)。我们只需要扩展 mask 到包含 23, 29 即可。这个方法的扩展性其实很好,只要 popcount 的范围不大(比如小于 64),我们就可以用一个 64 位整数做 mask。

还有一个有趣的边界:如果 left = 1

  • 1 的 popcount 是 1,不是素数。正确排除。

如果 left = 0?虽然题目说正整数,但如果遇到 0,popcount 是 0,也不是素数。

为什么这道题有趣

表面上看,这是一道"简单题"——遍历、计数、判断。但背后藏着的是问题空间的压缩

我们本能地会想写素数筛,或者写一个通用的 isPrime 函数,准备应对万亿级别的素数判断。但仔细看数据范围,发现我们只需要处理 0-20 这 21 种情况。这种**从"通用算法"到"查表法"**的转换,是性能优化的核心思想。

在硬件层面,这就好比把频繁访问的数据塞进 L1 Cache;在算法层面,这就是把 O(sqrt(n)) 的判断变成了 O(1) 的位运算。

而且那个 bitmask 技巧,虽然在这里有点 overkill(毕竟查数组 isPrime[bits] 也很快),但它展示了位运算的优雅——把集合的"成员存在性"问题,转化为一个机器指令就能完成的位与操作。

下次当你看到"判断一个数是否在很小的固定集合里"时,记得这个 0xA28AC。这就是编程的乐趣:把数学的抽象,压缩成硅片上的舞蹈。

本报告由 AI 自动生成。

@0xMashiro 0xMashiro closed this Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant