从质因数分解到勒让德定理:高效计算阶乘约数的算法精解
1. 问题背景与核心概念拆解“阶乘约数”这个问题乍一看像是纯粹的数学题很多初次接触算法竞赛的同学可能会直接掉进“暴力计算”的坑里。题目要求我们计算 100!100的阶乘的正约数个数。如果你试图用编程语言直接算出 100! 这个天文数字一个158位的整数然后再去循环找它的所有约数那你的程序大概率会直接超时甚至因为大数运算而内存溢出。这正是蓝桥杯这类竞赛题目设计的精妙之处——它考察的不是蛮力而是对数学原理的深刻理解和将其转化为高效算法的能力。这里涉及两个核心概念阶乘和正约数个数公式。阶乘大家都很熟悉n! 1 × 2 × 3 × ... × n。关键在于“正约数个数”。任何一个大于1的正整数 N都可以被唯一地分解为一系列质数的乘积即质因数分解N p1^a1 * p2^a2 * ... * pk^ak其中 p1, p2, ..., pk 是互不相同的质数a1, a2, ..., ak 是对应的指数正整数。那么N 的正约数个数 d(N) 有一个非常优美的公式d(N) (a1 1) * (a2 1) * ... * (ak 1)这个公式怎么理解呢我们可以把每个约数看作是从每个质因子的幂次中做一个选择。对于质因子 p1它在约数中出现的次数可以是 0, 1, 2, ..., a1一共 (a1 1) 种选择。各个质因子的选择是独立的根据乘法原理总的约数个数就是所有 (指数 1) 的乘积。所以原问题“求 100! 的约数个数”就转化为了先对 100! 进行质因数分解得到所有质因子及其对应的指数然后将每个指数加一后连乘。2. 从暴力到优化质因数分解的策略演进既然核心是质因数分解那我们如何得到 100! 的质因数分解形式呢100! 是 1 到 100 这100个连续整数的乘积。一个直接的思路是分别对 2, 3, 4, ..., 100 这每一个数进行质因数分解然后把所有分解结果中相同质因子的指数累加起来。2.1 基础实现对每个数单独分解我们先来实现这个最直观的方法并分析其效率。对于一个整数 num我们可以用试除法来分解它从最小的质数 2 开始判断 num 是否能被它整除。如果能则一直除下去并记录除的次数即指数直到 num 不再能被该质数整除。然后尝试下一个质数3, 5, 7...直到 num 被除到 1 为止。用 Python 实现一个对单个数的分解函数def prime_factorization(num): 返回一个字典键为质因子值为对应的指数 factors {} # 处理因子2 while num % 2 0: factors[2] factors.get(2, 0) 1 num // 2 # 处理奇数因子从3开始步长为2 p 3 while p * p num: while num % p 0: factors[p] factors.get(p, 0) 1 num // p p 2 # 如果最后剩下的数大于1它本身就是一个质数 if num 1: factors[num] factors.get(num, 0) 1 return factors然后我们遍历 2 到 100对每个数调用这个函数并将结果累加到一个总字典中def factorial_divisor_count_naive(n): total_factors {} for i in range(2, n 1): factors prime_factorization(i) for prime, exp in factors.items(): total_factors[prime] total_factors.get(prime, 0) exp # 计算约数个数 result 1 for exp in total_factors.values(): result * (exp 1) return result print(factorial_divisor_count_naive(100))这个方法可以得到正确答案对于 n100 来说计算速度也很快。但是我们思考一下它的时间复杂度。外层循环 O(n)内层分解函数对于每个数 i最坏情况需要试除到 sqrt(i)所以总复杂度大约在 O(n * sqrt(n)) 量级。当 n 增大到 10^5 甚至 10^6 时这个算法就会变得非常慢。2.2 优化思路利用阶乘的连续性我们真的需要对每个数都做一次完整的质因数分解吗仔细分析 100! 的构成我们发现可以利用阶乘的连续性来设计一个更高效的算法。核心观察是要得到 100! 中质因子 p 的指数我们只需要计算在 1 到 100 中有多少个数是 p 的倍数、p^2 的倍数、p^3 的倍数……然后将这些计数相加即可。为什么因为一个数如果是 p^k 的倍数那么它在阶乘乘积中就会为质因子 p “贡献” k 次。但是一个同时是 p^2 倍数的数在 p 的倍数里被算了一次在 p^2 的倍数里又被算了一次总共贡献了2次这正是我们需要的。具体公式勒让德定理为在 n! 中质因子 p 的指数等于[n/p] [n/p^2] [n/p^3] ...其中[x]表示对 x 向下取整。这个求和一直进行到 p^k n 为止因为此时[n/p^k] 0。例如计算 100! 中质因子 5 的指数[100/5] 20(5, 10, 15, ..., 100 这20个数每个至少贡献一个5)[100/25] 4(25, 50, 75, 100 这4个数每个在上一轮基础上再额外贡献一个5)[100/125] 0(125 100停止) 所以质因子 5 的总指数是 20 4 24。这个算法的优势在于无需分解每个合数我们只关心所有可能出现在 100 以内的质数。计算高效对于每个质数 p我们只需要进行大约 log_p(n) 次除法运算。100 以内的质数只有25个所以总计算量极小。3. 高效算法实现与逐步解析基于勒让德定理我们可以设计出一个非常简洁且高效的算法。整个流程可以分为三步筛选质数、计算每个质数的指数、应用约数个数公式。3.1 第一步筛选出 100 以内的所有质数因为 100! 的质因子只可能来自 100 以内的质数如果一个质数大于100它不可能整除 1到100 中任何一个数除了它自身但100!不包含大于100的质数。我们可以用经典的埃拉托斯特尼筛法来快速得到这个质数列表。def sieve_of_eratosthenes(limit): 返回一个列表包含所有小于等于limit的质数 is_prime [True] * (limit 1) is_prime[0:2] [False, False] # 0和1不是质数 for i in range(2, int(limit**0.5) 1): if is_prime[i]: # 将i的倍数标记为非质数 for j in range(i * i, limit 1, i): is_prime[j] False primes [i for i, flag in enumerate(is_prime) if flag] return primes limit 100 primes sieve_of_eratosthenes(limit) print(f100以内的质数有 {len(primes)} 个: {primes})3.2 第二步对每个质数应用勒让德定理对于筛选出来的每一个质数 p我们循环计算n // p,n // (p*p),n // (p*p*p)... 直到商为0并将这些商累加得到 p 在 n! 中的指数。def exponent_in_factorial(n, p): 计算质数p在n!中的指数 exp 0 power p while n // power 0: exp n // power power * p # 计算 p, p^2, p^3, ... return exp3.3 第三步整合计算并得出结果现在我们将所有步骤整合起来计算 100! 的约数个数。def count_divisors_of_factorial(n): 计算n!的正约数个数 # 1. 获取所有小于等于n的质数 primes sieve_of_eratosthenes(n) # 2. 计算总约数个数 total_divisors 1 for p in primes: exp exponent_in_factorial(n, p) total_divisors * (exp 1) return total_divisors result count_divisors_of_factorial(100) print(f100! 的正约数个数是: {result})运行这段代码我们可以立刻得到结果。这个算法的时间复杂度主要取决于筛法 O(n log log n) 和后续对每个质数的指数计算 O(π(n) * log n)其中 π(n) 是小于 n 的质数个数。对于 n100 乃至 n10^6这个算法都能在极短时间内完成。4. 深入验证与手工推算演示为了确保我们完全理解了这个过程也为了在竞赛中万一无法编程时能手动估算或验证我们来进行一次小规模的手工计算比如计算10! 的约数个数。首先列出 10 以内的质数2, 3, 5, 7。 然后对每个质数计算其在 10! 中的指数对于质数 2:[10/2]5,[10/4]2,[10/8]1指数 521 8对于质数 3:[10/3]3,[10/9]1指数 31 4对于质数 5:[10/5]2指数 2对于质数 7:[10/7]1指数 1所以10! 2^8 * 3^4 * 5^2 * 7^1。 根据约数个数公式d(10!) (81) * (41) * (21) * (11) 9 * 5 * 3 * 2 270。我们可以用一小段 Python 代码来验证这个结果import math # 直接计算10!并求其约数个数仅用于小数字验证 def brute_force_divisor_count_of_factorial(n): factorial math.factorial(n) count 0 # 只需要遍历到平方根即可 for i in range(1, int(math.sqrt(factorial)) 1): if factorial % i 0: count 1 if i ! factorial // i: # 避免重复计数平方根 count 1 return count print(f暴力验证 10! 的约数个数: {brute_force_divisor_count_of_factorial(10)}) print(f公式计算 10! 的约数个数: {count_divisors_of_factorial(10)})运行后两者都会输出 270验证了我们公式和算法的正确性。对于 100!暴力验证已不可能但我们算法的正确性在数学上是严格保证的。5. 算法扩展与竞赛中的变体思考掌握了“阶乘约数”的核心解法后我们可以思考一些相关的扩展问题这在蓝桥杯等竞赛中很可能以变体形式出现。5.1 变体一计算 n! 末尾有多少个零这是一个非常经典且相关的问题。n! 末尾零的个数取决于其质因数分解中因子 10 的个数而 10 2 × 5。由于在阶乘中因子 2 的个数远多于因子 5 的个数因为偶数比5的倍数多得多所以末尾零的个数就等于质因子 5 的指数。这正好是我们上面算法的一个直接应用exponent_in_factorial(n, 5)。5.2 变体二计算组合数 C(n, m) 的约数个数组合数 C(n, m) n! / (m! * (n-m)!)。我们可以分别计算出 n!, m!, (n-m)! 的质因数分解形式用我们上面的算法然后将 n! 的每个质因子指数减去 m! 和 (n-m)! 的对应指数就得到了 C(n, m) 的质因数分解。最后再套用约数个数公式即可。5.3 变体三n 非常大时如 10^9的估算与性质如果 n 大到无法进行筛法比如 10^9但问题可能只问某个特定质因子的指数或者约数个数的奇偶性等性质。这时勒让德定理依然有效我们只需要对特定的 p 进行计算即可。对于约数个数的奇偶性有一个重要结论一个数的约数个数是奇数当且仅当这个数是完全平方数。因为约数总是成对出现的d 和 N/d除非 d N/d即 N d^2。那么 n! 是不是完全平方数呢显然不是因为根据质因数分解只要有一个质因子的指数是奇数它就不是完全平方数。而在 n! 中质因子 2 的指数[n/2] [n/4] ...在 n1 时总是大于其他质因子的指数并且很容易证明其指数是奇数还是偶数。6. 实战编码技巧与常见“坑点”在竞赛中实现这个算法时有几个细节需要注意它们决定了你的代码是优雅高效还是冗长易错。技巧一质数筛的优化对于本题 n100简单的筛法足够。但如果 n 更大比如 10^6建议使用更高效的线性筛欧拉筛其时间复杂度是严格的 O(n)并且能同时得到每个数的最小质因子这在需要频繁分解质因数时非常有用。技巧二指数计算的循环终止条件在exponent_in_factorial函数中循环条件while n // power 0是正确且简洁的。有同学可能会写while power n这在 power 可能溢出比如对于大质数power 快速倍增后超出整数范围时就不安全了。而用除法判断则避免了溢出风险。技巧三数据类型与溢出问题计算约数个数时是多个(指数1)的连乘。对于 100!这个结果是一个巨大的整数。在 Python 中大整数是原生支持的所以没问题。但在 C 或 Java 中需要使用long long甚至高精度类型来存储最终结果否则可能在连乘过程中发生溢出导致答案错误。一个我踩过的“坑”在一次练习中我写了一个“优化”版本试图一次性为所有质数计算指数# 有问题的“优化”代码片段 exponents [0] * (n1) for i in range(2, n1): temp i for p in primes: while temp % p 0: exponents[p] 1 temp // p if temp 1: break这个代码看起来是遍历每个数并直接用质数表去分解它。但它的效率反而比勒让德定理的方法低很多因为内层循环对于每个数 i 都要遍历质数表直到找到能整除的质数。这本质上又退化成了对每个数进行质因数分解的原始方法而且多了很多不必要的模运算判断。这个“坑”告诉我数学上的优化勒让德定理远比在代码细节上小修小补带来的提升要大得多。正确的优化方向应该是选择更优的数学原理而不是在低效的算法框架内进行微调。7. 总结与举一反三回顾整个解题过程我们从最暴力的想法出发通过分析问题本质将其转化为“求质因数分解”的数学问题。接着我们发现了直接分解每个数的低效进而引入了数论中的勒让德定理实现了从 O(n√n) 到 O(n log log n) 的效率飞跃。最后我们给出了清晰的三步实现代码筛质数、算指数、乘结果。这个问题的价值远不止于得到一个数字答案。它提供了一个经典的算法优化范式当面对一个与大数阶乘或组合数相关的问题时直接计算数值往往是死路转而分析其质因数分解形式并利用数论定理勒让德定理来统计指数才是通用的高效解法。在真正的竞赛或面试中面试官可能不会直接问“100!的约数个数”而是问“100!末尾有多少个零”或“C(100, 50)的约数个数”。只要你掌握了“阶乘约数”这个母问题的核心——勒让德定理和约数个数公式这些变体问题都能迎刃而解。我的建议是不仅要会写代码更要理解其背后的数学原理并尝试手动推导小规模案例这样才能在遇到新问题时做到举一反三灵活应用。

