标签: 数学

Miller-Rabin素性测试与Pollard’s rho算法

Miller-Rabin素性测试与Pollard’s rho算法

Miller-Rabin素性测试(Miller–Rabin primality test)

我们知道,朴素的素性测试的实质是枚举因子,我们枚举2至\sqrt{n}的所有正整数,一一检验是否为因子,这样做的复杂度是O(\sqrt{n})。能不能使复杂度更低?

费马小定理

关于费马小定理的阐述参见:数学笔记:数论基础(部分) | KSkun’s Blog
我们知道了费马小定理的式子,那么它的逆定理如果成立,我们就得到了一种O(\log n)判定方法。可惜的是,逆定理并不成立。
我们选取若干a,每次检查a^{p-1} \bmod p是否为1,这种测试方法叫做费马测试。然而,如果有合数使得选取某个a时通过测试,则称该合数为以a为底的伪素数。能通过所有小于p的底数的合数p称为Carmichael数。如果这些数很大,大过了我们需要的范围,那还好。可是561就是一个很小的Carmichael数,这种测试仍然不能满足需求,我们需要加强测试。

二次探测定理

对于0 < x < p,若p是素数,则方程x^2 \equiv 1 \pmod{p}的解为x_1 = 1, x_2 = p - 1
证明:原方程可以化为 (x+1)(x-1) \equiv 0 \pmod{p},即 (x+1)(x-1) 可以整除p。因此x = \pm 1
因此对于p-1我们考虑拆成d \cdot 2^r。先计算出a^d的值,然后每次平方,检查结果是否为\pm 1,若是,则检查上一次结果是否为\pm 1。如果二次检测未通过,则非素数。这种测试就是Miller-Rabin素性测试了。我们可以知道,这样做的复杂度是O(T \log n)的,T代表测试次数。
但是仍有一些特殊合数能够通过以a为底的MR素性测试,因此我们要不断更换a多次测试。一般来说,我们可以随机几个a出来用,但是也可以预设一些a,比如2 3 7 61 24251这一组能够正确检测大多数数字。可以近似认为选择k个a测试的错误率为0.25^k

代码

本代码可以通过【P3383】【模板】线性筛素数 – 洛谷一题的较小数据。

// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

inline LL fpow(LL n, LL k, LL p) {
    LL res = 1;
    for(; k; n = n * n % p, k >>= 1) {
        if(k & 1) res = res * n % p;
    }
    return res;
}

inline bool miller_rabin(LL x) {
    if(x == 2) return true;
    LL t = x - 1, cnt2 = 0;
    while(!(t & 1)) {
        t >>= 1; cnt2++;
    }
    for(int i = 0; i < 5; i++) {
        LL a = rand() % (x - 1) + 1, now, lst;
        now = lst = fpow(a, t, x);
        for(int j = 1; j <= cnt2; j++) {
            now = lst * lst % x;
            if(now == 1 && lst != 1 && lst != x - 1) return false; // 二次检测
            lst = now;
        }
        if(now != 1) return false;
    }
    return true;
}

LL n, m, t;

int main() {
    srand(time(NULL));
    n = readint(); m = readint();
    while(m--) {
        t = readint();
        puts(miller_rabin(t) ? "Yes" : "No");
    }
    return 0;
}

Pollard’s rho算法(Pollard’s rho algorithm)

