CC++ & Algorithm

Miller-Rabin 大数素性测试

极难4
语言版本:通用
概述:用随机数快速判断一个数是不是质数,虽然偶尔误判但概率极低,就像用试纸测试病毒一样快速高效。

用“魔法镜子”快速判断大数字是不是质数——Miller-Rabin素性测试

你有没有想过,当你看到一个巨大的数字,比如123456789012345678901234567890123456789,你想知道它是不是质数(只能被1和自己整除)?如果用“试除法”一个个试,简直像把一片沙漠里的沙子一粒粒数——累死也数不完!这时候,我们需要一种像“魔法镜子”一样的高效方法:Miller-Rabin素性测试。它几分钟就能搞定一个几百位的数字,虽然偶尔会“看走眼”,但多照几次,错误的概率比被雷劈中还低!

一、为什么需要Miller-Rabin?——从生活小事说起

想象你是一个博物馆馆长,要鉴定一幅画是不是真迹(质数)。传统方法是请专家仔细检查每一笔颜料(试除法),但画很大,检查完可能几个月就过去了。Miller-Rabin就像一台“指纹扫描仪”:你按下一个按钮(随机选择一个数作为“基”),机器快速扫一下,如果它说“这不是真迹”,那肯定不是;如果它说“可能是真迹”,虽然可能误判,但多按几次按钮,误判的概率就降到几乎没有。

在编程竞赛、密码学(比如RSA加密需要生成大质数)中,判断一个数是否为质数是非常常见的需求。Miller-Rabin因为速度快、准确率高,成了最常用的方法。

二、数学原理拆解——从费马小定理到“二次探测”

1. 费马小定理:质数的“身份证”

费马小定理说:如果p是质数,那么对于任何整数a(a不能被p整除),一定有:

a^(p-1) ≡ 1 (mod p)

意思是:把a的(p-1)次方除以p,余数一定是1。反过来,如果存在一个a使得余数不是1,那么p肯定不是质数——这个a就像“证人”,立刻揭穿p的合数身份。

但!有些合数(比如561,叫做卡迈克尔数)对所有a都满足这个等式,就像有些假画做得太逼真,指纹扫描仪也认不出来。所以费马测试不够可靠。

2. Miller-Rabin的改进:加一道“二次探测”防线

Miller-Rabin升级了费马测试,加上了“平方根侦探”:如果p是奇质数,我们可以把p-1写成 d × 2^s,其中d是奇数(把偶数因子2都提出来)。那么,对于任何a,神奇的事会发生:

  • 要么 a^d mod p 等于1;
  • 要么 a^d, a^(2d), a^(4d), ..., 直到a^(d·2^(s-1))这些数中,有一个等于p-1(即-1 mod p)。

简单说,就像楼梯:要么你一开始就在一楼(a^d ≡ 1),要么你在中间某层遇到了“负一层”(a^(某次) ≡ -1)。如果这两个条件都没出现,那这个数一定是合数。这就是二次探测——它能抓住那些被费马测试放过的骗子。

3. 为什么叫“随机体检”?

测试时,我们随机选几个a(称为“基”),对每个a进行上述检查。如果某个a让条件不成立,n就是合数;如果所有a都通过,n很可能是质数。每个a误判的概率不超过1/4(因为对于任何合数,至少有3/4的a会揭露它)。独立选k个基,误判概率 ≤ (1/4)^k。k=10时概率小于百万分之一,k=20时比中彩票还难——几乎零风险。

三、算法步骤——一步步操作

假设我们要测试一个奇数n(n≥3):

  1. 分解指数:把n-1写成 d × 2^s,其中d是奇数。

    • 例如 n=17,n-1=16,16 = 1×2^4,所以 d=1,s=4。
    • 例如 n=25,n-1=24,24 = 3×2^3,所以 d=3,s=3。
  2. 选基:选择k个基a。对于小范围(小于2^64),可以用固定基组;对于大数,随机选a(2到n-2之间)。

  3. 对每个a检测

    • 计算 x = a^d mod n(用快速幂)。
    • 如果 x == 1 或 x == n-1,直接通过(继续下一个a)。
    • 否则,循环s-1次:每次将 x 平方 mod n( x = x^2 % n )。
      • 如果某次 x == n-1,通过(逃离循环)。
      • 如果某次 x == 1,说明找到了非平凡平方根(1的平方根除了±1还有别的),n一定是合数——立刻退出并返回“非质数”。
    • 如果s-1次平方后从未出现n-1,返回“非质数”。
  4. 最终判断:如果所有a都通过,我们说n是“强伪质数”——实际上几乎肯定是质数。

