Miller Rabin 和 Pollard Rho

为什么我要把他俩放一起写呢?俩随机化算法常常一起用,简直是天生一对儿。

Miller Rabin 算法

定义

Miller-Rabin 素性测试[1]是进阶的素数判定方法。

实现

二次探测定理

如果 \(p\) 是奇素数,则 \(x^2 \equiv 1 \pmod p\) 的解为 \(x \equiv1 \pmod p\)\(x\equiv p-1 \pmod p\)

若要证明,只需要将上面的方程移项,再平方差公式,得到 \((x+1)(x-1)\equiv 0 \pmod p\),即可得出上面的结论。

假设需要判断的数是 \(p\),我们把 \(p-1\) 分解成 \(2^k \times t\) 的形式。

由费马小定理[2],当 \(p\) 是素数时,有 \(a^{2^k \times t}\equiv 1 \pmod p\)

然后随机选择一个数 \(a\),计算出 \(a^t \bmod p\)

让其不断的自乘,同时结合二次探测定理进行判断。如果我们自乘后的数 \(\bmod p =1\),但是之前的数 \(\bmod p \ne \pm 1\),那么这个数就是合数(违背了二次探测定理)。

这样乘 \(k\) 次,最后得到的数就是 \(a^{p-1}\)。那么最后计算出的数不为 \(1\),这个数也是合数(费马小定理)。

代码

int Test[20] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
int test_time = 12;
bool millerRabin(int p) {
	if (p < 2) return 0;
	int t = p - 1, k = 0;
	while (!(t & 1)) { // 求出 k 和 t
		t >>= 1;
		k++; 
	}
	for (int i = 0; i < test_time; i++) {
        // 一般可以用前 12 个质数作为底数,当然也可以用随机数
		if (p == Test[i]) return 1;
		int a = fpow(Test[i], t, p);
		for (int j = 1; j <= k; j++) {
			if (a * a % p == 1 && a != 1 && a != p - 1) { 
				return 0; // 二次探测定理
			}
			a = a * a % p;
		}
		if (a != 1) return 0;
	}
	return 1;
}

Pollard Rho 算法

难评,看OI-WIKI吧。

引入

Pollard Rho 算法用于求快速找到一个正整数 \(n\) 的一个非平凡因数[3]

生日悖论

不考虑出生年份(假设每年都是365天),问:一个房间中至少多少人,才能使其中两个人生日相通的概率达到 \(50\%\)

解:假设一年有 \(n\) 天,房间中有 \(k\) 人,用 \(1\)\(k\) 给这些人编号。假定每个人的生日均与分布于 \(n\) 天之中,且两个人的生日相互独立。设 \(k\) 个人生日互不相同的时间为 \(A\),则 \(P(A)=\prod_{i=0}^{k-1}\frac{n-i}{n}\)。然后烂糟推一堆得出:

\[\exp(-\frac{k(k-1)}{2n})\le \frac{1}{2}\Rightarrow P(A)\le\frac{1}{2} \]

\(n=365\) 代入,解得 \(k \ge 23\)。所以一个房间中至少有 \(23\) 人,使其两人生日相同的概率达到 \(50\%\),但这个数学事实非常反直觉,故称之为一个悖论。当 \(k>56,n=365\) 时,出现两个人同一天生日的概率将大于 \(99\%\)

利用最大公约数求出一个约数

首先明确 \(n\) 和某个数的最大公约数一定是 \(n\) 的约数。我们可以通过 \(f(x)=(x^2+c)\bmod n\) 来生成一个序列 \({x_i}\):随机选取一个 \(x_i\),令 \(x_2=f(x_1),x_3=f(x_2),\dots,x_i=f(x_{i-1})\)。其中 \(c\) 是一个随机选取的常熟。

