CC++ & Algorithm

试除法分解质因数

困难2
语言版本:通用
概述:从最小的质数开始依次试除,把合数拆成质因子的乘积,就像用试钥匙开锁一样简单直接。

试除法分解质因数——用最笨的方法解决最根本的问题

什么是质因数分解?为什么要学它?

质因数分解就是把一个合数(大于1且不是质数的数)拆成若干个质数相乘的形式。比如 12 = 2 × 2 × 330 = 2 × 3 × 5。这在数学中非常有用,比如找最大公约数、最小公倍数、加密算法等。对于中小学生来说,就像把一堆硬币(合数)整理成一摞一摞等面额的硬币(质数),数起来更清楚。

试除法(Trial Division)是最直观、最容易理解的分解方法。就像拿着一串钥匙(质数2、3、5、7……)一把一把去试,看哪把能打开锁(整除这个数)。虽然它有点“笨”,但特别适合我们学习编程和数学的入门阶段。


一、生活中的比喻:用钥匙一把一把试开门(保留原有)

想象你面前有很多把钥匙(数字2,3,5,7,...),你要打开一把锁(分解一个合数)。最简单的方法就是一把钥匙一把钥匙地试:先用钥匙2,看能不能转动锁芯(整除);如果能,记录这把钥匙,然后重复;不能就换下一把。这就是试除法(Trial Division)的基本思想。它简单、可靠,但缺点是当锁越来越复杂(数字很大)时,钥匙的数量也会爆炸式增长。

再举一个例子:假设你有120颗糖果,要分给同学们,每人分到相同数量的“质数颗”,并且刚好分完。你可以先试试每人2颗(120÷2=60,可以),记录下来;剩下60颗,再试试2颗(60÷2=30,可以);继续……直到最后剩下一个质数。这个过程就是试除法。


二、数学原理:如何优化“试”的效率?——一步步变聪明

朴素试除法(最笨的方法)

对于一个整数n,从2到n-1依次试除,看哪些能整除。如果n是质数,我们可能要检查n-2次。比如n=13,要试2,3,4,...,12,共11次,效率极低!但这种方法很直观,适合理解。

优化1:只试除到√n —— 因为因子是成对出现的

为什么?
如果n有因子d,那么必然存在另一个因子n/d。如果d > √n,那么n/d < √n,所以较小的因子一定在√n以内。因此,只需要检查2到√n即可。

例子:分解36。√36=6,我们试除2(可以,得18),3(可以,得6),4(但2已经处理过了,4不是质数),5(不能),6(但6是合数,实际不用试)。实际上我们只需要试到6,就能发现所有因子。如果不优化,要试到35,太累。

更具体的例子:试判断97是不是质数。√97≈9.8,只需试除2,3,5,7,发现都不能整除,就知道97是质数。如果不优化,要试到96。

优化2:跳过偶数(除了2)—— 2是唯一偶质数

所有大于2的偶数都不是质数,所以试除时可以先处理2,然后只试除奇数(3,5,7,9...)。注意,9虽然是合数,但如果我们已经在试除3时把因子3都除掉了,那么9就不会再出现。所以试除合数也无所谓,但会导致一些无效尝试(比如试9,其实9已经不可能了),不过比试除所有偶数好多了。

例子:分解84。先处理2:84÷2=42,42÷2=21,记录两个2。然后从3开始试奇数:21÷3=7,记录3;然后试5(不能),试7(7÷7=1),记录7。完成。只试了3个奇数(3,5,7),而如果试所有数,要试2,3,4,5,6,7,多了不少。

优化3:使用质数表——只试真正的质数

如果事先知道所有小于√n的质数,那么只试除这些质数,不会浪费时间去试除合数。比如我们有一个质数表[2,3,5,7,11,13,...],只用这些去试。但是生成质数表本身需要额外时间(可以用后面的筛法)。对于大数,通常只使用优化2就足够了,因为试除合数的开销相对较小。

