CC++ & Algorithm

唯一分解定理——每个合数都能拆成质数积

困难16
语言版本:C++Python
概述:唯一分解定理说每个大于1的整数都可以写成若干个质数的乘积,而且这种写法是唯一的(不考虑顺序)。

拆解数字的秘密——唯一分解定理(算术基本定理)

你有没有想过,每个数字就像是用一些“最小零件”拼起来的?比如数字 12,它可以写成 2 × 2 × 3,再比如 30 可以写成 2 × 3 × 5。而且你会发现,每个数字只能有一种拆法(顺序不算)。这就是唯一分解定理,也叫算术基本定理——它是数学里最重要的基石之一。

这个定理告诉我们:任何一个大于 1 的整数,要么本身就是质数,要么都能写成若干个质数相乘的形式,而且这种写法是唯一的(不考虑质数的顺序)。理解了这个定理,你就抓住了数字最底层的结构,以后学约分、求最大公约数、判断完全平方数都会变得很简单。


1. 拆数字就像拆乐高

每个合数(比如 12)都可以看成是用一些“质数积木”搭起来的。这些积木就是质数,比如 2、3、5、7……你只能用这些“最小零件”来拼出任何数字。
例如:

  • 12 = 2 × 2 × 3(两块 2 积木和一块 3 积木)
  • 18 = 2 × 3 × 3(一块 2,两块 3)
  • 100 = 2 × 2 × 5 × 5

这种拆法只有一种,不会出现另一种完全不同的分解。比如 12,你永远找不到第二组质数相乘得到 12(除了把 2 和 3 顺序调换,但顺序不算)。

生活例子:班主任要把 24 块糖果平均分给几个小组,每个小组人数必须相同,且每组都是质数个同学。那么可能的分法是:2 人一组(每组 12 块)、3 人一组(每组 8 块)、4 人一组(每组 6 块)……但注意 4 不是质数,所以不能是 4 人一组。真正的质因数分解告诉我们:24 = 2 × 2 × 2 × 3,所以只有用 2 和 3 这两种质数来分组才能做到完全平分。


2. 怎么手动分解一个数?

我们想找出一个数的所有质因子,就像拆乐高一样,从最小的质数开始试:

  1. 从质数 2 开始,看看它能不能整除这个数。
  2. 如果能,就除一次,记录一个 2,然后用商继续试 2,直到不能整除为止。
  3. 接着试下一个质数 3,再试 5,7,11……直到除到剩下 1 为止。

重要技巧:你只需要试到 √n 就行(这里 √ 表示开平方)。为什么呢?因为如果 n 有一个比 √n 大的质因子,那么它一定只有一个(另外的因子必然比 √n 小),并且它就是最后剩下的那个数本身。比如 17 是质数,它大于 √17,却只能有一个因子就是它自己。

例子:分解 84

  • 先用 2:84 ÷ 2 = 42,记下 2;42 ÷ 2 = 21,再记一个 2;21 不能被 2 整除。
  • 换 3:21 ÷ 3 = 7,记下 3;7 不能被 3 整除。
  • 换 5:7 不能被 5 整除(√7 ≈ 2.6,其实试到 2 就够了,因为 2 已经试过不行。严格来说试到 √7 即 2.6,但我们已经试过 2 了,下一个质数是 3,也试过不行,所以 7 是质数)。
  • 最后剩下 7,把它记下来。结果:84 = 2 × 2 × 3 × 7。

3. 基础代码实现:用循环试除

下面是 C++ 的基础版本,直接按照上面的思路写:

#include <iostream>
using namespace std;

int main() {
    int n;   // 待分解的数字
    cout << "请输入一个正整数:";
    cin >> n;
    cout << n << " = ";

    for (int i = 2; i * i <= n; i++) {   // i 从 2 开始试,只试到根号 n
        while (n % i == 0) {             // 只要能整除就一直除
            cout << i;                   // 输出这个质因子
            n /= i;                      // 更新 n 为商
            if (n > 1) cout << " * ";    // 如果还有剩下的因子,加乘号
        }
    }
    if (n > 1) cout << n;   // 最后剩下的质因子(可能等于 1 或一个大于根号 n 的质数)
    cout << endl;
    return 0;
}

运行示例
输入 84 → 输出 84 = 2 * 2 * 3 * 7
输入 17 → 输出 17 = 17(因为 17 是质数,循环不执行,最后直接输出 n)


4. 代码进阶:封装成函数,更安全

把分解功能单独写成函数,这样更容易在多个地方使用。注意每行变量定义都加上了中文注释。

#include <iostream>
#include <vector>   // 用向量存储因子
using namespace std;

// 函数:返回一个数的所有质因子(按从小到大顺序)
vector<int> factorize(int num) {
    vector<int> factors;      // 用来存放质因子的数组
    int n = num;              // 复制一份,避免修改原值
    for (int i = 2; i * i <= n; i++) {   // i 是当前试除的质数
        while (n % i == 0) {             // 如果能整除
            factors.push_back(i);        // 把 i 加入列表
            n /= i;                      // 除以 i
        }
    }
    if (n > 1) factors.push_back(n);     // 最后剩下的大于1的也是质因子
    return factors;
}

int main() {
    int number;   // 要分解的整数
    cout << "请输入一个正整数:";
    cin >> number;

    vector<int> result = factorize(number);   // 调用函数得到质因子列表
    cout << number << " = ";
    for (int i = 0; i < result.size(); i++) {
        cout << result[i];
        if (i != result.size() - 1) cout << " * ";   // 除了最后一个,后面都加乘号
    }
    cout << endl;
    return 0;
}