举个例子,设 \(n=50,c=6,x_1=1\)\(f(x)\) 生成的数据为 \(1,7,5,31,17,45,31,17,45,31,\dots\) 可以发现数据在 \(x_4\) 以后都在 \(31,17,45\) 循环。如果将这些数如下图一样排列起来,就发现他是一个这个玩意儿,长得像一个 \(\rho\),所以这个算法得名 rho。

Pollard-rho1.png (520×480) (oi-wiki.org)

选择 \(f(x)=(x^2+c)\bmod n\) 作为这个生成函数序列,是因为它有一个性质:\(\forall x \equiv y \pmod p,f(x)\equiv f(y)\pmod p\),其中 \(p \mid n\)

证明

\(x=y \pmod p\),则可以将它们表示为 \(x=k_1p+a,y=k_2p+a\),满足 \(k_1,k_2,a \in \mathbb{Z},a \in [0,p)\)

\(f(x)=(x^2+c)\bmod n\),因此 \(f(x)=x^2+c-kn\),其中 \(k\in\mathbb{Z}\)

\[\begin{align*} f(x) & = x^2+c-kn\\ & = (k_1p+a)^2+c-kn\\ & = k_1^2p^2+2k_1pa+a^2+c-kn\\\ & \equiv a^2+c \end{align*} \]

同理, \(f(y)\equiv a^2+c \pmod p\),因此 \(f(x) \equiv f(y) \pmod p\)

根据生日悖论,生成的序列中不同值的数量约为 \(\sqrt{n}\) 个。设 \(m\)\(n\) 的最小非平凡因子,显然有 \(m \le \sqrt{n}\)。将 \({x_i}\) 中每一项对 \(m\) 取模,我们得到了一个新序列 \({y_i}\),并且根据生日悖论可以得知新序列中不同值的个数约为 \(O(\sqrt m) \le O(n^{\frac{1}{4}})\)。因此,我们可以在期望 \(O(n^{\frac{1}{4}})\) 的时间内找到两个位置 \(i,j\),使得 \(x_i\ne x_j \&\& y_i=y_j\),这意味着 \(n \nmid |x_i-x_j| \&\& m\mid |x_i-x_j|\),我们可以通过 \(\gcd(n,|x_i-x_j|)\) 获得 \(n\) 的一个非平凡因子。

代码实现

这是一个带了倍增优化的代码。

#include <bits/stdc++.h>
#define int long long

using namespace std;
mt19937 wdz(time(0)); // 随机数
int N;
int gcd(int n, int m) {
	return m == 0 ? n : gcd(m, n % m);
}
int pollard(int n, int c) {
	int x, y, d, i = 1, k = 2;
	x = wdz() * wdz() % (n - 1) + 1; // 此处的 wdz() 是随机数
	y = x;
	while (1) {
		x = (x * x % n + c) % n;
		d = gcd((x - y + n) % n, n);
		if (1 < d && d < n) return d;
		if (x == y) return n;
        // x = y 说明构成了一个环,说明选定的 c 不好,那么重新来一遍
		if (++i == k) {
			k <<= 1;
			y = x;
            // 由于 y 不一定在环内,所以随时更新 y 的值,不然会死循
		}
	}
	return 23333333; // 返回一个值,无实际意义
}
signed main() {
	scanf("%lld", &N);
	int p = N;
	while (p >= N) {
		p = pollard(N, wdz() * wdz() % (N - 1) + 1);
	}
    // p 为 N 的一个因子
	printf("%lld\n", p);
	
	return 0;
}

你别急,再给一个再加上 127 优化的代码。

int pollard(int n, int c) { // 127 优化的 pollard rho
	int x, y, d, i = 1, k = 2, v = 1;
	x = y = 0;
	while (1) {
		x = (x * x % n + c) % n;
		v = v * abs(x - y) % n;
		if (i % 127 == 0) {
			d = gcd(v, n);
			if (1 < d) return d;
		}
		if (x == y) return n;
		if (++i == k) {
			k <<= 1;
			y = x;
			d = gcd(v, n);
			if (1 < d) return d;
		}
	}
	return 23333333;
}