朴素的分解质因数算法是枚举每个可能的因数,并且把它们全部除出来,这样做的复杂度是O(\sqrt{n} \log n)的。有没有更好的方法?
依赖随机化怎么样?
直接随机到一个因子?概率很小。
那……如果两个因子分别为c和d且c>d,我们让a = \frac{c+d}{2}, b = \frac{c-d}{2},则a^2 - b^2 = n,枚举a和b,并检查a^2 - n是否是一个完全平方数。概率还是很小啊。
如果我们随机两个数,作差看看是不是呢?emmm没什么差别。
那么如果把作差与n取gcd呢?虽然直接枚举出因子的概率很小,但是因子的倍数是很多的吧。这种想法是能接受的。
于是我们可以随机两个数,作差,如果与n取gcd得到了一个因子则返回它。我们有一个很棒的伪随机函数f(x) = (x^2 + k) \bmod n,其中k是常量,在一个流程中一般不变化,但是取值没有特殊规定。然而,生成的随机数具有循环。
那么我们可以存下哪些数访问过了来避免进入死循环,但是可能会太大存不下呀!于是我们可以利用Floyd判环算法,即设定一个一倍速指针(f(x)),一个两倍速指针(f(f(x))),如果进入环中,两倍速指针一定会在某一时刻追上一倍速指针,此时环肯定已经完整地走完了一圈。(然而底下的代码似乎采用的是算导的方法,即记录下2的幂次结果,判断是否相等)如果遇到环,返回一个寻找失败的信息即可。不要气馁,重新设定常量k再跑一次吧。
其实PR算法的流程就是利用随机化特性找到n的一个因子,然后分治解决。那么分治的终点应该是该数为质数已经不可约了,我们把Miller-Rabin套进去,就可以判断当前数字是否是质数了。
那么总结一下这个算法的流程。

  1. 如果这个数字已经是质数了,加入这次分解的质因数表中并结束分治。
  2. 如果非质数,利用PR找到一个因子。
  3. 将n分为p和n/p两部分,分治解决。

代码

【P1075】质因数分解 – 洛谷

// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>

#include <vector>
#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

inline LL fpow(LL n, LL k, LL p) {
    LL res = 1;
    for(; k; n = n * n % p, k >>= 1) {
        if(k & 1) res = res * n % p;
    }
    return res;
}

LL T, n;

// Miller-Rabin素性检测
inline bool miller_rabin(LL x) {
    if(x == 2) return true;
    LL t = x - 1, cnt2 = 0;
    while(!(t & 1)) {
        t >>= 1; cnt2++;
    }
    for(int i = 0; i < 10; i++) {
        LL a = rand() % (x - 1) + 1, now, lst;
        now = lst = fpow(a, t, x);
        for(int j = 1; j <= cnt2; j++) {
            now = lst * lst % x;
            if(now == 1 && lst != 1 && lst != x - 1) return false;
            lst = now;
        }
        if(now != 1) return false;
    }
    return true;
}

std::vector<LL> divi;

inline LL gcd(LL a, LL b) {
    if(!b) return a;
    return gcd(b, a % b);
}

inline LL randint(LL lim) {
    return (1ll * rand() * RAND_MAX + rand()) % lim + 1;
}

inline LL pollard_rho(LL x, LL k) {
    LL a = randint(x - 1), b = a, t1 = 1, t2 = 2;
    for(;;) {
        t1++;
        a = (a * a % x + k) % x;
        LL g = gcd(abs(b - a), x);
        if(g > 1 && g < x) return g;
        if(b == a) break;
        if(t1 == t2) {
            b = a; // 记录2的幂次生成的结果
            t2 <<= 1;
        }
    }
    return x;
}

inline void caldivisor(LL x, LL k) {
    if(x == 1) return;
    if(miller_rabin(x)) {
        divi.push_back(x);
        return;
    }
    LL p = x;
    while(p >= x) p = pollard_rho(x, k--);
    caldivisor(p, k);
    caldivisor(x / p, k);
}

int main() {
    srand(time(NULL));
    n = readint();
    caldivisor(n, 200);
    printf("%lld\n", *std::max_element(divi.begin(), divi.end()));
    return 0;
}

1811 — Prime Test

吐槽:

  1. 被卡了std::abs的常
  2. 数据太大,long long也会爆,得换成慢速乘
  3. ctime头在POJ上会RE
// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>
//#include <ctime>
#include <cstdlib>

typedef long long LL;

inline LL min(LL x, LL y) {
    return x < y ? x : y;
}

inline LL abs(LL x) {
    return x < 0 ? -x : x;
}

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

