news 2026/9/18 5:20:44

纯粹合数怎么判断?C++两种解法与质数筛优化思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯粹合数怎么判断?C++两种解法与质数筛优化思路

把东华OJ的题库翻到第100题,名字叫“纯粹合数”,第一眼看到这个题,我脑子里冒出来的想法是:这不就是判断合数然后筛一遍就行了?等我真正把题面读清楚,才发现“纯粹”两个字的含义比我想象的复杂得多——它要求一个数的每一位数字本身也必须是合数。也就是说,光是判断整个数是不是合数远远不够,还得管到数字的“零件”。

这道题用C++做下来,其实是一个典型的“题目看着简单、想快速AC还得动点脑子”的题。它既考了质数判断的基本功,又考了枚举策略的选择,还顺便磨了一下输出格式这种容易被忽略的细节。今天就把我的完整思路、两种解法、踩过的坑一次说清楚。

1. “纯粹合数”的数学定义:数字级合法性才是真正的考点

1.1 拆解题意:从“合数”到“每一位都是合法数字”

先说合数本身的定义:一个大于1的自然数,如果除了1和它自身之外还有其他因数,就称为合数。换句话说,它不是质数,而且不是0和1。这个大家都熟,但“纯粹合数”的条件是两层:

第一层,整个数必须是合数。比如 46 是合数(2 × 23),满足第一层。

第二层,这个数的十进制表示中,每一位数字都必须是合数。4 和 6 本身都是合数,所以 46 就是一个纯粹合数。再比如 489:4、8、9 三个数字全是合数,489 本身等于 3 × 163,也是合数,所以 489 也是纯粹合数。

那什么情况下会挂在第二层?举个反例:325。325 本身是合数(5 × 65),但数字里有个 3,3 是质数,不是合数,所以 325 不是纯粹合数。再比如 27 这种,2 和 7 都是质数,哪怕 27 是合数,也完全不满足纯粹条件。

到这里就能发现,这道题真正需要盯住的不是“合数判断有多难”,而是“哪些数字能出现在这种数的每一位上”。

1.2 边界值辨析:0、1、2、3、5、7为什么不能出现在纯粹合数里

按数学定义,0 和 1 既不是质数也不是合数,所以永远不能算作“合数数字”。剩下的一位数里,2、3、5、7 都是质数,也进不了候选集合。所以,真正能用的个位数字只有四个:4、6、8、9。

这个结论是整个题目的突破口。你想,四位纯粹合数其实就只能由 4、6、8、9 这四个数字拼出来,每一位都只能从这四个里选。这么一想,枚举范围瞬间从“所有四位整数”缩小到 4 × 4 × 4 × 4 = 256 种排列,这个数量级对计算机来说压根不算事。

我第一次做这道题时,就是被“纯粹”这个词绕了一下,差点把 2 也算进去,因为潜意识里总觉得 2 是偶数、应该算合数。但质数的定义很明确:质数是大于1且只有1和自身两个正因数的自然数。2 只有 1 和 2 两个因数,所以它是质数。这一点如果没搞清楚,后面程序跑出来的结果肯定是错的。

2. 初版暴力解法:直观三层循环与复杂度分析

2.1 判断一个数是否为合数的函数实现

既然是算法题,最稳妥的起步方式是先写一个暴力版本,保证答案正确,再考虑优化。暴力思路很简单:从 1000 到 9999 遍历每个数,先判断这个数本身是不是合数,再拆出每一位,判断每个数字是不是落在 {4, 6, 8, 9} 里。两个条件都满足,就记录并输出。

判断合数这事,先判断是不是质数,再取反就行。C++代码可以这样写:

#include <iostream> using namespace std; bool isPrime(int x) { if (x < 2) return false; // 0和1不是质数 for (int i = 2; i * i <= x; i++) { if (x % i == 0) return false; } return true; } bool isComposite(int x) { return x >= 2 && !isPrime(x); } bool isLegalDigit(int d) { return d == 4 || d == 6 || d == 8 || d == 9; }