算法步骤(清晰版)

  1. 输入n(必须大于1)。
  2. 处理因子2:当n能被2整除时,记录2,并把n除以2,重复直到n为奇数。
  3. 令i = 3,循环直到 i × i > n(改用 i ≤ n / i 避免溢出):
    • 当n能被i整除时,记录i,并把n除以i,重复。
    • 否则 i = i + 2(只试奇数)。
  4. 如果最后n > 1,那么n本身就是一个质因子,记录下来。
  5. 返回所有记录的因子列表。

时间复杂度:最坏情况下(当n是质数时)需要检查大约√n/2个奇数,所以时间复杂度为O(√n)。对于n=10^12,√n=10^6,检查50万个奇数,在计算机里是很快的(毫秒级)。但对于更大的数(比如10^18),√n=10^9,就不适合了。


三、编程实现注意事项(新手容易犯的错误)

? 常见错误1:忘记处理最后剩下的n

如果循环结束后n > 1,表明最后一个因子是质数,必须记录。比如n=17,循环内没找到因子,n还是17,最后要记录17。很多新手会漏掉这一步,导致结果不完整。

? 常见错误2:循环条件写成 i*i <= n 导致整数溢出

在C++中,如果n是long long(最大9e18),i本身可能很大,i*i会超出long long范围,变成负数或溢出。用 i <= n / i 比较安全,因为除法不会溢出。

? 常见错误3:在循环中修改循环变量i导致死循环或漏检

比如在while循环内改变n的值是正常的,但是要注意不要在for循环的循环变量中不小心修改i。一般使用while或for配合正确条件。

? 常见错误4:输入1或负数没有处理

质因数分解只对大于1的整数有意义。输入1应该报错,输入负数可以先取绝对值(但负数通常不考虑质因数,因为负号不计入)。最好在代码开头检查。

? 常见错误5:认为试除法对所有数都很快

试除法适合小数字(比如10^9以内),但大数字会非常慢。例如分解1000000007(约10^9的质数),需要试到√1000000007≈31623,试1.5万个奇数,虽然还行,但如果数字再大10倍,试除量就增加√10≈3.16倍,很快变慢。


四、完整代码示例(添加详细中文注释)

C++ 实现(优化版,支持long long)

#include <iostream>
#include <vector>
using namespace std;

typedef long long ll; // 给long long起个别名,方便书写

// 试除法质因数分解,返回质因子列表(可重复)
vector<ll> trial_division(ll n) {
    vector<ll> factors; // 存储质因子的列表
    // 处理因子2
    while (n % 2 == 0) {
        factors.push_back(2); // 记录因子2
        n /= 2;               // 把2除掉
    }
    // 试除奇数,从3开始
    // 注意:用 i <= n / i 避免 i*i 溢出
    for (ll i = 3; i <= n / i; i += 2) {
        while (n % i == 0) {
            factors.push_back(i); // 记录因子i
            n /= i;               // 把i除掉
        }
    }
    // 如果n>1,则剩下的n是质因子(比如n本身是质数)
    if (n > 1) {
        factors.push_back(n);
    }
    return factors;
}

int main() {
    ll num;
    cout << "请输入一个大于1的整数(支持到long long范围): ";
    cin >> num;
    if (num <= 1) {
        cout << "输入必须大于1" << endl;
        return 0;
    }
    
    vector<ll> result = trial_division(num);
    cout << num << " 的质因数分解: ";
    for (size_t i = 0; i < result.size(); i++) {
        cout << result[i];
        if (i != result.size() - 1) cout << " * ";
    }
    cout << endl;
    
    // 验证乘积
    ll product = 1;
    for (ll f : result) product *= f;
    cout << "验证: " << product << (product == num ? " (正确)" : " (错误)") << endl;
    
    return 0;
}

Python 实现(同样加入中文注释)

import math

def trial_division(n):
    """
    试除法分解质因数,返回质因子列表
    n必须是大于1的整数
    """
    factors = []  # 存储质因子的列表
    # 处理因子2
    while n % 2 == 0:
        factors.append(2)  # 记录因子2
        n //= 2            # 把2除掉
    
    # 试除奇数因子,从3开始
    i = 3
    # 用 i <= n // i 避免浮点误差,且比 i*i <= n 更安全(防止大数平方溢出)
    while i <= n // i:
        while n % i == 0:
            factors.append(i)  # 记录因子i
            n //= i            # 把i除掉
        i += 2  # 只试奇数
    
    # 如果n>1,则剩下的n是质因子(比如n本身是质数)
    if n > 1:
        factors.append(n)
    
    return factors