inline LL mul(LL a, LL b, LL p) {
    a %= p; b %= p;
    LL res = 0;
    while(b) {
        if(b & 1) {
            res += a; res %= p;
        }
        a <<= 1;
        if(a >= p) a %= p;
        b >>= 1;
    }
    return res;
}

inline LL fpow(LL n, LL k, LL p) {
    LL res = 1;
    for(; k; n = mul(n, n, p), k >>= 1) {
        if(k & 1) res = mul(res, n, p);
    }
    return res;
}

LL T, n;

inline bool miller_rabin(LL x) {
    if(x == 2) return true;
    LL t = x - 1, cnt2 = 0;
    while(!(t & 1)) {
        t >>= 1; cnt2++;
    }
    for(int i = 0; i < 20; i++) {
        LL a = rand() % (x - 1) + 1, now, lst;
        now = lst = fpow(a, t, x);
        for(int j = 1; j <= cnt2; j++) {
            now = mul(lst, lst, x);
            if(now == 1 && lst != 1 && lst != x - 1) return false;
            lst = now;
        }
        if(now != 1) return false;
    }
    return true;
}

LL mn;

inline LL gcd(LL a, LL b) {
    if(a == 0) return 1;
    while(b) {
        LL t = a % b; a = b; b = t;
    }
    return a;
}

inline LL pollard_rho(LL x, LL k) {
    LL a = rand() % x, b = a, t1 = 1, t2 = 2;
    for(;;) {
        t1++;
        a = (mul(a, a, x) + k) % x;
        LL g = gcd(abs(b - a), x);
        if(g > 1 && g < x) return g;
        if(b == a) return x;
        if(t1 == t2) {
            b = a;
            t2 <<= 1;
        }
    }
}

inline void caldivisor(LL x) {
    if(x == 1) return;
    if(miller_rabin(x)) {
        mn = min(mn, x);
        return;
    }
    LL p = x;
    while(p >= x) p = pollard_rho(x, rand() % (x - 1) + 1);
    caldivisor(p);
    caldivisor(x / p);
}

int main() {
    //srand(time(NULL));
    T = readint();
    while(T--) {
        n = readint();
        if(miller_rabin(n)) {
            puts("Prime");
            continue;
        }
        mn = 1e15;
        caldivisor(n);
        printf("%lld\n", mn);
    }
    return 0;
}

参考资料

中国剩余定理及其扩展原理及应用

中国剩余定理及其扩展原理及应用

中国剩余定理(Chinese remainder theorem)

内容

对于以下一元线性同余方程组:
(S): \begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \\ \vdots \\ x \equiv a_n \pmod{m_n} \end{cases}
其中整数m_1, m_2, \ldots, m_n两两互质,则对于任意的整数a_1, a_2, \ldots, a_n,方程组 (S) 有解,并且可以用如下方式构造解:

  1. M=\prod_{i=1}^n m_iM_i = \frac{M}{m_i}
  2. M_i^{-1}M_im_i意义下的逆元,即M_iM_i^{-1} \equiv 1 \pmod{m_i}
  3. 该方程组的通解为x = kM + \sum_{i=1}^n a_iM_iM_i^{-1} \ (k \in \mathbb{Z})。在模M意义下,该方程组的唯一解为x = \sum_{i=1}^n a_iM_iM_i^{-1}

例题:中国剩余定理 问题 – 51Nod

按照上述方式构造解即可。代码如下。

// Code by KSkun, 2018/4
#include <cstdio>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 20;

int n, p[MAXN], m[MAXN];

inline LL fpow(LL n, LL k, LL p) {
    LL res = 1;
    while(k) {
        if(k & 1) res = res * n % p;
        n = n * n % p;
        k >>= 1;
    }
    return res;
}

inline LL crt() {
    LL pall = 1, res = 0;
    for(int i = 1; i <= n; i++) {
        pall *= p[i];
    }
    for(int i = 1; i <= n; i++) {
        res = (res + m[i] * pall / p[i] % pall 
            * fpow(pall / p[i], p[i] - 2, p[i]) % pall) % pall;
    }
    return res;
}

