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):
-
分解指数:把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。
-
选基:选择k个基a。对于小范围(小于2^64),可以用固定基组;对于大数,随机选a(2到n-2之间)。
-
对每个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,返回“非质数”。
-
最终判断:如果所有a都通过,我们说n是“强伪质数”——实际上几乎肯定是质数。
四、常见错误——新手容易踩的坑
- 忘记处理偶数和小数字:如果n是偶数且不是2,直接返回false;如果n=2或3,直接返回true。
- 基的选择太大导致溢出:在快速幂中,乘法可能超过普通整数范围,需要__int128(C++)或Python自动大整数。
- 快速幂写错模运算:a^b mod m 必须每一步都取模,否则数字会爆炸。
- 二次探测的循环次数:s-1次平方,不是s次;第一次x是在平方之前算的。
- 忽略a是n的倍数的情况:如果a % n == 0,a^d mod n = 0,不会等于1或-1,导致误判。所以检测前最好跳过或直接返回true(因为这样的a没用)。
- 固定基组超出范围:比如基组{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就像给大数做了一次“随机体检”——快速、高效,偶尔打盹,但多查几次就万无一失。
例题精讲
在Miller-Rabin素性测试中,如果选取底数a后,计算得到a^(n-1) mod n ≠ 1,那么n是?
Miller-Rabin测试是一种确定性素性测试,即如果返回“素数”,则n一定是素数。
在Miller-Rabin算法中,需要对每个随机底数a计算模幂。请补全以下Python代码片段:
x = ___ (a, d, n)
其中d是n-1去除所有因子2后的奇数部分。Miller-Rabin测试中的二次探测定理表明,如果存在a满足a^2 ≡ 1 mod n但a ≠ ±1 mod n,则n是?
Miller-Rabin测试中,只要选取足够多的不同底数进行测试,就可以将错误概率降至任意小。