文章

能力综合提升-数学-1

能力综合提升-数学-1

0x01 素数

线性筛素数

首先是线性筛吧!这个必须要背的。

注意:在线性筛中,每一个合数都是被最小的质因子筛掉。

时间复杂度 O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vector<int> prime;
bitset<Max> notprime;

void sift(const int Max)
{
    F(i, 2, Max)
    {
        if(!notprime[i]) prime.push_back(i);
        for(int j : prime)
        {
            if(i * j > Max) break;
            notprime[i * j] = true;
            if(i % j == 0) break; 
        }
    }
}

Miller-Rabin

判断一个数是不是素数。

  1. 暴力试除法 O(n),O(n),不可接受。
  2. Fermat 小定理:p 为素数时,apa(modp) —— 然而逆命题并不一定成立,这些素数被称为 Carmichael 数(561=3×11×17)。在 109 以内,这样的数有 646 个,因此不能满足需求。
  3. Miller-Rabin 素性测试。

以下内容来自 warzone 的 Blog

Miller-Rabin 素性测试是对 Fermat 小定理的优化。具体的,我们有二次探测定理:

p 为奇素数,则 x21(modp) 的解为 x±1(modp)

PPT 上是这么说的:如果 n 为素数,取 a<n,设 n1=d×2r,则要么 ad1(modn),要么 0i<r,s.t.ad×2i1(modn)。其证明见 Wikipedia

具体的,若 p 是奇数,则 p1 是偶数:

  1. 选取一个底数 a,初始化指数 d=p1,判断 admodp 是否为 1,若否则非素数。
  2. 重复 dd2 并判断,直到 d 为奇数。
  3. 最后,若 ad±1(modp),则 p 非素数,否则 p 大概率是一个素数。
1
2
3
4
5
6
7
8
9
10
11
12
bool check(const int a, const ll p)
{
    ll d = p - 1, res = fastpow(a, d, p);
    if(res != 1) return false;
    while(!(d & 1))
    {
        res = fastpow(a, d >>= 1, p);
        if(res == p - 1) return true;
        else if(res != 1) return false;
    }
    return true;
}

初学者写 Miller-Rabin 时可能会犯的错误:

  1. 没有特判 2p11(modp) 的情况。
  2. 先计算 admodp,再检查 d 的奇偶性。

若随机选取 k 个底数,Miller-Rabin 的错误率为 14k。也就是说,选取 2040 个底数,正确率是可以接受的。

对于竞赛的特殊环境,有

  1. 判断 unsigned int 范围内,选取 2, 7, 61 三个底数即可。
  2. 判断 unsigned long long 范围内,选取 2, 325, 9375, 28178, 450775, 9780504, 1795265022 七个底数即可。
  3. 判断 unsigned long long 范围内,选取前十二个素数(到 37 为止)作为底数即可,OEIS

注:不是哥们,你这个写的太他妈丑陋了,还是换一个抄吧。

注:fastpow 要注意别炸 long long,用 __int128

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
inline ll fastpow(ll a, ll b, const ll mod)
{
    ll ans = 1;
    while (b)
    {
        if (b & 1)
            ans = (__int128)ans * a % mod;
        a = (__int128)a * a % mod;
        b >>= 1;
    }
    return ans;
}
bool check(const ll a, const ll n)
{
    ll d = n - 1;
    while(d)
    {
        ll res = fastpow(a, d, n);
        if(res != 1 && res != n - 1)
            return false;
        if((d & 1) || res == n - 1)
            return true;
        d >>= 1;
    }
    return true;
}
vector<int> MRTest = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
bool Miller_Rabin(const ll p)
{
    if(p > 40)
    {
        for(int i : MRTest)
            if(!check(i, p)) return false;
        return true;
    }
    else
    {
        for(int i : MRTest)
            if(i == p) return true;
        return false;
    }
}

Pollard-Rho

质因数分解。

  1. 暴力枚举依然是 O(n) 的。

  2. 利用生日悖论减少枚举量。

生日悖论:从 n 个数中随机选取 k 个两两之差全部不等于 c 的概率约为 O(1nk2)

所以:找到 k 个数,检查这 k 个数两两的差与 ngcd

伪随机函数 fi=fi12+c

它具有的优秀的性质:如果 gcd(fifj,n)>1,则 gcd(fi+1fj+1,n)=gcd((fifj)(fi+fj),n)>1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mt19937_64 rand64(time(nullptr));
ll rnd(const ll l, const ll r) // 左闭右开
{
    return l + (rand64() % (r - l));
}
ll f(const ll x, const ll c, const ll p)
{
    return (x * x + c) % p;
}
ll Pollard_Rho(const ll n)
{
    ll x = rnd(0, n), y = x;
    // 关于这里 y 和下面 k 的含义见下面正文
    ll c = rnd(3, n), d = 1;
    // 当 c = 0 或 c = 2 的情况应当避免
    // 来自算法导论,书上也没有具体解释原因
    for(int i = 1, k = 2; d == 1; i ++)
    {
        x = f(x, c, n);
        d = gcd(qabs(y - x), n);
        if(i == k)
        {
            y = x;
            k <<= 1;
        }
    }
    return d;
}