int main() {
    n = readint();
    for(int i = 1; i <= n; i++) {
        p[i] = readint(); m[i] = readint();
    }
    printf("%lld", crt());
    return 0;
}

扩展中国剩余定理(exCRT)

内容

对于CRT的扩展适用于CRT模数非互质的情况。
我们来研究两个同余方程的情况:
\begin{cases} x \equiv b_1 \pmod{a_1} \\ x \equiv b_2 \pmod{a_2} \end{cases} \Rightarrow \begin{cases} x = a_1x_1 + b_1 \\ x = a_2x_2 + b_2 \end{cases}
我们可以联立后面的展开式,得到这个式子a_1x_1 + a_2x_2 = b_2 - b_1。这个式子有解的条件是\mathrm{gcd}(a_1, a_2) | (b_2 - b_1),如果有解,我们可以利用扩展欧几里得算法(欧几里得算法和扩展欧几里得算法 | KSkun’s Blog)求出x_1的一个解(实际上求出的是方程a_1x_1 + a_2x_2 = \mathrm{gcd}(a_1, a_2)的解,需要转换,即乘一个\frac{b_2-b_1}{\mathrm{gcd}(a_1, a_2)}),回代可以得到x的一个解x'
这时,我们可以用这个方程x \equiv x' \pmod{\mathrm{lcm}(a_1, a_2)}代替原来的两个方程。
我们可以每次合并两个方程,最终得到一个同余方程,就可以求解了。

例题:2891 — Strange Way to Express Integers

直接利用上述方法解决即可。代码如下。

// Code by KSkun, 2018/4
#include <cstdio>

typedef long long LL;

const int MAXN = 100005;

LL k, a[MAXN], r[MAXN];

inline LL exgcd(LL a, LL b, LL &x, LL &y) {
    if(!b) {
        x = 1; y = 0; return a;
    }
    LL res = exgcd(b, a % b, x, y);
    LL t = x; x = y; y = t - a / b * y;
    return res;
}

inline LL excrt() {
    LL A = a[1], R = r[1], x, y;
    for(int i = 2; i <= k; i++) {
        LL g = exgcd(A, a[i], x, y);
        if((r[i] - R) % g) return -1;
        x = (r[i] - R) / g * x; x = (x % (a[i] / g) + a[i] / g) % (a[i] / g);
        R = A * x + R;
        A = A / g * a[i]; R %= A;
    }
    return R;
}

int main() {
    while(scanf("%lld", &k) != EOF) {
        for(int i = 1; i <= k; i++) {
            scanf("%lld%lld", &a[i], &r[i]);
        }
        printf("%lld\n", excrt());
    }
    return 0;
}

参考资料

[JLOI2016]成绩比较 题解

[JLOI2016]成绩比较 题解

题目地址:洛谷:【P3270】[JLOI2016]成绩比较 – 洛谷、BZOJ:Problem 4559. — [JLoi2016]成绩比较

题目描述

G系共有n位同学,M门必修课。这N位同学的编号为0到N-1的整数,其中B神的编号为0号。这M门必修课编号为0到M-1的整数。一位同学在必修课上可以获得的分数是1到Ui中的一个整数。
如果在每门课上A获得的成绩均小于等于B获得的成绩,则称A被B碾压。在B神的说法中,G系共有K位同学被他碾压(不包括他自己),而其他N-K-1位同学则没有被他碾压。D神查到了B神每门必修课的排名。
这里的排名是指:如果B神某门课的排名为R,则表示有且仅有R-1位同学这门课的分数大于B神的分数,有且仅有N-R位同学这门课的分数小于等于B神(不包括他自己)。
我们需要求出全系所有同学每门必修课得分的情况数,使其既能满足B神的说法,也能符合D神查到的排名。这里两种情况不同当且仅当有任意一位同学在任意一门课上获得的分数不同。
你不需要像D神那么厉害,你只需要计算出情况数模10^9+7的余数就可以了。

输入输出格式

