最近刷题时刷到 HJ140《小红的合数寻找》这道题,题目很短,却意外地把合数判断、区间统计、数位枚举这几个基础点全串起来了。再结合搜索时经常一起出现的“1143. 纯粹合数”来看,这其实是一条很典型的“数论入门到进阶”路线:先会判断单个数是不是合数,再学会用筛法批量预处理,最后用前缀和回答多组区间询问。如果你刚开始刷算法题,或者想复习质数筛和前缀和,这篇文章应该能帮你少走不少弯路。
我拿到的题面描述很简单:小红拿到了一个区间 [l, r],想知道这个区间里有多少个合数。部分版本还会追加一个条件,问其中有多少个“纯粹合数”。乍看是个再基础不过的题,但真正写起来,边界条件、筛法写法、输入输出性能都会影响你能不能一次通过。下面我就按自己的解题过程来拆一遍。
1. 题目在考什么:HJ140 的题意还原与核心考点
1.1 我拿到的题面和我对题意的解读
既然是“小红的合数寻找”,核心任务就是在一段连续整数区间里找出满足条件的合数。最常见的版式是:输入第一行一个整数 T,表示有 T 组询问,每组询问给两个正整数 l 和 r,要求输出区间 [l, r] 内合数的数量。
这里有一个必须提前盯死的定义:合数是大于 1 且不是质数的整数。换句话说,1 既不是质数也不是合数,0 也不是合数。很多新手第一次写这题,就是把 1 误判成合数,导致输出结果整体偏大。
至于“纯粹合数”,网上热词对应的是“1143. 纯粹合数”那道题。我见到的常见定义是:一个数 x 是纯粹合数,需要同时满足两个条件:
- x 本身是合数;
- x 的十进制表示的每一位都不是质数数字,也就是每一位只能来自 {0, 1, 4, 6, 8, 9}。
注意 0 和 1 本身不是质数,所以它们不算“质数数字”,在大部分题面里是允许出现的。比如 10、14、16 这些都是合数,而且数位里没有 2、3、5、7,所以算纯粹合数。这一点如果没读清楚,很容易把 10、14 这类数漏掉。
1.2 为什么这道题能串起一整套基础数论技能
这道题的精彩之处在于它一个题覆盖了三个层次的知识点:
第一个是质数/合数判断。单个数判断用试除法,批量判断用筛法,这是数论题的基本功。第二个是区间统计。如果不用前缀和,遇到多组询问基本必超时,这里考察的是“预处理换时间”的思维。第三个是数位拆分。纯粹合数要求检查每一位,这就涉及到十进制拆位,也是很多数位相关题目的雏形。
所以哪怕你只是想在 OJ 上把这道题的分数拿到,也建议把这三个层面的原理都搞清楚。我自己的感受是,做完这道题之后,再看“区间内统计某类特殊数”的题目,思路会清晰很多,因为它本质上就是“筛法/判断 + 前缀和/离线”的组合套路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 合数判断的两种姿势:试除法与筛法
2.1 试除法:判断单个数的合数/质数
判断一个数 n 是不是质数,最简单的办法是试除:枚举从 2 到 sqrt(n) 的所有整数,如果 n 能被其中任何一个整除,那它就是合数;否则就是质数。
为什么枚举到 sqrt(n) 就够了?因为如果 n 能分解成两个因子 a 和 b,也就是 n = a * b,那么 a 和 b 里至少有一个不超过 sqrt(n)。换句话说,如果 2 到 sqrt(n) 之间没有 n 的因子,那 n 一定是质数。这个结论虽然简单,却把 O(n) 的判断降到 O(sqrt(n)),在 n 比较小的时候非常实用。
python复制def is_prime(n):
if n < 2:
return False
i = 2
while i * i <= n:
if n % i == 0:
return False
i += 1
return True
这里有一个很多人忽略的坑:当 n 很大时,i * i 可能会溢出整数范围。比如在 C++ 里,n 接近 2^31 时,i*i 直接爆 int。保险的写法是改成 i <= n // i(Python)或 i <= n / i(C++)。虽然本题数据范围未必这么极限,但这个习惯最好从一开始就养成。
2.2 埃氏筛:批量预处理质数的首选
如果区间上限 n 是 1e6 甚至 1e7,而且有多组询问,再对每个数单独试除就不太合适了。这时候应该用筛法一次性把所有数的质数/合数状态算出来。
埃氏筛的思路很直白:准备一个布尔数组 is_prime,初始全部为 True,先把下标 0 和 1 设为 False。然后从 2 开始,遇到一个质