结合应用

例题

P4718 【模板】Pollard-Rho - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

对于每一个询问,给出一个整数。如果这个数是质数就输出 Prime;如果不是质数,输出它最大的质因子是哪个

代码

#include <bits/stdc++.h>

using namespace std;

mt19937 wdz(time(0));
long long read() {
	long long x = 0; char ch = getchar();
	while (ch < '0' || ch > '9') ch = getchar();
	while (ch >= '0' && ch <= '9') {
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar();
	}
	return x;
}
void write(long long x) {
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
long long gcd(long long n, long long m) {
	return m == 0 ? n : gcd(m, n % m);
}
long long fpow(long long a, long long x, long long MOD) {
	a %= MOD;
	long long ans = 1;
	while (x) {
		if (x & 1) ans = (__int128)ans * a % MOD; 
		a = (__int128)a * a % MOD;
		x >>= 1;
	}
	return ans;
}
int Test[15] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
int test_time = 12;
bool millerRabin(long long p) {
	if (p < 2) return 0;
	long long t = p - 1, k = 0;
	while (!(t & 1)) {
		t >>= 1;
		k++; 
	}
	for (long long i = 0; i < test_time; i++) {
		if (p == Test[i]) return 1;
		long long a = fpow(Test[i], t, p);
		for (long long j = 1; j <= k; j++) {
			if ((__int128)a * a % p == 1 && a != 1 && a != p - 1) {
				return 0;
			}
			a = (__int128)a * a % p;
		}
		if (a != 1) return 0;
	}
	return 1;
}
long long pollard(long long n, long long c) { // 127 优化的 pollard rho
	long long x, y, d, i = 1, k = 2, v = 1;
	x = y = 0;
	while (1) {
		x = ((__int128)x * x % n + c) % n;
		v = (__int128)v * abs(x - y) % n;
		if (i % 127 == 0) {
			d = gcd(v, n);
			if (1 < d) return d;
		}
		if (x == y) return n;
		if (++i == k) {
			k <<= 1;
			y = x;
			d = gcd(v, n);
			if (1 < d) return d;
		}
	}
	return 23333333;
}
long long n, ans;
void resolve(long long x) { // 找到 x 的最大质因子,记录在 ans 中
	if (x == 1 || x <= ans) return; 
    // 一个剪枝,如果 x 已经小于等于当前最大质因子,则绝对不可能产生比当前答案更大的解
	if (millerRabin(x)) {
		ans = max(ans, x);
		return;
	}
	long long p = x;
	while (p >= x) {
		p = pollard(x, wdz() % (x - 1) + 1);
	}
	while (x % p == 0) x /= p;
	resolve(x);
	resolve(p);
}
int main() {
	int qq = read();
	while (qq--) {
		n = read(), ans = 0;
		resolve(n);
		if (ans == n) {
			printf("Prime\n");
		} else {
			write(ans);
			putchar('\n');
		}
	}
	
	return 0;
} 

脚注


  1. 素性测试(Primality test)是一类在 不对给定数字进行素数分解(prime factorization)的情况下,测试其是否为素数的算法。 ↩︎

  2. 费马小定理:若 \(p\) 为素数,\(\gcd(a,p)=1\),则 \(a^{p-1}\equiv 1 \pmod p\)↩︎

  3. 平凡约数(平凡因数):对于整数 \(b \ne 0\)\(\pm1\)\(\pm b\)\(b\) 的平凡约数。当 \(b=\pm1\) 时,\(b\) 只有两个平凡约数。非平凡约数就是指 \(b\) 的所有约数中,除了 \(\pm1\)\(\pm b\) 的其他约数。 ↩︎

posted @ 2024-08-23 20:08  Zctf1088  阅读(65)  评论(0)    收藏  举报