但是有一个问题:这个伪随机数的函数会包含死循环————循环节的产生不难理解:在模 n 意义下,整个函数的值域只能是 0,1,2,,n1。总有一次,函数在迭代的过程中会带入之前的值,得到相同的结果。生成数列的轨迹很像一个希腊字母 ρ ,所以整个算法的名字叫做 Pollard-Rho。

这个问题可以基于 Floyd 算法优化。具体的,我们希望判断环的存在,通俗来讲就是“龟兔赛跑”。假设环的长度为 c,在环内有 fi=fi+kc。乌龟每次迭代一次,兔子每次迭代两次,如果二者相同,则必有 f2i=fi+kc,这样就找到了环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ll Pollard_Rho(const ll n)
{
    if(n == 4) // 初始跳两步没法分解 4,需要特判
        return 2;
    ll x = rnd(0, n), y = x;
    ll c = rnd(3, n), d = 1;
    x = f(x, c, n);
    y = f(f(y, c, n), c, n);
    while(x != y)
    {
        x = f(x, c, n);
        y = f(f(y, c, n), c, n);
        d = gcd(abs(x - y), n);
        if(d > 1)
            return d;
    }
    return n;
}

考虑做 gcd 的时间复杂度是 O(logn) 的,如果多次使用时间复杂度会大到爆炸。其实,每次计算 gcd 是不必要的,可以计算一段连续的乘积,然后再做 gcd 也是一样的效果(倍增的思想)。

  1. 枚举 i=2j,2j+1,,2j+11,计算 gcd((fif2j),p)
  2. 但是这样长度太长才检查一次,所以加一个每 t 次乘积之后就检查一次的条件。一般 t=128(此处无任何道理说是)。

注:做乘法要防止炸 long long,必须使用 __int128

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
ll mul(ll a, ll b, ll p)
{
    return __int128(a) * b % p;
}
mt19937_64 rand64(time(nullptr));
ll rnd(const ll l, const ll r)
{
    return l + (rand64() % (r - l));
}
ll f(const ll x, const ll c, const ll p)
{
    return (mul(x, x, p) + c) % p;
}
int cnt;
ll Pollard_Rho(ll n)
{
    if(n == 4)
        return 2;
    ll x = rnd(0, n), y = x;
    ll c = rnd(3, n), d = 1;
    x = f(x, c, n);
    y = f(f(y, c, n), c, n);
    for(int lim = 1; x != y; lim = qmin(128, lim << 1))
    {
        ll cnt = 1;
        F(i, 1, lim)
        {
            ll tmp = mul(cnt, qabs(x - y), n);
            if(tmp == 0)
                break;
            cnt = tmp;
            x = f(x, c, n);
            y = f(f(y, c, n), c, n);
        }
        d = gcd(cnt, n);
        if(d > 1)
            return d;
    }
    return n;
}   

0x02 最大公约数

辗转相除法

首先是最简单的欧几里得算法!

1
2
3
4
ll gcd(ll a, ll b)
{
    return b == 0 ? a : gcd(b, a % b);
}

不过时间复杂度是 O(logV)

当我们需要访问 gcd() 很多次时,就需要用到其他的算法了。

一种基于值域预处理的快速 gcd

以下内容来自 moongazer 的 Blog

这是一种 O(V) 时间预处理 O(1) 询问求 gcd 的算法。

记:V 为值域,P 为素数集,集合 a1,a2,,anx 的分解 x=i=1nai

定理一:可以把值域内的每个 x 分解成 a,b,c 且必有 a,b,c 要么 x 要么 P(称为合法分解)。

反证。不失一般性,令 abc。若 cPc>x,则必存在 c 的分解 d,e 且有 dedx

a×b=xc<x,于是也有 x 的分解 d,a×b,e。若 e>x 则重复上述过程直到满足条件。

定理二:对于 gcd(x, y),考虑 x 的合法分解 a,b,c,分别考虑它们对答案的贡献。a 的贡献为 va=gcd(a,y)b 的贡献为 vb=gcd(b,yva) (因为 va 这个因子已经被计算过,不能重复计算),c 的贡献同理,vc=gcd(c,yvavb)

我们发现实现的难点在于如何在 O(V) 时间内进行分解,询问部分较为容易。

记最小质因子函数 f(x)x 的最小质因子。

如何分解?对于 x2,找到 x 的最小质因子 p=f(x)xp 的一个合法分解 a0,b0,c0,则 x 的一个合法分解为 sorta0×p,b0,c0

