分解质因数从试除法到素数表优化:C++题解与避坑指南

发布时间:2026/10/5 12:02:42

分解质因数从试除法到素数表优化:C++题解与避坑指南 东华OJ这道进阶题分解质因数我当年刷到的时候差点被名字唬住以为要上什么高深的数论算法结果静下心来拆解之后发现它考察的就是最基础的算术基本定理加一层循环嵌套的功力。C写这道题麻烦点不在数学而在循环边界和输出格式那些容易翻车的小细节。这篇文章完整梳理一遍我的解题思路、代码演变过程和踩过的坑不管你是刚刷OJ的新手还是在准备机试的考研党应该都能从中拿到点能直接用的东西。1. 题目到底在考什么分解质因数的数学本质与隐藏要求1.1 质因数分解与算术基本定理先把这个概念彻底说透。任何一个大于1的正整数都可以唯一地写成若干个质数的乘积而且不考虑顺序时写法是唯一的。比如60 2 × 2 × 3 × 5你会写成60 2^2 × 3^1 × 5^1但本质上还是2、3、5这三个质数撑起了60的全部构成。这个唯一性就是数学里的算术基本定理也是整道题的根。质因数分解之所以在算法题里频繁出现是因为很多数论性质——约数个数、约数之和、最大公约数、最小公倍数——全都可以建立在质因数分解的结果之上。比如说一个数n p1^a1 × p2^a2 × … × pk^ak那它的约数个数就是(a11)(a21)…(ak1)。这道题表面上只让你输出分解式实际上是在为后面一堆数论题打地基。OJ题目的特点就是表面看是一个动作实际考的是你把这个动作做到严谨的能力。分解质因数写起来不过十几行代码但要想不超时、不越界、格式完全正确每一行都需要反复斟酌。1.2 OJ进阶题的位置和考察逻辑东华OJ把这道题放在进阶题里并不是因为它算法难度大而是因为它综合了三个基本功循环控制、整除判断、格式化输出。很多新手写代码单独拿出for循环、if条件、printf输出都会写但组合在一起就出事——要么忘记处理最后剩下的质因子要么在输出星号时多打一个*要么循环终止条件写错导致死循环或者漏因子。这三个能力恰好是机试和笔试中最常被考察的基本盘。所以别看这道题小它背后的考察点非常典型是那种一道题能测出你有没有真正写过代码的题。还有一点进阶题通常还会考察多组数据输入的处理方式。这需要你提前看清楚题目要求是一次输入一次输出还是读到EOF为止不同的OJ风格不一样东华OJ很多题目是多组测试数据写在一个输入文件里的代码要设计成可以循环处理的逻辑。2. 基础解法试除法实现与逐行细节解析2.1 最朴素的试除框架解题思路不需要绕弯子核心就是从小到大试除。我从2开始一个一个质数去试如果当前数字能整除n就说明它是一个质因子把它输出然后用n除以这个因子继续试直到n变成1为止。这个思路很简单但实现时要分清整除和完全分解。举个例子处理数字12时2能整除12输出一个2n变成6此时2仍然能整除6还要再输出一次2n变成3然后2不能整除3了才轮到i3去试。也就是说对同一个因子要用while循环反复除直到除不动为止。很多人第一次写只写了一个if判断结果输出成122×6这种残缺形式这就是没有弄明白一个质因子可以出现多次。代码最直接的写法是这样的#include cstdio int main() { int n; scanf(%d, n); printf(%d, n); int temp n; bool first true; // 控制星号输出 for (int i 2; i * i temp; i) { while (temp % i 0) { if (!first) printf(*); printf(%d, i); first false; temp / i; } } if (temp 1) { if (!first) printf(*); printf(%d, temp); } printf(\n); return 0; }这段代码用了一个技巧i * i temp作为循环终止条件。为什么要这样写因为如果temp还存在一个大于sqrt(temp)的质因子那它一定是独自存在的不可能再配一个同样大于sqrt(temp)的因子否则乘积就超过temp本身了。所以循环结束后如果temp不等于1那它本身必定是一个质数直接输出即可。这步处理极端重要少了它输入17这样的质数时你的输出会变成17然后什么都没了。2.2 循环中变量变化的隐藏逻辑刚接触这道题的人最容易困惑的点在于i * i temp里面的temp是不断缩小的那这个条件会不会出问题我们实际推演一下n 72。初始temp 72。i2时4 72进入while输出2temp变36继续输出2temp变18再继续temp变9。此时224 9还成立但9%2不等于0所以while退出i变成3。然后339 9成立9%30输出3temp变3再试3%30输出3temp变1。此时339 1不成立循环终止。最后temp1不输出。最终结果722223*3完全正确。注意其中的关键点temp在while循环内被不断更新而for循环的终止条件每次都重新计算i * i temp所以当temp缩减到很小时即使i还不大循环也可能提前结束。比如72去掉因子2之后变成9i2时还能继续但i3时处理完9就变成1了循环立刻终止。这种动态边界恰恰是算法高效的关键它避免了无谓的试除。如果换成固定边界写成for (int i 2; i * i n; i)在n2×largePrime这种场景下就会出bug。举个具体例子n2×99991199982使用固定边界n/2时会一直试到447才停下来因为447²199809不超过199982然后在temp已经是99991的情况下后面一堆除数根本除不动纯浪费时间。使用动态temp边界则i只需要试到2就已经处理完了temp此时是99991大于1直接输出。这两种写法性能差距很大在数据量大时直接决定会不会超时。2.3 输出格式处理的两个细节输出格式是这类题目的隐性扣分点。很多人代码逻辑全对就因为多了一个*或者少了换行提交就是Wrong Answer。我使用的方案是维护一个bool first标志。第一次输出因子时不打星号之后每次输出因子前先打印一个*。这样的好处是不管因子出现几次代码都不需要预判这是不是最后一个因子逻辑清晰且不容易出错。还要注意题目要求的输出通常是np1*p2*...*pk的形式等号和乘号前后有没有空格要严格对照题目原文。东华OJ一般没有空格但有些OJ会有格式不符一样判错。所以拿到题第一步不是写代码而是把输出样例抄下来看清楚格式再动手。3. 算法复杂度账本为什么试除法在这道题足够优秀3.1 时间复杂度直观感知与worst case分析很多人一看试除法三个字就觉得这是新手村写法不够高级。但在分解质因数这个场景里试除法的真实复杂度并不是O(n)而是O(√n)量级。核心原因就是一条不需要把所有小于n的数都试一遍只需要试到√temp就能保证覆盖所有可能的质因子。因为如果一个合数n存在某个因子d那么d和n/d中必然有一个不超过√n。这个性质保证了搜索范围极短。但严格地说试除法的最坏情况还是有点表演空间的。假设输入n是一个接近10^9的大质数比如999999937那么i从2一直试到31622约√n才发现除不动等于白白做了三万多轮取模运算。单次运行没问题但如果OJ的测试数据里有一百组这样的大质数三百万次取模也不算轻松——不过说实话现代CPU跑这个量级依然是毫秒级别的事真正的淘汰风险更多来自每组数据都做了大量无谓试除的写法而动态边界的写法已经规避掉了大部分无谓计算。3.2 空间复杂度和实际运行表现这个算法是原地进行的只用了几个整型变量空间复杂度是O(1)哪怕输入的n撑到int极限也不会栈溢出或内存超限。这一点在OJ上非常友好几乎不可能因为内存问题被卡。我实测过一组数据连续分解100000以内所有整数的质因数用动态边界的试除法总耗时大约在50毫秒以内。这个表现说明在绝大多数入门和进阶题里试除法完全够用根本不需要上更复杂的优化方案。所以我的建议很明确先把试除法写熟练、写对再去琢磨优化不要一上来就追求花哨。当然也有场景是试除法压不住的。比如n达到了10^12以上的数量级或者有几百组大质数输入那3万多次循环×几百组就可能真有点慢了。这时候才需要考虑下一章的预处理素数表方案。4. 进阶优化预处理素数表到底值不值得4.1 埃氏筛原理与代码实现用素数表优化的思路是与其让i遍历2到√n的所有整数其中包括大量合数不如提前筛出所有质数只让质数去试除。因为任何一个合数因子它的质因子一定更小所以用质数表按顺序试除效果等价但省掉了对合数的取模判断。素数表的生成用埃氏筛就够了。原理简单说就是从小到大标记合数。从2开始2是质数然后把4、6、8……所有2的倍数全部标记为合数下一个未被标记的数是3它也是质数然后把6、9、12……全部标记以此类推。#include vector std::vectorint getPrimes(int limit) { std::vectorbool isComposite(limit 1, false); std::vectorint primes; for (int i 2; i limit; i) { if (!isComposite[i]) { primes.push_back(i); for (int j i * 2; j limit; j i) { isComposite[j] true; } } } return primes; }使用这个表去分解质因数时只需要循环for (int i 0; i primes.size() primes[i] * primes[i] temp; i)内部逻辑不变。对n999999937这种大质数原来要试31622次现在只需要试3401次质数3401是小于31622的质数个数性能提升接近10倍非常可观。4.2 何时该用素数表一个性价比判断但我也要说句实在话如果这道题只在东华OJ上跑数据量没有那么变态直接用试除法就能过。素数表真正的用武之地是遇到两种情况之一要么是单组数据但n极大且非常抗分解比如RSA那种几百位的大数要么是多组测试数据比如要分解1万个数每个都是10^8量级这时候重复计算质数表的成本被分摊了收益就非常明显。工程上有个经验法则如果测试数据组数超过100组且n的范围上限能被提前知道那预处理上限范围内所有质数是值得的。如果只是三五组数据直接试除反而更快因为筛法的O(nloglogn)预处理开销也不算小为了三四次分解去筛1000000以内的表反而有点浪费。所以这道题真正想考察的能力恰恰是你不仅要会写代码还要能判断够用就行和应该优化的边界。我见过很多人在这一题上强行上线性筛整得代码冗长最后运行效率也没比试除法快多少那就属于用力过猛。5. 踩坑实录OJ提交中的格式与隐藏边界问题5.1 scanf读取失败是隐患吗这道题最常见的一个陷阱是题目可能会给多组测试数据直到EOF结束。很多人误以为只输入一个数写死单次读取结果提交后Wrong Answer。东华OJ的题面通常会在输入描述里写清楚但我吃过的亏就是——做题太急没读题直接写代码。正确的多组写法很简单int n; while (scanf(%d, n) ! EOF) { // 处理一次分解 }如果你用的是C的cin对应写法是int n; while (cin n) { // 处理一次分解 }这里有一个细节值得提醒scanf的返回值是成功读取的变量个数。如果输入结束返回EOF也就是-1循环退出。cin n重载了bool转换操作符读取失败时返回false。两种方式面对多组输入都是安全的。但还有一些零基础教程会让你把EOF理解成文件末尾字符然后写if判断害人。借着这个机会把EOF的含义说清楚它是一个宏本质是-1代表输入流已经结束不是文件里的字符EOF。理解了这一层你就不会在EOF怎么比较这种事情上卡住。5.2 大数和溢出问题再看一个隐蔽的坑i * i temp这一步i和temp都是int类型如果temp接近2^31-1i在循环到大约46340时i*i就已经超过2^31了。好在C的int溢出是定义行为实际是回绕但结果不可靠一旦溢出i*i可能变成负数负数 temp 永远成立循环就不停往下跑直到i溢出又转回正数最终产生不可预料的超时或者死循环。所以如果数据范围没有明确说很小最稳妥的写法是把所有中间量声明为long longlong long n, temp;反正64位整数在现代OJ上几乎不增加运行开销用long long挡掉溢出风险属于白赚的保险。输入输出格式对应改成scanf(%lld, n)或printf(%lld, n)。更极端的情况是n接近0或者负数题目如果没保证n为正整数建议自己加工。负数没有质因数分解的正统意义通常我给出的兜底方案是先输出符号然后对绝对值做分解比如-12 -1 × 2 × 2 × 3。虽然这种处理在题目里大概用不到但写清楚总归严谨。5.3 1这个边界值怎么处理1既不是质数也不是合数它的质因数分解式在数学上定义是空分解。很多题目输入范围写的是正整数不会让你遇到1但万一呢如果你在循环里直接跑temp等于1for条件1*1 1成立初始i2时2*241不成立所以for循环根本不会进入然后temp1判断也不成立最终输出1缺少右侧部分。有的裁判会判格式错误有的会直接忽略这道边界。我的建议是提前把这个分支处理掉if (n 1) { printf(11\n); }这样无论题目数据里藏了什么输出都有明确的右侧值不会被卡格式。虽然实际测试数据大概率不会包含1但写代码的严谨性有时就体现在这种多余的判断里。6. 从这道题延伸出去质因数分解的典型应用与更广阔的算法方向6.1 约数个数、约数和与公约数的快速计算东华这道题拿下了你能立刻把它迁移到好几个经典题目上。第一个是约数个数问题给你n求n的所有正约数个数。如果对每个数都用枚举到√n去数约数那就是O(√n)的暴力但如果你先分解质因数得到 n 2^a × 3^b × 5^c那么约数个数就是 (a1)(b1)(c1)。加一的原因也很直观每个质因子的指数可以取0到a总共a1种组合起来就是总数。第二个是约数和n的所有约数之和等于 (2^02^1…2^a)(3^03^1…3^b)(5^0…5^c)也就是对每个质因子用等比数列求和再相乘。这两个结论在很多数论题里直接用不用反复枚举。第三个是最大公约数和最小公倍数。虽然通常用辗转相除法就够快了但如果你拿到一个数的质因数分解结果公约数问题也可以转化为每个质因子指数的min运算公倍数则对应max运算。这种底层互通的感觉是我觉得质因数分解值得反复刷熟的根本原因。6.2 大数分解的方向Miller-Rabin与Pollards Rho算法如果哪天你遇到的n从10^9涨到了10^18甚至是一个128位的大数试除法和素数表就都撑不住了。这时候经典的进阶方案是Pollards Rho算法配合Miller-Rabin素性测试一起使用。前者用伪随机序列在大概率下快速找到一个大数的一个非平凡因子后者在O(k log n)的复杂度内高概率判断一个数是不是质数。Pollards Rho的思想很有意思它构造一个数列利用生日悖论原理期望用 O(n^(1/4)) 的时间找到一个因子。这里的O(n^(1/4))听起来还是幂级数但对10^18的数来说就是10^4.5量级这在工程上是完全可接受的。不过这个话题展开的话篇幅就失控了。我在这里提它是想说明质因数分解这条技术路线是有纵深感的。你从试除法起步到预处理素数表优化再到Miller-Rabin和Pollards Rho一层层递进知识体系是连贯的。而这一切的起点就是东华OJ这一道进阶题里的十几行基础代码。6.3 这道题对新手还有一层心态训练价值最后从我的实际教学案例说一点体会。我带过的学生里不少人第一次写试除法分解质因数会卡在为什么最后一个temp要单独输出这个点上卡半小时。原因在于他们的思维还停留在在循环里把所有输出做完的定式上没有接受循环结束后剩余部分还需要收尾这种非对称的算法设计。一旦习惯了这种循环处理大部分收尾处理剩余的思维模式后面很多题都会顺畅很多。典型例子如快速排序的partition之后递归处理两侧还有二分查找最后对边界的单独判断本质上都带着类似的逻辑结构。所以每次有人问我这道进阶题到底进阶在哪我的答案都是进阶在让你从埋头写循环升级为跳出循环想清楚整个流程。我个人的建议是刷这道题时别急着一次写对先故意写个错误版本——比如漏掉最后的temp输出或者忘了用while只用了if——然后拿去跑样例观察哪里错了想清楚为什么错了。这个故意犯错-观察-修正的过程对巩固理解比直接抄一遍正确答案有效十倍。另外一个小技巧是写完代码后给自己设计几组特殊测试数据至少包含一个偶数如72、一个奇数合数如45、一个质数如17、一个完全平方数如100、一个大质数如99991。这五组数据跑通了这道题的正确性基本就有保证。这习惯放到所有OJ题上都通用——很多WA问题不是算法错是边界没测到。
延伸阅读