四、常见错误——新手容易踩的坑

  1. 忘记处理偶数和小数字:如果n是偶数且不是2,直接返回false;如果n=2或3,直接返回true。
  2. 基的选择太大导致溢出:在快速幂中,乘法可能超过普通整数范围,需要__int128(C++)或Python自动大整数。
  3. 快速幂写错模运算:a^b mod m 必须每一步都取模,否则数字会爆炸。
  4. 二次探测的循环次数:s-1次平方,不是s次;第一次x是在平方之前算的。
  5. 忽略a是n的倍数的情况:如果a % n == 0,a^d mod n = 0,不会等于1或-1,导致误判。所以检测前最好跳过或直接返回true(因为这样的a没用)。
  6. 固定基组超出范围:比如基组{2,3,5}对于n=3时,基a=3等于n,需要处理(跳过或用更小的基)。

五、完整代码示例(C++和Python)

代码中每行变量都加了通俗的中文注释,方便理解。

C++ 实现(确定性版本,适用于小于2^64的所有数)

#include <iostream>
using namespace std;

typedef unsigned long long ull;  // 定义无符号长长整型为ull,方便输入大数

// 快速幂取模:计算 (a的b次方) % m,使用二进制分解加速
ull mod_pow(ull a, ull b, ull m) {
    ull result = 1;          // 存放最终结果,初始为1
    a %= m;                  // 先让a模一下m,防止太大
    while (b > 0) {
        if (b & 1) {          // 如果b当前二进制位是1
            result = (__int128)result * a % m;  // 乘上a,并取模(用__int128防止乘法溢出)
        }
        a = (__int128)a * a % m;  // a自己平方,并取模
        b >>= 1;                 // b右移一位,相当于除2
    }
    return result;
}

// 单次Miller-Rabin测试,用基a检测n
bool miller_rabin_test(ull n, ull a) {
    if (a % n == 0) return true;   // 如果a是n的倍数,跳过(这种情况不会暴露问题)

    // 将 n-1 分解为 d * 2^s
    ull d = n - 1;
    int s = 0;
    while (d % 2 == 0) {
        d /= 2;
        s++;
    }

    ull x = mod_pow(a, d, n);      // 计算 a^d mod n
    if (x == 1 || x == n - 1) return true;  // 通过第一关

    for (int i = 0; i < s - 1; i++) {
        x = (__int128)x * x % n;   // 平方取模
        if (x == n - 1) return true;      // 出现了 -1,通过
        if (x == 1) return false;  // 出现了1以外的平方根,n一定是合数!
    }
    return false;  // 循环结束都没出现 -1,合数
}