试着证明它:

  1. xP 时显然成立,分解为 1,1,x
  2. px4 时有 a0xp3a0×px13p23x13x16=x
  3. p>x4
    1. a0=1,有 a0×p=px
    2. a01,则 xPq=f(xp)f(x)=p,则 pqa0b0c0,从而 p×a0×b0×c0>(x4)4=xp×a0×b0×c0=x 矛盾,故此情况不存在。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
constexpr int maxm = 1e6 + 10;
constexpr int maxk = 1e3 + 10;
struct Fac
{
    int a, b, c;
    Fac() : a(1), b(1), c(1) {}
    int operator[](int idx) // 方便使用 for 来调用每个元素
    {
        switch(idx)
        {
            case 1: return a;
            case 2: return b;
            case 3: return c;
            default: break;
        }
        return 0;
    }
    void rec(Fac t, int p) // 赋值并让 a *= p,并排序
    {
        a = t.a * p, b = t.b, c = t.c;
        sort();
    }
    void sort() // 注意到 bc 一定是顺序,只需要看 a 的位置即可
    {
        if(a > b) qswap(a, b);
        if(b > c) qswap(b, c);
    }
} fac[maxm]; //用于存放分解
bitset<maxm> isPrime; // bitset 存是否是质数
int Pri[maxm], cnt; // 质数集 P
int pre[maxk][maxk]; // 预处理的 sqrt(V) 内的 gcd
const int V = 1e6, T = 1e3; // T = sqrt(V)
void Pre() // 线性筛预处理
{
    isPrime.flip(); // 全部标为质数
    F(i, 2, V) // 线性筛
    {
        if(isPrime[i])
        {
            fac[i].c = i;
            Pri[++ cnt] = i;
        }
        for(int j = 1, k = Pri[j] * i; k <= V; j ++, k = Pri[j] * i)
        {
            isPrime[k] = false;
            fac[k].rec(fac[i], Pri[j]); // 把质数的分解偷过来,并且 Pre[j] 一定是最小质因子 p
            if(i % Pri[j] == 0)         // 最小的保证是因为 i % Pri[j] == 0 时就 break 掉了
                break;
        }
    }
    // 预处理 gcd 数组
    // 因为由辗转相除法,a > b 时有 gcd(a, b) = gcd(b, a % b),这可以预处理实现
    F(i, 0, sqrt(V))
        pre[0][i] = pre[i][0] = i;
    F(i, 1, sqrt(V))
        F(j, 1, i)
            pre[i][j] = pre[j][i] = pre[j][i % j];
}
int gcd(int a, int b)
{
    int ans = 1;
    F(i, 1, 3)
    {
        int tmp;
        if(fac[a][i] > sqrt(V)) // 查看 a 的分解的第 i 个和 sqrt(V) 的关系
        {
            if(b % fac[a][i])
                tmp = 1; // 互质,没有贡献
            else
                tmp = fac[a][i]; // 整除,贡献就是本身
        }
        else
            tmp = pre[fac[a][i]][b % fac[a][i]]; // 否则两边都是 sqrt(V) 内的内容可以预处理解决
        b /= tmp; // 防止算重
        ans *= tmp; // 计算贡献
    }
    return ans;
}

例题

求出 k=1,2,,n,在给定的 ai 中挑出 k 个数,最大的 gcd

n104,ai106

一个相当经典的结论是,直接去求 gcd 是没有必要的,枚举因子反而是更简单也更正确的。

对于每个 aiO(n) 暴力枚举因子把它们存到一个桶里,并且显然有 anskansk+1

那你从上往下扫一遍就好啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
    int n = readint();
    F(i, 1, n)
    {
        read(a);
        F(j, 1, sqrt(a))
            if(a % j == 0)
                b[j] ++, b[a / j] += (a != j * j);
    }
    for(int i = 1, j = 1e6; i <= n; i ++)
    {
        while(b[j] < i)
            j --;
        write('\n', j);
    }
    return 0;
}

例题

n 次询问,求出所有可能的 x 的个数,使 gcd(x,a0)=a1lcm(x,b0)=b1

n2000,1a0,a1,b0,b12×109

你看到这个东西很自然的就要想到