输入格式:
第一行包含三个正整数N,M,K,分别表示G系的同学数量(包括B神),必修课的数量和被B神碾压的同学数量。第二行包含M个正整数,依次表示每门课的最高分Ui。第三行包含M个正整数,依次表示B神在每门课上的排名Ri。保证1<=Ri<=N。数据保证至少有1种情况使得B神说的话成立。

输出格式:
仅一行一个正整数,表示满足条件的情况数模10^9+7的余数。

输入输出样例

输入样例#1:

3 2 1
2 2
1 2

输出样例#1:

10

说明

N<=100,M<=100,Ui<=10^9

题解

本题需要的数学姿势:拉格朗日插值法及其应用 | KSkun’s Blog
参考资料:4559: [JLoi2016]成绩比较 – CSDN博客
计数问题,分层考虑。我们令dp[i][j]表示当前枚举到第i门课,被B神碾压的同学数为j的方案数,考虑转移中从j大的向j小的转移,即
dp[i][j] = \sum_{k=j}^n dp[i-1][k] \cdot \mathrm{C}_{k}^{k-j} \cdot \mathrm{C}_{n-k}^{R_i - 1 - (k-j)} \cdot d_i
组合系数的含义是,计算之前科目被碾压但该科没被碾压的学生方案数及其他该科不低于B神的学生方案数。后面的d_i含义是该科依照该排名的分数分配合法的方案数。这个值实际上可以通过枚举B神的分数而计算出来,如下
d_i = \sum_{j=1}^{U_i} k^{n-R_i}(U_i-k)^{R_i-1}
考虑到U_i的数据范围,暴力求解这个值是不太可行的。我们知道\sum_i i^n可以表达为一个n+1次多项式,因此d_i可以表达为一个关于U_i的n次多项式,我们求出n+1个点值对然后插值得到d_i即可。

代码

// Code by KSkun, 2018/4
#include <cstdio>
#include <cstring>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 105, MO = 1e9 + 7;

LL n, m, k, C[MAXN][MAXN], u[MAXN], r[MAXN], ptv[MAXN], dp[MAXN][MAXN];

inline LL fpow(LL n, LL k) {
    LL res = 1;
    while(k) {
        if(k & 1) res = res * n % MO;
        n = n * n % MO;
        k >>= 1;
    }
    return res;
}

inline void calc() {
    for(int i = 0; i <= n; i++) {
        C[i][0] = 1;
        for(int j = 1; j <= i; j++) {
            C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % MO;
        }
    }
}

inline LL interpolation(LL u, LL r) {
    memset(ptv, 0, sizeof(ptv));
    // 求n+1个点值
    for(int i = 1; i <= n + 1; i++) {
        for(int j = 1; j <= i; j++) {
            ptv[i] = (ptv[i] + fpow(j, n - r) * fpow(i - j, r - 1) % MO) % MO;
        }
    }
    // 插值
    LL res = 0;
    for(int i = 1; i <= n + 1; i++) {
        LL a = ptv[i], b = 1;
        for(int j = 1; j <= n + 1; j++) {
            if(i == j) continue;
            a = (a * (u - j) % MO + MO) % MO;
            b = (b * (i - j) % MO + MO) % MO;
        }
        res = (res + a * fpow(b, MO - 2) % MO) % MO;
    }
    return res;
}

int main() {
    n = readint(); m = readint(); k = readint();
    for(int i = 1; i <= m; i++) u[i] = readint();
    for(int i = 1; i <= m; i++) r[i] = readint();
    calc();
    dp[0][n - 1] = 1;
    for(int i = 1; i <= m; i++) {
        LL d = interpolation(u[i], r[i]);
        for(int j = k; j <= n - 1; j++) {
            for(int p = j; p <= n - 1 && p - j <= r[i] - 1; p++) {
                dp[i][j] = (dp[i][j] + dp[i - 1][p] * C[p][p - j] % MO 
                    * C[n - 1 - p][r[i] - 1 - (p - j)] % MO * d % MO) % MO;
            }
        }
    }
    printf("%lld", dp[m][k]);
    return 0;
}