更多相关文章

2026/10/5 11:57:42

Linux进程生命周期:退出、收尸与exec替换

写代码这么多年,我一直觉得Linux下的进程生命周期是整个操作系统里反馈最明显、也最容易踩坑的一环。一个程序从被启动到运行结束,中间经历的退出方式、父进程如何采集退出状态、以及如何把子进程替换成另一个可执行文件,这三件事理解不清楚&…

2026/10/5 11:57:42

Grok Bot主动建议功能实战:从被动响应到智能协作者的设计与配置

1. 主动建议功能到底解决了什么痛点做聊天机器人这行的朋友应该都有体会,过去几年我们做的绝大多数对话系统,本质上都是“被动响应式”的——用户问一句,机器人答一句,用户不吭声,机器人就干等着。这种模式在客服场景里…

2026/10/5 11:57:42

对话机器人主动建议功能实战:触发策略、生成排序与落地排查

1. 从“你问我答”到“我猜你需要”:主动建议功能到底改变了什么 做对话机器人这行十来年,我见过太多产品卡在同一个瓶颈上:用户不开口,机器人就是个摆设。你问一句它答一句,你不问它就永远沉默,这种“被动…

2026/10/5 16:07:56

PLC四点多点同步顶升系统:高精度液压协同控制实战解析