{gcd(x,a0)=a1(xmoda1=a0moda1=0)(gcd(xa1,a0a1)=1)lcm(x,b0)=b1(b1modx=b1modb0=0)(gcd(b1x,b1b0)=1)

那你直接 O(V) 枚举 x 然后依次检查这四个条件即可。总时间复杂度 O(nV)109 级别但是跑的飞快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool check(int c2, int d2, int a1, int b1, int x)
{
    int d1 = b1 / x;
    if(x % a1)
        return false;
    int c1 = x / a1;
    if(gcd(c1, c2) > 1)
        return false;
    if(gcd(d1, d2) > 1)
        return false;
    return true;
}
int workwork()
{
    int a0, a1, b0, b1;
    read(a0, a1, b0, b1);
    if(a0 % a1)
        return 0;
    int c2 = a0 / a1;
    if(b1 % b0)
        return 0;
    int d2 = b1 / b0;
    int ans = 0;
    F(i, 1, sqrt(b1))
    {
        if(b1 % i)
            continue;
        ans += check(c2, d2, a1, b1, i);
        if(i * i < b1)
            ans += check(c2, d2, a1, b1, b1 / i);
    }
    return ans;
}

0x03 欧拉函数

φ(n)=i=1n[gcd(n,i)=1]

性质

  • nPφ(n)=n1

  • 积性函数。即 a,b,gcd(a,b)=1φ(ab)=φ(a)φ(b)

  • n 为奇数时 φ(2n)=φ(n)

  • n=dnφ(d)

  • n=pk,pP,φ(n)=pkpk1

  • φ(n)=ni=1spi1pipi 是质因数

  • φ(gcd(m,n))φ(mn)=gcd(m,n)φ(m)φ(n)

  • n>12φ(n)

部分证明见 OI-Wiki

单点求 φ(n)

主要利用性质 φ(n)=ni=1spi1pipi 是质因数)进行 O(n) 的质因数分解。

可以用 Pollard-Rho 优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int Phi(int n)
{
    int ans = n;
    for(int i = 2; i * i <= n; i ++)
        if(n % i == 0) // 对于每个质因数
        {
            ans = ans / i * (i - 1);
            while(n % i == 0) // 除掉所有的当前质因数
                n /= i;
        }
    if(n > 1) // 分解最后剩下单个质数
        ans = ans / n * (n - 1);
    return ans;
}

线性筛 φ(n)

只需要在线性筛素数的基础上改进即可。

还记得线性筛的原理吗?每一个合数都是被最小的质因子筛掉

p1n 的最小质因子,有 n=p1×n

分两个情况讨论:

  • nmodp1=0,那么 n 包含了 n 的所有质因子

φ(n)=n×pi1pi=p1×n×pi1pi=p1×φ(n)

  • nmodp10,那么 gcd(p1,n)=1,由欧拉函数的积性有

φ(n)=φ(p1×n)=φ(p1)×φ(n)=(p11)×φ(n)

其中 n<nφ(n) 已经被算出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
vector<int> prime;
bitset<Max> notprime;
int Phi[Max];

void sift(const int Max)
{
    Phi[1] = 1; // 特判
    F(i, 2, Max)
    {
        if(!notprime[i]) 
        {
            prime.push_back(i);
            Phi[i] = i - 1; // 质数的情况
        }
        for(int j : prime)
        {
            if(i * j > Max) break;
            notprime[i * j] = true;
            if(i % j == 0)
            {
                Phi[i * j] = Phi[i] * j; // 情况 1,直接乘 p_1
                break;
            }
            Phi[i * j] = Phi[i] * Phi[j]; // 情况 2,乘 Phi(p_1) 也就是 (p_1 - 1)
        }
    }
}

例题

n×n 的方阵,求视线能看到的人的个数。

忽略几个特殊的点,我们注意到,可以被看到的人必须满足 gcd(x,y)=1。 证明是显然的,若 d=gcd(x,y)1,则 (xd,yd) 必然能把 (x,y) 挡住,所以我们只需要求出范围内互质的对数即可。

由于把 (1,1) 看成坐标原点,因此 x,yn1 而非 n

注意到 i,j 的对称性,只需要计算 i>j 的情况。剩下的 (0,1),(1,0),(1,1) 最后补上即可。

ans=2+i=1n1j=1n1[gcd(i,j)=1]=2+i=1n1[gcd(i,i)=1]+2×i=1n1j=1i1[gcd(i,j)=1]=3+2×i=1n1φ(i)

φ(n)n1 项和即可。时间复杂度 O(n)

例题

i=1nj=1n[gcd(i,j)P]

ans=pPi=1nj=1n[gcd(i,j)=p]=pPi=1npj=1np[gcd(i,j)=1]=pP(1+2×i=1npj=1i[gcd(i,j)=1])=pP(1+2×i=1npφ(i))

O(n) 预处理 φ(n)i=1nφ(i),枚举每个质数即可。时间复杂度 O(n)

例题

i=1nj=1ngcd(i,j)

ans=i=1nj=1nk=1n[gcd(i,j)=k]×k=k=1nk×i=1nj=1n[gcd(i,j)=k]

对于 i=1nj=1n[gcd(i,j)=k] 的处理,套路同上。时间复杂度 O(n)

本文由作者按照 CC BY 4.0 进行授权