注意i * i <= x这个写法,它的意思是只需要检查到根号 x 即可。因为如果 x 有一个大于根号 x 的因数,那必然同时存在一个小于根号 x 的因数,从小的那个开始试就能试出来。用i * i <= x而不用i <= sqrt(x),是为了避免浮点运算可能带来的精度误会,比如某些边界值刚好在根号附近时,浮点舍入可能导致循环多跑一圈或少跑一圈。

2.2 逐位拆分与数字合法性判断

有了辅助函数,主逻辑就清爽了:

int main() { bool first = true; for (int n = 1000; n <= 9999; n++) { if (!isComposite(n)) continue; // 第一层:整个数必须是合数 bool ok = true; int t = n; while (t > 0) { // 第二层:每一位都必须是合数数字 if (!isLegalDigit(t % 10)) { ok = false; break; } t /= 10; } if (ok) { if (!first) cout << ' '; first = false; cout << n; } } cout << endl; return 0; }

这个写法里,t % 10取出当前最低位,判断完就t /= 10把这一位丢掉,循环到数字取完为止。对四位整数来说就是循环四次,逻辑不需要任何额外数组。输出部分用空格分隔、末尾统一换行,符合多数OJ的常规要求。

2.3 暴力的代价:时间复杂度与可优化点

暴力版本要遍历 9000 个数,每个数判断合数时要跑最多sqrt(n)次除法,也就是大约 100 次。总计算量在 90 万次除法的量级,这个数字对现代CPU来说微不足道,东华OJ 上直接提交也能过。

但如果你想追求更优雅的解法,或者想把这类题目的通用思路练熟,暴力版本还有两个明显的可优化点:

第一个点是质数判断被反复执行了 9000 次,里面有大量重复计算。比如判断 1000 是合数时会试除,判断 1001 是合数时又要重新从 2 开始试,完全没有复用前面的结果。

第二个点是遍历了 9000 个数,但真正可能满足“每一位都是4、6、8、9”的数只有 256 个。也就是说,绝大部分遍历工作是在浪费——那些含 0、1、2、3、5、7 的数字,在第一轮就注定了不满足条件,却被白白检查了个遍。

所以,更聪明的做法是把“生成候选数”和“判断合数”解耦。

3. 欧拉筛预处理+合法数字枚举:把两件事彻底解耦

3.1 为什么质数判断可以用“查表”替代

既然我们处理的最大范围不超过四位数(9999),完全可以在程序一开始就用筛法把所有质数标记出来,之后判断一个数是不是质数,只需要查一次布尔数组,时间复杂度为 O(1)。这就是典型的“空间换时间”:预先把知识整理成表,后面每次查询都直接翻答案。

筛法里我推荐欧拉筛(线性筛),它的思想是保证每个合数只被它的最小质因子筛掉一次,所以整体复杂度是 O(n)。相比之下,埃拉托斯特尼筛法虽然代码更短,但每个合数可能被多个质数重复标记,虽然实际应用中差别不大,但从学习角度了解欧拉筛更能加深对“唯一分解定理”的理解。

欧拉筛的C++实现:

const int MAXN = 10000; bool isPrime[MAXN]; void eulerSieve(int n) { vector<int> primes; for (int i = 0; i <= n; i++) isPrime[i] = true; isPrime[0] = isPrime[1] = false; for (int i = 2; i <= n; i++) { if (isPrime[i]) primes.push_back(i); for (int p : primes) { if (i * p > n) break; isPrime[i * p] = false; if (i % p == 0) break; // 核心:保证每个合数只被最小质因子筛掉 } } }

筛完之后,isPrime[4999]就能直接告诉我们 4999 是不是质数,不再需要任何循环除法。这个“把判断变成查表”的思路,在很多数字相关的算法题里都能用到。

3.2 基于4个合法数字拼接候选值

然后我们改变枚举思路:不再遍历 9000 个四位数,而是直接用 {4, 6, 8, 9} 四个数字做四层循环,拼出所有可能的四位候选值。这样做的好处是,从源头就保证了每一位都合法,连拆位判断都省了。

int main() { eulerSieve(9999); const int digits[4] = {4, 6, 8, 9}; int res[1000]; int cnt = 0; // 四层循环,每位只从4、6、8、9里选 for (int a = 0; a < 4; a++) { for (int b = 0; b < 4; b++) { for (int c = 0; c < 4; c++) { for (int d = 0; d < 4; d++) { int num = digits[a] * 1000 + digits[b] * 100 + digits[c] * 10 + digits[d]; // 直接查表判断是不是合数,等价于 isComposite(num) if (num >= 2 && !isPrime[num]) { res[cnt++] = num; } } } } } // 升序输出结果,空格分隔 for (int i = 0; i < cnt; i++) { if (i) cout << ' '; cout << res[i]; } cout << endl; return 0; }

由于生成时从千位到个位都是按 4、6、8、9 的顺序嵌套循环,得到的候选值天然是从小到大排列,不需要额外排序。这一点和暴力遍历法是一样的,都是升序。

3.3 两种解法的性能实测对比

我在本地跑了两组数据,结果如下:

解法候选数枚举规模质数判断方式大致时间
暴力遍历+逐个试除9000个四位数每个数最多约100次除法约 1-2 ms
欧拉筛+合法数字拼接256个四位数每次查表 O(1)0.1 ms 以下

说实话,对这道题的数据范围,两种解法提交到OJ上都能过,性能差异肉眼几乎看不出来。但第二种解法的价值在于:它体现了一种更通用的思考方式——先用数学条件缩小搜索空间,再引入预处理手段消除重复计算。这种思路以后遇到“满足某些数字特征”的题目时,全都是同一个套路。

4. 合数判断的工程细节:sqrt上限、函数复用与防呆设计

4.1 sqrt(9)=3的边界问题

用试除法判断质数时,循环结束条件写成i * i <= x的另一个原因是避免浮点误差。拿 x = 9 举例,sqrt(9) 精确等于 3,但如果某个实现里sqrt(9)返回 2.999999999,然后循环条件写i <= sqrt(x),当 i = 3 时循环可能直接跳出,导致 9 被误判成质数。

当然在实际环境中,sqrt(9)返回 3.0 的概率极高,但写算法一定要考虑最坏情况。用i * i <= x完全是整数运算,不存在精度问题。这也是很多有经验的选手默认的写法。

另外要注意,试除法里 i 从 2 开始,不考虑 1。因为1是所有数的因数,用它试除没有任何区分度。

4.2 把质数表当成“知识库”来用

如果你用的是欧拉筛方案,那isComposite这个函数其实可以写得非常朴素:

bool isComposite(int x) { return x >= 2 && !isPrime[x]; }

这里x >= 2是必须的,因为筛法里isPrime[0]isPrime[1]都被标成了 false,如果直接写!isPrime[x],0 和 1 也会被误判成合数,这不符合数学定义。看,边界条件在任何方案里都躲不掉。

这种防呆设计的价值在于:它让程序对脏数据也有一定鲁棒性。比如以后你把这段逻辑抽出去做范围更大的计算,传入的数是负数或者0,也不会产生错误输出。写算法题不要只盯着“这次能过”,把函数设计得健壮一点,以后反复用的概率很高。

还有一个小经验:如果你把isPrime数组声明成局部变量,记得初始化;如果声明成全局数组,C++ 默认会初始化为 0(false),但为了可读性我还是会在筛法里显式赋值。全局变量 + 显式初始化,这是最不容易出错的组合。

5. 输出格式与OJ提交的隐蔽扣分点

5.1 升序输出是隐含要求

这道题题面里通常不会专门强调“升序”,但从结果展示的角度,OJ的裁判程序一般会按照严格字符串匹配来比对输出。如果你的结果顺序和标准答案不一致,哪怕数字全对,也会被判 Wrong Answer。

暴力法按 1000 -> 9999 正序枚举,天然升序;拼接法因为嵌套循环的顺序是 4 -> 6 -> 8 -> 9,也天然升序。所以只要不手滑把循环顺序改了,一般不会出问题。但如果你用了别的枚举方式,比如把数字存进 set 后遍历,或掉进容器顺序的坑,那就可能输出乱序。

我的习惯是:无论题面是否明确要求升序,都按升序输出。这不仅仅是迎合裁判,也是让人类检查答案时更舒服。

5.2 空格、换行、末尾多余输出的处理

输出格式的细节包括三点:

第一,数字之间用什么分隔。常见的是空格分隔,也有题要求换行。如果你不确定,看样例输出的最后一行有没有多余空格——样例里往往藏着答案。

第二,行末是否有空格。很多OJ的裁判程序会忽略行末空格和末尾换行差异,但也有些严格要求逐字节一致。稳妥的做法是:第一个数字前不打空格,之后每个数字前打一个空格,这样最后一个数字后没有多余空格。上面代码里的if (i) cout << ' ';就是干这个的。

第三,禁止输出额外信息。比如“共有 87 个纯粹合数”这种提示性语句,在本地调试时很有用,提交前一定要注释掉或删掉,否则一律判错。

我自己在这里吃过亏。有一次我为了调试方便在程序里加了cout << "count:" << cnt << endl;,本地跑起来很爽,结果提交的时候忘了删,白送了一发 WA。后来我养成一个习惯:调试输出全部用cerr,因为cerr走的是标准错误流,OJ 比对输出的时候通常只比对cout对应的标准输出流,这样即使忘了删调试代码,也不会影响判题结果。

6. 我在刷这题时实际踩过的坑与调试过程

6.1 第一次提交:答案错误,原因是把2当成了合数

说说我第一次做这个题的真实经历。最开始我手里的候选数字集合写的是{2, 4, 6, 8, 9},我当时的想法是:2 是偶数啊,偶数不都是合数吗?结果程序跑出来一堆奇奇怪怪的结果,比如 2222 被输出了,但它明明是合数同时每一位都是 2……按我的逻辑它确实算“纯粹合数”,但数学定义不认这个账。

后来我一查定义,才反应过来:质数是只有1和自身两个因数的数,而 2 只有 1 和 2 两个因数,所以它是质数;偶数不一定是合数,2 就是唯一的偶质数。这个知识点初中就学过,但写代码时人的直觉经常会压过课本知识。

所以这题的第一个坑,恰恰是最基础的定义。做算法题时,凡是涉及数学定义的判断,务必以定义为准,不要依赖直觉。

6.2 用手算小数据集验证算法正确性

为了验证程序对不对,我建议先不算四位数,把问题缩小到一位数和两位数,手算几个结果,再反推程序是否一致。

比如两位数纯粹合数,从 44、46、48、49、64、66、68、69、84、86、88、89、94、96、98、99 这些组合里筛选:

  • 44 是合数,且两位都是合数 -> 符合
  • 46 是合数 -> 符合
  • 48 是合数 -> 符合
  • 49 = 7 × 7 是合数 -> 符合
  • 64 是合数 -> 符合
  • 66 是合数 -> 符合
  • 68 是合数 -> 符合
  • 69 = 3 × 23 是合数 -> 符合
  • 84 是合数 -> 符合
  • 86 是合数 -> 符合
  • 88 是合数 -> 符合
  • 89 是质数 -> 不符合
  • 94 是合数 -> 符合
  • 96 是合数 -> 符合
  • 98 是合数 -> 符合
  • 99 是合数 -> 符合

所以两位数纯粹合数一共 15 个,唯一的例外是 89。这个例子可以很好地验证你的合数判断函数。

我在本地测试时,先只跑isComposite函数,打印 2 到 100 里所有的合数,跟手写的结果比对;再单独跑isLegalDigit,确认候选集合只有 4、6、8、9。分步验证比一次性看最终输出要容易定位问题。如果你发现两位数结果和自己手算不一致,那就逐层排查,别直接看四位数结果。

6.3 扩展思考:如果题目改成“纯粹质数”或任意位数怎么办

做完这题之后,可以顺手做一个小扩展:如果题目改成“输出所有四位纯粹质数”,你会怎么改?

思路其实完全一样:先把候选数字集合从 {4, 6, 8, 9} 换成质数数字 {2, 3, 5, 7},再把判断条件从“是合数”改成“是质数”,其余代码结构完全不用动。这就是把数字合法性和整个数的属性解耦带来的好处,两套逻辑互不干扰。

如果题目改成“输出1000以内所有纯粹合数”,那就把枚举范围的边界改一下,循环层数从四层改成三层,或者直接用一位数/两位数/三位数分别拼接。这类变体在OJ里经常出现,掌握“构造合法候选 + 查表判断属性”的组合拳,几乎所有这类题都能轻松应对。

回头再看这道“纯粹合数”,它真正的难点从来不是“判断合数”这个动作,而是你能不能把“位数字合法性”和“整个数属性”当成两个独立维度来处理。想通了这一点,代码量反而比暴力版本更少,思路也更清晰。

如果你正在刷东华OJ的基础题,遇到类似这种“XX数”的题目,我建议你统一走这个流程:先把数学定义严格划清边界,再决定是遍历还是构造候选数,最后用筛法或查表优化重复计算。这套流程下来,不只是这一道题,后面很多数论题都能少走弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 5:20:42

ANSYS Workbench带孔平板静力学实例:网格收敛与参数化分析

简介&#xff1a;ANSYS Workbench是一款集成多物理场分析的工程仿真平台&#xff0c;这份PDF教程聚焦其Mechanical模块&#xff0c;面向需要开展结构与热分析的工程师、科研人员及高校学生。内容从Workbench环境启动、项目文件管理和单位制设置讲起&#xff0c;系统讲解几何模型…

作者头像 李华
网站建设 2026/9/18 5:20:08

Git Submodule实战:从添加子模块到递归克隆与踩坑指南

前阵子接手一个微服务仓库&#xff0c;里面要复用三个公共库&#xff1a;一份API定义、一套工具函数、一份配置模板。最初大家都是复制粘贴&#xff0c;结果每次一改接口&#xff0c;其他服务马上编译失败&#xff0c;喊破嗓子才对齐。后来我把这三个公共库改成 git submodule …

作者头像 李华
网站建设 2026/9/18 5:19:36

实测DeepSeek 4.1 Flash:轻量级模型的能力边界与正确用法

最近有技术群里的朋友让我测一下 DeepSeek 4.1 Flash&#xff0c;说这个版本听起来“又快又轻”&#xff0c;很适合接入线上业务。我本来不想碰&#xff0c;因为“Flash”这种后缀在模型产品线里通常意味着妥协&#xff0c;但群里催得紧&#xff0c;我还是专门花了一个下午&…

作者头像 李华
网站建设 2026/9/18 5:18:23

SAP Cloud Integration OData API客户端证书认证完整指南

你们有没有遇到过这种场景&#xff1a;外部系统要调用你搭在 SAP Cloud Integration 上的 OData API&#xff0c;对方张口就要用户名密码&#xff0c;你心里却特别不踏实。Basic Auth 确实简单&#xff0c;可凭据一旦从某个日志里漏出去&#xff0c;整个接口就等于裸奔。尤其遇…

作者头像 李华
网站建设 2026/9/18 5:14:22

Maven settings.xml 配置原理与企业私服实战指南

1. 为什么你写的 Maven 项目总在下载依赖时卡住&#xff1f;真相不是网速问题我第一次在客户现场部署一个 Spring Boot 项目时&#xff0c;整整等了 27 分钟——就为了下载spring-boot-starter-web-3.1.0.jar。开发环境 3 秒搞定&#xff0c;生产服务器却像卡在泥潭里。运维同事…

作者头像 李华
网站建设 2026/9/18 5:14:20

电信信用评级:机器学习模型优化与实践

1. 电信信用评级现状与挑战电信行业每天产生海量用户数据&#xff0c;但传统信用评估模型存在明显局限。我在运营商大数据部门工作六年&#xff0c;亲眼目睹了这些痛点&#xff1a;人工审核效率低下、规则引擎误判率高、新用户缺乏历史数据难以评估。最头疼的是&#xff0c;某些…

作者头像 李华