1. 项目概述:这不是“抬东西”,而是一场毫米级的工业协同作战你见过几十吨重的桥梁节段,在空中稳如磐石地平移30米吗?你见过老旧厂房的整栋钢架结构,在不拆不卸的前提下,被整体抬升1.2米后精准落回新基础吗…

2026/10/5 16:07:56

跨端开发实战:RN for OpenHarmony构建动漫资讯页面

做 AnimeHub 这个项目之前,我已经在 React Native 上摸爬滚打两年多,大部分页面都是给 Android 和 iOS 做的。这次接到一个有点特殊的任务:用 RN for OpenHarmony 给动漫资讯应用 AnimeHub 开发“即将上映”页面。所谓即将上映,就…

2026/10/5 16:07:56

Vue.js开发环境搭建全攻略:从Node.js到Vite一步到位

写这篇教程之前我先交代个背景。我这些年带过不少前端新人,发现一个特别有意思的现象:很多人学Vue.js不是被API难倒的,而是倒在第一步——环境搭建。要么是Node版本不对,要么是npm镜像慢到崩溃,要么是脚手架装到一半报…

2026/10/5 16:07:56

Vue.js开发环境搭建指南:Node.js安装、Vite配置与项目创建全教程

很多朋友刚接触Vue.js时,最容易挫败的往往不是组件、路由这些语法,而是第一步:环境搭不起来。教程看了一遍又一遍,代码也照着敲了,结果一执行npm install满屏报错,或者node直接提示“不是内部或外部命令”。…

2026/10/5 16:07:56

SOFC单通道非绝热逆流模型COMSOL仿真:从几何到收敛全流程

做SOFC数值仿真的同行应该都体会过这种尴尬:文献里那些漂亮的温度分布云图,一到自己手里就变成满屏红色报错。最近完整跑了一遍COMSOL中单通道非绝热逆流固体氧化物燃料电池模型的搭建和求解,从几何处理到后处理出图踩了不少坑,这…

2026/10/5 16:02:56

Comsol水力压裂仿真:井眼应力场与多分支缝应力干扰分析

开头搞水力压裂仿真的人,几乎都绕不过一个灵魂拷问:井底破裂压力到底怎么预测、裂缝往哪个方向长、多分支缝之间怎么互相干扰。这些问题的核心,就是井眼应力场和多分支缝应力分布。这两个量搞不清楚,压裂设计基本就是盲人摸象——…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