def main():
    try:
        num = int(input("请输入一个大于1的整数: "))
        if num <= 1:
            print("输入必须大于1")
            return
        result = trial_division(num)
        # 用*号连接打印
        print(f"{num} 的质因数分解: {' * '.join(map(str, result))}")
        # 验证
        prod = 1
        for f in result:
            prod *= f
        print(f"验证: {prod} {'(正确)' if prod == num else '(错误)'}")
    except ValueError:
        print("请输入有效的整数")

if __name__ == "__main__":
    main()

运行示例

输入

请输入一个大于1的整数: 84

输出

84 的质因数分解: 2 * 2 * 3 * 7
验证: 84 (正确)

输入

请输入一个大于1的整数: 97

输出

97 的质因数分解: 97
验证: 97 (正确)

五、试除法的局限与改进

试除法虽然简单,但当n超过10^12时,循环次数可能达百万甚至上亿,速度变慢。另外,如果n本身是一个大质数(比如10^12+39),试除法需要检查约50万个奇数才能判断它是质数,这在某些实时系统中可能不够快。

改进方向

  1. 结合Miller-Rabin素性测试快速判断是否为质数,如果确定是质数,直接输出,省去试除。
  2. 先用筛法预处理出小质数表(比如百万以内的质数),然后用这些质数进行试除,可以加速。
  3. 对于大数,使用Pollard Rho随机算法,它能在较短时间内找到大数的因子,但实现更复杂。

六、练一练(巩固知识)

  1. 手工分解:试试用试除法分解 999983。提示:先判断它是不是质数(√999983 ≈ 999.99,只需试除到997的奇数)。你能发现什么?
  2. 修改代码:输出每个质因子的指数形式。例如 12 = 2^2 * 3,而不是 2 * 2 * 3。提示:可以用一个循环统计相同因子的个数。
  3. 思考题:为什么试除法的复杂度是O(√n)?如果n是质数,最坏情况是什么?(提示:需要试多少个奇数?)
  4. 编程挑战:输入一个整数,输出它的所有质因子,要求每个因子只输出一次,并标注指数(例如 84 = 2^2 * 3^1 * 7^1)。提示:在原代码基础上增加一个计数器变量即可。

七、相关知识点指引

学会了试除法,你就可以进一步学习:

  • 埃拉托斯特尼筛法:快速生成小于某个数的所有质数,适合用来优化试除法的“质数表”。
  • Miller-Rabin素性测试:快速判断一个数是不是质数,适合用来先判断再分解超大质数。
  • Pollard Rho算法:一个随机算法,能分解很大的合数(比如10^18以上),是密码学中的常用工具。

掌握这些知识,你就能从“钥匙一把一把试”升级到“用万能钥匙”了!加油!

例题精讲

1单选题

试除法分解质因数的核心思想是什么?

A从最小的质数2开始,不断尝试除以n,如果能整除则记录因子,并继续除以该因子,直到n变为1
B从n开始递减,找到第一个因子
C用埃氏筛筛选出所有不超过n的质数,再逐个判断是否能整除n
D使用随机算法生成可能的因子
2判断题

在试除法分解质因数中,如果能被2整除,那么后续因子都是奇数,因此可以将步长设为2,仅试除奇数。

3填空题
以下函数使用试除法分解质因数,请补全循环体中的代码(C++风格)。

vector<int> factorize(int n) {
    vector<int> factors;
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            factors.push_back(i);
            while (n % i == 0) {
                ___;
            }
        }
    }
    if (n > 1) factors.push_back(n);
    return factors;
}
4单选题

以下哪个优化能最有效地提高试除法分解质因数的最坏情况性能?

A只试除质数(预处理质数表)
B试除到n/2
C使用除法代替取模
D将n开平方
5判断题

在使用试除法分解质因数时,对于任意合数n,其所有质因子都一定不大于√n。