// 判断n是否为质数(确定性基组,适用于n < 2^64)
bool is_prime(ull n) {
    if (n < 2) return false;        // 小于2不是质数
    if (n == 2 || n == 3) return true;  // 2和3是质数
    if (n % 2 == 0) return false;   // 偶数(除2外)不是质数

    // 确定性基组:对于 n < 2^64,这些基就足够了
    ull bases[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
    for (ull a : bases) {
        if (a >= n) break;          // 如果基>=n,跳过(因为没意义)
        if (!miller_rabin_test(n, a)) {
            return false;           // 有一个基没通过,就是合数
        }
    }
    return true;  // 所有基都通过,n被认为是质数
}

int main() {
    ull num;
    cout << "请输入一个整数(支持到unsigned long long): ";
    cin >> num;
    if (is_prime(num)) {
        cout << num << " 是质数。" << endl;
    } else {
        cout << num << " 不是质数。" << endl;
    }
    return 0;
}

Python 实现(可处理任意大数,随机多次测试)

import random

def mod_pow(a, b, m):
    """快速幂取模:计算 a^b mod m,用于大数"""
    result = 1
    a %= m
    while b > 0:
        if b & 1:          # 如果b当前二进制位是1
            result = (result * a) % m
        a = (a * a) % m
        b >>= 1
    return result

def miller_rabin_test(n, a):
    """单次Miller-Rabin测试,用基a检测n"""
    if a % n == 0:
        return True       # 跳过倍数情况
    # 分解 n-1 = d * 2^s
    d = n - 1
    s = 0
    while d % 2 == 0:
        d //= 2
        s += 1
    x = mod_pow(a, d, n)
    if x == 1 or x == n - 1:
        return True
    for _ in range(s - 1):
        x = (x * x) % n
        if x == n - 1:
            return True
        if x == 1:
            return False   # 二次探测发现非平凡平方根
    return False

def is_prime(n, k=12):
    """
    Miller-Rabin素性测试
    参数:
        n: 待测整数(支持任意大)
        k: 测试循环次数,默认12次,错误概率极低
    对于小于2^64的数,可以用确定性基组(上面C++实现);这里用随机,更通用。
    """
    if n < 2:
        return False
    # 先检查小质数,快速排除
    small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
    for p in small_primes:
        if n % p == 0:
            return n == p   # 如果能被小质数整除,只有等于它本身才是质数
    # 随机测试k次
    for _ in range(k):
        a = random.randrange(2, n - 1)   # 随机选一个2到n-2之间的基
        if not miller_rabin_test(n, a):
            return False
    return True   # 所有测试通过,基本确定是质数

def main():
    try:
        num = int(input("请输入一个整数: "))
        if is_prime(num):
            print(f"{num} 是质数。")
        else:
            print(f"{num} 不是质数。")
    except ValueError:
        print("请输入有效整数")

if __name__ == "__main__":
    main()

六、完整示例展示

试试运行程序,输入以下数字:

  • 7 → 输出“是质数”
  • 15 → 输出“不是质数”
  • 2^61 - 1 = 2305843009213693951 → 这是一个有名的梅森质数,程序应该输出“是质数”
  • 561(卡迈克尔数)→ 程序会正确判断“不是质数”
  • 一个随机的100位数字(比如1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890)→ 虽然它很大,但程序也能快速给出结果(大概率是合数)。

七、性能与稳定性——为什么它这么快?

Miller-Rabin每次测试只需要做O(log n)次模乘,对于一个100位的十进制数(约330位二进制),只需要几百次乘法。而试除法需要sqrt(n)次,那是10^50量级——宇宙毁灭都算不完。所以,当我们需要判断大数是否为质数时(比如在RSA加密中生成密钥、在Pollard Rho分解中快速筛选),Miller-Rabin是绝对的王者。

八、相关指引——接下来可以学什么?

如果你学会了Miller-Rabin,你可以继续探索:

  • Pollard Rho分解算法:用于分解大合数的因子,它内部就用Miller-Rabin来判断中间结果是否质数。
  • 确定性素性测试:比如AKS算法,它是第一个可以保证确定性的多项式时间算法,但速度慢,常用于理论。
  • 欧拉函数与密码学:质数在RSA、ElGamal等加密中扮演核心角色。
  • 二次探测与勒让德符号:更深入理解二次剩余在数论中的应用。

试着用上面的代码检测一下2^1279-1(一个著名的梅森质数),看看你的电脑多久能算出结果?你会发现,Miller-Rabin就像给大数做了一次“随机体检”——快速、高效,偶尔打盹,但多查几次就万无一失。

例题精讲

1单选题

在Miller-Rabin素性测试中,如果选取底数a后,计算得到a^(n-1) mod n ≠ 1,那么n是?

A肯定合数
B肯定素数
C可能素数
D无法确定
2判断题

Miller-Rabin测试是一种确定性素性测试,即如果返回“素数”,则n一定是素数。

3填空题
在Miller-Rabin算法中,需要对每个随机底数a计算模幂。请补全以下Python代码片段:
x = ___ (a, d, n)
其中d是n-1去除所有因子2后的奇数部分。
4单选题

Miller-Rabin测试中的二次探测定理表明,如果存在a满足a^2 ≡ 1 mod n但a ≠ ±1 mod n,则n是?

A素数
B合数
C可能是素数
D无法判断
5判断题

Miller-Rabin测试中,只要选取足够多的不同底数进行测试,就可以将错误概率降至任意小。