能力综合提升-数学-1
0x01 素数
线性筛素数
首先是线性筛吧!这个必须要背的。
注意:在线性筛中,每一个合数都是被最小的质因子筛掉。
时间复杂度
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
判断一个数是不是素数。
- 暴力试除法
,不可接受。 - Fermat 小定理:
为素数时, —— 然而逆命题并不一定成立,这些素数被称为 Carmichael 数( )。在 以内,这样的数有 646 个,因此不能满足需求。 - Miller-Rabin 素性测试。
以下内容来自 warzone 的 Blog。
Miller-Rabin 素性测试是对 Fermat 小定理的优化。具体的,我们有二次探测定理:
若
为奇素数,则 的解为
PPT 上是这么说的:如果
具体的,若
- 选取一个底数
,初始化指数 ,判断 是否为 ,若否则非素数。 - 重复
并判断,直到 为奇数。 - 最后,若
,则 非素数,否则 大概率是一个素数。
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 时可能会犯的错误:
- 没有特判
的情况。 - 先计算
,再检查 的奇偶性。
若随机选取
对于竞赛的特殊环境,有
- 判断
unsigned int
范围内,选取2, 7, 61
三个底数即可。 - 判断
unsigned long long
范围内,选取2, 325, 9375, 28178, 450775, 9780504, 1795265022
七个底数即可。 - 判断
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
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;
}
但是有一个问题:这个伪随机数的函数会包含死循环————循环节的产生不难理解:在模
这个问题可以基于 Floyd 算法优化。具体的,我们希望判断环的存在,通俗来讲就是“龟兔赛跑”。假设环的长度为
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;
}
考虑做
- 枚举
,计算 。 - 但是这样长度太长才检查一次,所以加一个每
次乘积之后就检查一次的条件。一般 (此处无任何道理说是)。
注:做乘法要防止炸
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);
}
不过时间复杂度是
当我们需要访问 gcd()
很多次时,就需要用到其他的算法了。
一种基于值域预处理的快速 gcd
以下内容来自 moongazer 的 Blog。
这是一种 gcd
的算法。
记:
定理一:可以把值域内的每个
反证。不失一般性,令
而
定理二:对于 gcd(x, y)
,考虑
我们发现实现的难点在于如何在
记最小质因子函数
如何分解?对于
试着证明它:
时显然成立,分解为 ; 时有 则 ; 时 ,有 ; ,则 , ,则 ,从而 与 矛盾,故此情况不存在。
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;
}
例题
求出
一个相当经典的结论是,直接去求
对于每个
那你从上往下扫一遍就好啦。
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;
}
例题
你看到这个东西很自然的就要想到
那你直接
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 欧拉函数
性质
是积性函数。即
为奇数时 , 是质因数
部分证明见 OI-Wiki。
单点求
主要利用性质
可以用 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;
}
线性筛
只需要在线性筛素数的基础上改进即可。
还记得线性筛的原理吗?每一个合数都是被最小的质因子筛掉。
记
分两个情况讨论:
,那么 包含了 的所有质因子。
,那么 ,由欧拉函数的积性有
其中
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)
}
}
}
例题
忽略几个特殊的点,我们注意到,可以被看到的人必须满足
由于把
注意到
求
例题
求
例题
求
对于