Closed
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LeetCode 762. Prime Number of Set Bits in Binary Representation
题目信息
文件位置
include/leetcode/problems/prime-number-of-set-bits-in-binary-representation.hsrc/leetcode/problems/prime-number-of-set-bits-in-binary-representation.cpptest/leetcode/problems/prime-number-of-set-bits-in-binary-representation.cpp刚看到这道题的时候,我愣了一下。LeetCode 762,标题长得像个论文题目——"Prime Number of Set Bits in Binary Representation"。听起来很吓人,但让我把它翻译成人话:
我们要干的只有一件事:给你两个数
left和right,在这个区间里,有多少个整数,它的二进制形式里包含素数个的 1?比如数字 21,二进制是
10101,有三个 1。3 是素数,所以 21 算一个。就这么简单。第一直觉:这也太直接了吧?
我的第一反应是:"这题是不是太简单了?" 毕竟思路清晰可见:
left遍历到right代码大概长这样:
等等,
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 数组:
但等等,作为程序员,看到"集合里是否存在"这种操作,我们的 DNA 应该动起来——位掩码(Bitmask)。
我们可以把这 20 个可能性压缩到一个整数里:
如果某个位置是素数,就把对应的 bit 设为 1。这个 mask 等于:
算一下:4 + 8 + 32 + 128 + 2048 + 8192 + 131072 + 524288 = 665772。
所以
const int PRIME_MASK = 0xA28AC;(或者十进制 665772)。现在,素数判断变成了一次位运算:
这太美了。O(1) 的素数判断,没有分支,没有循环,就是一次位与操作。
构建最终解法
让我们把思路串起来,像搭积木一样。
首先,我们需要计算 popcount。虽然 GCC 提供了
__builtin_popcount,但为了理解原理,我们也应该知道怎么手动算。Brian Kernighan 的算法很优雅:这个操作的原理是:
x - 1会把最低位的 1 变成 0,并把右边所有的 0 变成 1。所以x & (x-1)就消掉了最低位的 1。但既然我们在写实际代码,而且编译器优化得很好,直接用内置函数更清爽:
等等,这里有个微妙的 bug。如果
bits是 0 或 1,1 << bits分别是 1 和 2。我们的 mask 在位置 0 和 1 上是 0(因为 0 和 1 不是素数),所以(mask & (1 << bits))会是 0,判断正确。但让我检查一下 mask 的构造:
...
看起来没问题。但等等,
1 << bits当 bits 是 31 时会有符号问题吗?不会,因为 right <= 10^6,bits 最大 20,远小于 31。魔鬼在细节里
让我们走一遍具体的 case,确保没踩坑。
假设
left = 6, right = 10:总共 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?如果
left = 0?虽然题目说正整数,但如果遇到 0,popcount 是 0,也不是素数。为什么这道题有趣
表面上看,这是一道"简单题"——遍历、计数、判断。但背后藏着的是问题空间的压缩。
我们本能地会想写素数筛,或者写一个通用的
isPrime函数,准备应对万亿级别的素数判断。但仔细看数据范围,发现我们只需要处理 0-20 这 21 种情况。这种**从"通用算法"到"查表法"**的转换,是性能优化的核心思想。在硬件层面,这就好比把频繁访问的数据塞进 L1 Cache;在算法层面,这就是把 O(sqrt(n)) 的判断变成了 O(1) 的位运算。
而且那个 bitmask 技巧,虽然在这里有点 overkill(毕竟查数组
isPrime[bits]也很快),但它展示了位运算的优雅——把集合的"成员存在性"问题,转化为一个机器指令就能完成的位与操作。下次当你看到"判断一个数是否在很小的固定集合里"时,记得这个 0xA28AC。这就是编程的乐趣:把数学的抽象,压缩成硅片上的舞蹈。
本报告由 AI 自动生成。