多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

质因数分解与约数个数公式:从算术基本定理到算法实现

质因数分解与约数个数公式:从算术基本定理到算法实现 1. 项目概述从一道模板题看约数问题的核心解法在算法竞赛和日常编程中处理整数的约数因数是一个高频出现的基础问题。无论是判断质数、分解质因数还是解决更复杂的数论问题都离不开对约数性质的深入理解和高效计算。AcWing 870题“约数个数”正是这样一个经典的“模板题”。它不要求你输出所有约数而是要求计算一个数的约数总个数。这看似简单但直接遍历1到N判断能否整除的暴力方法其时间复杂度为O(N)当N很大时例如10^9甚至10^12是完全不可行的。这道题的精髓在于它引导我们利用算术基本定理将求约数个数的问题转化为一个基于质因数分解的公式计算问题从而将复杂度降低到O(√N)。掌握这个模板就意味着你掌握了解决一大类约数相关问题的钥匙。本文将深入拆解这道题的数学原理、C实现细节、常见变形以及在实际编码中的避坑技巧让你不仅会做这道题更能透彻理解其背后的思想并应用到更广泛的场景中。2. 核心思路与数学原理拆解2.1 为什么暴力枚举行不通面对“求N的约数个数”这个问题最直观的想法是从1循环到N逐个判断是否能整除N。对于较小的N比如小于10^6这种方法勉强可用。但在算法题常见的约束下N的范围动辄达到10^9循环10亿次显然会超时Time Limit Exceeded。更极端的情况如果N接近10^12暴力枚举更是遥不可及。因此我们必须寻找更优的数学方法。2.2 算术基本定理与约数个数公式这里就需要请出数论中的基石——算术基本定理。该定理指出任何一个大于1的自然数N都可以唯一地分解成有限个质数的乘积。 即N p1^α1 * p2^α2 * ... * pk^αk其中p1, p2, ..., pk 是互不相同的质数α1, α2, ..., αk 是正整数。这个分解式是理解约数个数的关键。N的任何一个约数d必然是由N的这些质因子以不大于其指数的幂次组合而成。 例如假设N 2^3 * 3^2 72。那么它的一个约数d 2^1 * 3^2 18。可以看到d中质因子2的指数1取自原数N中指数3的可能取值0, 1, 2, 3质因子3的指数2取自原数N中指数2的可能取值0, 1, 2。由此我们可以推导出约数个数的计算公式对于N p1^α1 * p2^α2 * ... * pk^αk它的约数总个数为f(N) (α1 1) * (α2 1) * ... * (αk 1)公式解释对于每一个质因子pi在构成约数时它的指数可以有(αi 1)种选择即0, 1, 2, ..., αi。所有质因子指数的选择相互独立根据乘法原理总的组合数就是各个(αi 1)的乘积。以N72为例72 2^3 * 3^2约数个数 (3 1) * (2 1) 4 * 3 12。 我们可以列举验证72的约数有 1, 2, 3, 4, 6, 8, 9, 12, 18, 24, 36, 72正好12个。2.3 算法流程设计基于以上公式我们的算法流程变得清晰质因数分解对给定的整数N进行质因数分解得到所有质因子pi及其对应的指数αi。应用公式计算遍历分解结果将所有(αi 1)相乘得到的积就是约数的总个数。问题的核心和难点转移到了如何高效地对一个整数进行质因数分解。3. 核心细节解析与C实现要点3.1 高效的质因数分解方法质因数分解最常用的方法是试除法。但并非从2遍历到N而是利用一个关键性质如果N是一个合数那么它必然有一个不大于√N的质因子。因此我们只需要用i从2遍历到√N即i * i N。每当N % i 0说明i是N的一个质因子为什么此时i一定是质数因为我们在循环中从小到大尝试如果i是合数它早就会被它的质因子整除从而在轮到它之前N中对应的质因子已经被除干净了。接着我们用一个循环while (N % i 0)来除尽N中所有的i因子并统计次数s。将质因子i和指数s记录下来。循环结束后如果剩下的N 1那么此时的N本身就是一个大于√N的质因子其指数为1。这个算法的时间复杂度是O(√N)比暴力枚举的O(N)快了几个数量级。注意在代码中i需要声明为long long类型因为当N很大时i * i可能会超出int的范围导致溢出。这是一个非常容易忽略的细节。3.2 数据结构选择如何存储质因数分解结果我们需要存储每个质因子及其对应的指数。在C中有两种常见选择vectorpairint, int清晰直观pair的first存质因子second存指数。unordered_mapint, int利用哈希表键(Key)为质因子值(Value)为指数。它的好处是如果后续需要对多个数进行质因数分解并合并例如求多个数乘积的约数个数使用map合并指数会非常方便。对于本题单个数两者皆可。为了清晰展示原理我们先用vectorpairint, int。3.3 代码实现与逐行解析以下是AcWing 870题的核心解法实现#include iostream #include vector #include unordered_map using namespace std; int main() { int n; cin n; unordered_mapint, int primes; // 使用哈希表存储质因子和指数 while (n -- ) { int x; cin x; // 质因数分解 x for (int i 2; i x / i; i ) { // 试除法遍历到 sqrt(x) while (x % i 0) { x / i; primes[i] ; // 对应质因子指数加1 } } if (x 1) primes[x] ; // 处理剩余的大于 sqrt(x) 的质因子 } long long res 1; // 结果可能很大用 long long 存储 const int MOD 1e9 7; // 题目要求的取模值 for (auto prime : primes) { res res * (prime.second 1) % MOD; // 应用公式(α_i 1) 连乘 } cout res endl; return 0; }代码关键点解析for (int i 2; i x / i; i )这是试除法的核心循环条件。用i x / i代替i * i x是为了防止i * i溢出尽管本题x是int但这是一个好习惯。while (x % i 0)这个内层循环用于除尽当前质因子i并同时在primes[i]中累加指数。if (x 1) primes[x] ;循环结束后如果x大于1那么它一定是原始x的一个大于其平方根的质因子且指数为1。long long res 1;最终结果是指数加一的连乘积这个数可能非常大必须用long long类型存储并在计算过程中及时取模防止溢出。res res * (prime.second 1) % MOD;遍历哈希表应用约数个数公式并同时取模。4. 模板的扩展与常见问题剖析4.1 模板的通用性上述代码不仅仅能解决“求一个数约数个数”的问题它是一个质因数分解公式应用的通用框架。稍作修改可以解决一系列衍生问题求约数之和约数之和公式为S(N) (p1^0 p1^1 ... p1^α1) * ... * (pk^0 pk^1 ... pk^αk)。在分解质因数后计算每个括号内的等比数列和可用快速幂加速再连乘即可。求多个数乘积的约数个数/和这是AcWing 871题的内容。只需对每个输入的数分别进行质因数分解并将所有分解结果合并到同一个哈希表里指数相加然后再对合并后的质因数结果套用公式。判断约数个数是否为奇数约数个数为奇数意味着公式(α11)*...*(αk1)为奇数即每个(αi1)都是奇数所以每个αi必须是偶数。这意味着原数N必须是一个完全平方数。这是一个非常有用的小结论。4.2 常见“坑点”与调试技巧溢出问题中间计算溢出在试除法循环中使用i * i n判断时如果i是inti*i可能溢出。更安全的写法是i n / i。结果溢出约数个数可能非常大即使对1e97取模连乘过程中也可能溢出int。务必使用long long类型存储中间结果res并在每次乘法后立即取模。时间复杂度误判试除法复杂度是 O(√N)对于单个10^9级别的数是很快的。但如果题目要求处理10^5个这样的数总复杂度就是10^5 * √(10^9) ≈ 10^8可能处于超时的边缘。这时需要考虑更优的预处理如线性筛法或数学优化。特殊输入的处理N 11的质因数分解是空的根据公式空乘积定义为1所以1的约数个数是1约数只有它本身。我们的算法中primes为空res初始为1输出正确。大质数输入例如N 10^97本身是个质数。算法会从2遍历到√N都没有找到因子最后x 1分支会将其记录为质因子指数为1。结果(11)2正确。unordered_map与map的选择unordered_map基于哈希表平均查找插入是O(1)map基于红黑树是O(log n)。在算法竞赛中对于本题规模两者差异不大。但unordered_map的哈希函数对于整数类型效率很高通常更快。需要注意的是遍历unordered_map时键值对的顺序是不确定的本题不影响结果。5. 从模板题到实战算法思想的应用与变种5.1 算法思想的核心质因数分解的桥梁作用这道题教会我们的核心思想是许多关于整数的全局性质问题如约数个数、约数和、欧拉函数等可以通过质因数分解这座“桥梁”转化为对局部质因子指数的独立计算问题。分解后问题往往变得简单且可组合。5.2 实战变种题举例AcWing 872. 最大公约数求多个数的最大公约数可以直接使用欧几里得算法。但理解其质因数分解的视角也很有益最大公约数就是所有数公共质因子的最低次幂的乘积。LeetCode 319. Bulb Switcher灯泡开关第i轮切换所有i的倍数的开关。最终亮着的灯是其编号约数个数为奇数的灯。根据我们4.1的结论这些编号是完全平方数。所以答案就是sqrt(n)。这道题完美地将约数个数奇偶性结论应用于实际场景。判断一个数是否只有质因子2、3、5丑数问题可以不断用2、3、5去除看最后是否剩下1。这本质上是特定质因数分解的判定。5.3 性能优化进阶预处理质数表当需要频繁地对多个数进行质因数分解时每次都从2开始试除会重复判断很多合数。一个优化策略是先用线性筛法欧拉筛预处理出一定范围内比如sqrt(最大值)的所有质数存放在一个数组中。在分解时只用这些预处理的质数去试除。 这样做的好处是试除的次数等于N的质因子个数加上小于√N的质数个数比原始的O(√N)常数更小。当处理批量数据时优势明显。// 线性筛法求质数表 primes[] vectorint get_primes(int n) { vectorint primes; vectorbool st(n 1, false); for (int i 2; i n; i) { if (!st[i]) primes.push_back(i); for (int j 0; primes[j] n / i; j) { st[primes[j] * i] true; if (i % primes[j] 0) break; } } return primes; } // 使用质数表进行质因数分解 unordered_mapint, int factorize(int x, const vectorint primes) { unordered_mapint, int res; for (int p : primes) { if (p x / p) break; // 质数已经大于 sqrt(x)退出 if (x % p 0) { while (x % p 0) { x / p; res[p]; } } } if (x 1) res[x]; return res; }掌握约数个数的模板其意义远超解出一道题。它代表了你对数论基础工具——质因数分解的熟练运用以及将复杂问题转化为可计算模型的思维能力。在C实现中注意数据类型的选取防止溢出理解循环边界条件的写法并善用STL容器来组织数据这些细节共同决定了代码的鲁棒性和效率。下次遇到约数相关的问题不妨先想想能不能先分解质因数
返回列表