优点:函数返回一个 vector,你可以用它做更多操作,比如统计每个质因子出现的次数,或者判断是否是质数(如果结果只有自己本身)。


5. 新手容易犯的错误

  • 忘记处理最后的质因子:循环结束后,如果 n 还大于 1,它一定是质数(比如输入 17,循环一次都不执行,n 还是 17),必须输出它。很多初学者只输出循环内的因子,导致质数的情况只输出空。
  • 循环条件写错:写成 i <= n 会导致循环次数过多,效率极低,而且如果 n 本身是质数,循环会一直试到 n,浪费大量时间。写成 i * i <= n 才是正确的。
  • 忘记更新 n 的值:在 while 循环里要执行 n /= i,否则会无限循环(因为 n 一直不变,永远被 i 整除)。
  • 输出格式混乱:比如最后一个乘号多输出,或者没有乘号。上面的代码用 if (n > 1) cout << " * "; 来控制,稳妥。
  • 不理解试除范围:为什么只试到根号 n?因为如果 n 有因子 a 和 b(a ≤ b),那么 a ≤ √n,b ≥ √n。只要试到 √n,就能找到所有小因子,剩下的就是大因子(如果有,一定是质数)。

6. 生活中的例子

  • 分班游戏:学校要把 60 个学生平均分成几个小组,每组人数一样且是质数。你能有几种分法?用唯一分解定理:60 = 2 × 2 × 3 × 5,所以可能的每组人数是 2、3、5、2×2=4(但 4 不是质数!)、2×3=6(也不是质数)、2×5=10(不是)、3×5=15(不是)、2×2×3=12(不是)……等等。实际上只有质数本身(2,3,5)可以作为每组人数,因为只有质数才能保证组数也是整数且每组人数为质数。所以可以分成 2 人一组(30 组)、3 人一组(20 组)、5 人一组(12 组)。
  • 排队问题:学校有 72 名同学要排成方阵(每行人数相同且为质数),请问每行最少排几人?72 = 2×2×2×3×3,它的质因子只有 2 和 3,所以可能的每行人数是 2、3、2×2=4(不是质数)、2×3=6、2×2×2=8、3×3=9、2×2×3=12……只有 2 和 3 是质数,所以可以排 2 人一行(36 行)或 3 人一行(24 行)。最少的是 2 人一行。

7. 唯一分解定理有什么用?

这个定理是数论的基石,很多问题都离不开它:

  • 求最大公约数(GCD):比如求 12 和 18 的最大公约数。12=2²×3,18=2×3²,取每个质因子的最小指数:2¹×3¹ = 6。
  • 求最小公倍数(LCM):取每个质因子的最大指数:2²×3² = 36。
  • 判断完全平方数:一个数是完全平方数当且仅当它的每个质因子的指数都是偶数。比如 36 = 2²×3²,指数都是偶数,所以 36 是完全平方数;而 12 = 2²×3¹,3 的指数是奇数,所以不是。
  • 判断质数:如果一个数只有一种分解方式(即只有它本身),它就是质数。
  • 简化分数:约分就是去掉分子分母公共的质因子。

8. 总结与相关学习指引

唯一分解定理告诉我们:每个数字的内部结构是唯一确定的。掌握了分解方法,你就能理解数字的“DNA”。接下来可以学习:

  • 质数判断:如何快速判断一个数是不是质数(试除法配合根号 n)。
  • 筛法(埃氏筛、欧拉筛):一次性找出一个范围内的所有质数。
  • 最大公约数与最小公倍数:用质因数分解法或辗转相除法。
  • 完全平方数与完全立方数:用质因子指数判断。

试着用今天学到的知识,分解一下你的生日日期(比如 2009 年 5 月 15 日,数字 2009515),看看它有哪些质因子?动手写代码试一试吧!

例题精讲

1单选题

根据唯一分解定理,以下哪个数分解为质因数的结果与其他选项不同?(假设按从小到大顺序排列)

A60 = 2 × 2 × 3 × 5
B60 = 2 × 3 × 5 × 2
C60 = 2 × 5 × 2 × 3
D60 = 2 × 2 × 5 × 3
2单选题

以下哪个选项不是60的质因数分解?

A60 = 2 × 2 × 3 × 5
B60 = 2 × 3 × 2 × 5
C60 = 2 × 2 × 15
D60 = 2 × 5 × 2 × 3
3判断题

根据唯一分解定理,100的质因数分解结果可以写成2²×5²,也可以写成5²×2²,但写成2×5×2×5也是正确的。

4填空题
以下C++函数用于输出整数n(n≥2)的质因数分解结果(从小到大,重复因子重复输出)。例如输入12,输出"2 2 3"。请补全代码。

void factorize(int n) {
    for (int i = 2; i * i <= n; i++) {
        while (___1___) {
            cout << i << " ";
            ___2___;
        }
    }
    if (___3___) {
        cout << n;
    }
}
5填空题
以下C++函数用于计算整数n(n≥2)所有不同质因子的个数(例如12的不同质因子有2和3,返回2)。请补全代码。

int countPrimeFactors(int n) {
    int cnt = 0;
    for (int i = 2; i * i <= n; i++) {
        if (___1___) {
            cnt++;
            while (___2___) {
                ___3___;
            }
        }
    }
    if (n > 1) cnt++;
    return cnt;
}