相关新闻

iOS工程师必备技能:从UIStackView到证书上架的工程化实践

iOS工程师必备技能:从UIStackView到证书上架的工程化实践

朋友给我发来一份《iOS工程师综合练习卷(二星级)》,说是给刚走上iOS开发这条路的新人摸底用的,让我帮忙看看值不值得做。我把整份卷子从头到尾过了一遍,又把相关的考点、周边工具链、常见的坑全部捋了捋,发…

2026/10/25 11:19:35 阅读更多 →
高速公路抛洒识别 道路路面抛洒物散落货物数据集 高速障碍 yolo数据集 物检测数据集

高速公路抛洒识别 道路路面抛洒物散落货物数据集 高速障碍 yolo数据集 物检测数据集

数据集核心信息 数据集名称:智慧交通高速公路城市道路路面抛洒物散落货物障碍物检测数据集数据规模:共 26127 张图像 标注格式:Pascal VOC 格式 YOLO 格式 标注类别数:1 类 类别名称:抛洒) 核心用途&#…

2026/10/25 15:22:28 阅读更多 →
中级安卓应用开发

中级安卓应用开发

核心28题1. Activity 生命周期 & 配置变更:切后台再回来走哪些回调?旋转屏幕 ViewModel 为什么不丢数据?切到后天应用生命周期onPause()->onStop(),若被系统回收还会onSaveInstanceState()回到应用则会从onRestart()->onStart()->onResume()&#xf…

2026/10/24 5:40:57 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/6 15:21:35 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/6 9:29:27 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/6 22:01:11 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/6 22:01:11 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/6 8:08:42 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/6 22:01:09 阅读更多 →

日新闻

周新闻

月新闻