高级的数论知识更为困难,但也更有意思。本文将介绍省选以内的常见数论知识点,进一步学习模意义下的数论、数论函数和整除性相关问题,并通过题目介绍它们的应用。
整除性问题
与整除相关的内容是数论的根基,我们再来梳理一下这些内容。
算术基本定理
之前介绍过算术基本定理的标准分解式:
它还可以写成 进赋值序列。记 ,定义 的 进赋值序列为:。
这有什么用呢? 进赋值序列是从数论角度刻画正整数的重要工具,在这个视角下,每个整数均可以定位在一个无穷维空间上的整点,每个维度都对应一个素数。
取模与整除
首先:,实现了取模与整除的转化,需要根据具体情况分析使用哪种形式。
而 可以将一部分的除法问题转化为乘法问题。
数论分块
数论分块。 只有 中不同的取值,并且每一种取值都是一个连续的区间。
比如光速幂是利用这一点来实现的,这个技巧也可以在 时间内枚举到所有区间。
最经典的应用就是快速计算 ,需要 计算 ,然后将 相同的打包计算。
使得 成立的最大满足 的块的右端点为 。为什么?令 ,可知 ,那么 ,所以 。时间复杂度 。
模板,代码如下:
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long i64;
i64 H(i64 n) {
i64 res = 0; i64 l = 1, r;
while (l <= n) {
r = n / (n / l);
res += (r - l + 1) * (n / l);
l = r + 1;
}
return res;
}
int main(void) {
int T; i64 x; cin >> T;
while (T--) { cin >> x; cout << H(x) << "\n"; }
return 0;
}
同样可以扩展到有多个形如 的求和,只需要对 取 ,时间复杂度为 。
扩展欧几里得算法
辗转相除法和更相减损法是一个很重要的结构,出现时一般都伴随着 相关的结论。
类欧几里得算法
大致意义是使用一个类似辗转相除法的方法进行函数求和。
设:
需要给出一个 的算法。
数论分块似乎应对不了这样的求和,因此我们考虑一个奇怪的方式。要将 或 的情况转化为 。
我们有:
注意第二行中的后两个一定是可以整除的。
因此原式等于:
现在有 了,然后呢?式子中只有 一个变量。原式中的条件是 ,贡献是 。考虑将贡献合并计算,将式子改为:
这个 被 限制着,不好做,我们交换 ,让 被 限制:
万能欧几里得算法
万能欧几里得可以解决几乎全部类欧几里得算法能解决的问题。其优势在于,不管要求什么式子的值,代码都类似。
数论函数
我们一般研究数论函数中的积性函数。
常见积性函数
- 单位函数 ,是完全积性函数。
- 常数函数 ,是完全积性函数。
- 恒等函数 ,是完全积性函数,当 时可以省略不写。
- 除数函数 。这样的话,, 代表约数和,有 。
- 欧拉函数, 代表 中与 互质的数的个数,你肯定不会忘记它。
- 本质不同质因子个数函数 。
线性筛求积性函数
方法很简单,记录 表示最小质因子的最高次幂,然后分类讨论一下即可。
for (int i = 2; i <= n; i++) {
if (!vis[i]) prime[++tot] = i, f[i] = ..., low[i] = i; // 单独算 f(p)
for (int j = 1; j <= tot && i * prime[j] <= n; j++) {
vis[i * prime[j]] = 1;
if (i % prime[j] == 0) { // i 与 p 不互质
low[i * prime[j]] = low[i] * prime[j];
if (i == low[i]) f[i * prime[j]] = ...; // i = p^k,单独算 f(p^{k+1})
else f[i * prime[j]] = f[i / low[i]] * f[low[i] * prime[j]];
break;
}
low[i * prime[j]] = prime[j];
f[i * prime[j]] = f[i] * f[prime[j]]; // i 与 p 互质,f(ip) = f(i)f(p)
}
}
狄利克雷卷积
狄利克雷卷积是数论函数的基本运算。定义:
简记为 。
狄利克雷卷积具有交换律、结合律和分配律。
首先,。因此单位函数 为狄利克雷卷积的单位元,那么就可以定义数论函数的逆元 ,满足 。
一个 存在逆元,当且仅当 ,并且逆元唯一。 的充要条件是 。这样一来有 。
积性函数的狄利克雷卷积是积性函数,积性函数的逆元也是积性函数。
以下是一些常用的狄利克雷卷积:
- ,
- 。
狄利克雷前缀和
任意数论函数 卷常数函数 等价于做 的狄利克雷前缀和,即:。含义是对每个 计算给定数论函数在其所因数处的取值和。
将每个数写成无穷序列 表示 。由于 的充要条件为 ,因此 可以看成对下标做其无穷序列的高维前缀和。
模板,采用高维前缀和实现即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned int uint;
uint n, seed;
inline uint getnext(void) {
seed ^= seed << 13, seed ^= seed >> 17, seed ^= seed << 5;
return seed;
}
bool v[20000005];
uint prime[10000005], tot;
uint a[20000005];
int main(void) {
cin >> n >> seed;
for (int i = 1; i <= n; ++i) a[i] = getnext();
for (int i = 2; i <= n; ++i) {
if (!v[i]) prime[++tot] = i;
for (int j = 1; j <= tot && i * prime[j] <= n; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
}
}
for (int i = 1; i <= tot; ++i)
for (int j = 1; j * prime[i] <= n; ++j)
a[j * prime[i]] += a[j];
uint ans = 0;
for (int i = 1; i <= n; ++i) ans ^= a[i];
cout << ans << "\n"; return 0;
}
莫比乌斯反演
我们定义莫比乌斯函数:
使用线性筛求解莫比乌斯函数
mu[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= n; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) {
mu[i * prime[j]] = 0;
break;
}
mu[i * prime[j]] = -mu[i];
}
}
我们定义莫比乌斯函数:
为什么要定义这样一个奇怪的函数呢?实际上它是在对 做容斥。设 ,已知 ,要求 。 等于 在 的倍数处取的取值和,减去质数处的取值和,但是多减了两个质数乘积的地方,因此还要加回来。这就是容斥原理,容斥系数是 。比如求无平方因子的数的个数就是像这样容斥。
莫比乌斯函数不仅是积性函数,还满足:
上述式子描述的其实是 ,这是莫比乌斯函数最重要的性质,其引出了性质:,我们可以将这个判断式转化为和式,这样更加方便计算。两者的转化称之为莫比乌斯反演。
莫比乌斯反演有以下结论:
- 若 ,则 。
- 若 ,则 。这也被称为莫比乌斯变换。
- 由于 ,因此 。
一个常见的应用是,。虽然看起来这像是废话,但这一步将 互质转化为了枚举 的约数 。如果 同样需要枚举,那么枚举 并计算合法的 个数(当且仅当 )即可。具体来说:
就可以直接使用数论分块在 的时间内计算。
关于莫比乌斯反演的更多应用请在题目中寻找。
欧拉反演
欧拉函数有一条性质:。
用与莫比乌斯反演一样的套路,有:
这就是欧拉反演的公式。
模意义下的数论
数论算法的综合应用
Problemset
这里的题可能有一些难,也会与别的算法综合,但是都应该完成。
简单问题
不适合放在《数论初步》,但是又没用到本文介绍的知识点的简单题。
[SP5971] LCMSUM - LCM Sum
后面这个东西是在 以内所有与 互质的数的和,是欧拉函数的结论,答案是 ,注意 要特判掉。
线性筛出 即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const int N = 1000000;
int tot, prime[500005], low[1000005];
i64 f[1000005];
int main(void) {
f[1] = 1;
for (int i = 2; i <= N; ++i) {
if (!f[i]) prime[++tot] = low[i] = i, f[i] = 1 + 1ll * i * (i - 1);
for (int j = 1; j <= tot && i * prime[j] <= N; ++j) {
if (i % prime[j] == 0) {
low[i * prime[j]] = low[i] * prime[j];
if (i == low[i]) f[i * prime[j]] = f[i] + 1ll * (i * prime[j]) * i * (prime[j] - 1);
else f[i * prime[j]] = f[i / low[i]] * f[low[i] * prime[j]];
break;
}
low[i * prime[j]] = prime[j];
f[i * prime[j]] = f[i] * f[prime[j]];
}
}
int T, n; scanf("%d", &T);
while (T--) {
scanf("%d", &n);
printf("%lld\n", (f[n] + 1) * n / 2);
}
return 0;
}
[六省联考 2017] 相逢是问候
给定 ,维护一个正整数序列,支持区间替换为 ,区间求和(对 取模)。
数论分块
这里的数论分块不会很难,因为其往往会和莫比乌斯反演和杜教筛综合。
[Luogu P2424] 约数和
将 做前缀和之后就可以直接使用数论分块计算。
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long i64;
i64 H(int n) {
i64 res = 0, l = 1, r;
while (l <= n) {
r = n / (n / l);
res += (l + r) * (r - l + 1) * (n / l) / 2;
l = r + 1;
}
return res;
}
int main(void) {
int x, y; cin >> x >> y;
cout << H(y) - H(x - 1) << "\n";
return 0;
}
[CQOI2007] 余数求和
将取模拆开即可。
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long i64;
int main(void) {
i64 n, k;
scanf("%lld%lld", &n, &k);
i64 ans = n * k;
for (i64 l = 1, r; l <= n; l = r + 1) {
if (k / l != 0) r = min(n, k / (k / l));
else break;
ans -= (l + r) * (r - l + 1) * (k / l) / 2;
}
printf("%lld\n", ans);
return 0;
}
[CTT2012] 模积和
直接数论分块即可,需要将式子拆开。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const int P = 19940417;
i64 n, m;
i64 a, b, c;
i64 calc(int n) {
return 3323403ll * n % P * (n + 1) % P * (n * 2 % P + 1) % P;
}
i64 calc(int l, int r) {
return calc(r) - calc(l - 1);
}
int main(void) {
cin >> n >> m; if (n > m) swap(n, m);
a = n * n % P; b = m * m % P; c = n * m % P * n % P;
for (int l = 1, r = 0; l <= n; l = r + 1) {
r = n / (n / l);
a = (a - 1ll * (l + r) * (r - l + 1) / 2 % P * (n / l) % P) % P;
}
for (int l = 1, r = 0; l <= m; l = r + 1) {
r = m / (m / l);
b = (b - 1ll * (l + r) * (r - l + 1) / 2 % P * (m / l) % P) % P;
}
for (int l = 1, r = 0; l <= n; l = r + 1) {
r = min(n / (n / l), m / (m / l));
i64 len = 1ll * (l + r) * (r - l + 1) / 2 % P;
c = (c + calc(l, r) * (n / l) % P * (m / l) % P - m * len % P * (n / l) % P - n * len % P * (m / l) % P) % P;
}
cout << ((a * b % P - c) % P + P) % P << "\n";
return 0;
}
莫比乌斯反演
带有特殊技巧的题目会标注。
莫比乌斯反演往往只能去掉艾弗森括号(虽然莫比乌斯函数自身还有容斥的性质),因此重要的反而是推式子。
[HAOI2011] Problem b
利用二维差分进行计算,而且发现只有 的倍数有用,因此可以将所有数除以 转变为求互质。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int mu[50005], prime[50005], tot;
bool v[50005];
i64 calc(int x, int y) {
i64 res = 0;
for (int l = 1, r; l <= x && l <= y; l = r + 1) {
r = min(x / (x / l), y / (y / l));
res += 1ll * (mu[r] - mu[l - 1]) * (x / l) * (y / l);
}
return res;
}
int main(void) {
mu[1] = 1;
for (int i = 2; i <= 50000; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= 50000; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
mu[i * prime[j]] = -mu[i];
}
}
for (int i = 2; i <= 50000; ++i) mu[i] += mu[i - 1];
int T; scanf("%d", &T); while (T--) {
int a, b, c, d, k; scanf("%d%d%d%d%d", &a, &b, &c, &d, &k);
a = (a - 1) / k, c = (c - 1) / k, b /= k, d /= k;
printf("%d\n", calc(a, c) - calc(a, d) - calc(b, c) + calc(b, d));
}
return 0;
}
欧拉反演 | [NOI2010] 能量采集
要求的显然是:
然后欧拉反演直接拍上去。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int n, m;
int prime[50005], tot;
i64 phi[100005];
int main(void) {
scanf("%d%d", &n, &m);
if (m > n) swap(n, m);
phi[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!phi[i]) prime[++tot] = i, phi[i] = i - 1;
for (int j = 1; j <= tot && i * prime[j] <= n; ++j) {
if (i % prime[j] == 0) {
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
phi[i * prime[j]] = phi[i] * (prime[j] - 1);
}
}
for (int i = 1; i <= n; ++i) phi[i] += phi[i - 1];
i64 ans = 0;
for (int l = 1, r; l <= m; l = r + 1) {
r = min(n / (n / l), m / (m / l));
ans += 1ll * (phi[r] - phi[l - 1]) * (n / l) * (m / l);
}
printf("%lld\n", 2 * ans - 1ll * n * m);
return 0;
}
莫比乌斯函数与容斥原理 | [Luogu P4318] 完全平方数
设 代表 中无平方因子的数的个数,然后二分答案,找到 的最小 。现在考虑如何计算 。
首先去掉 的倍数,但是其中有的会算入两次,所以对于 这种要减去。而像 这种在 时已经算完了,不能统计。发现这就是莫比乌斯函数,因此莫比乌斯函数是在对 做容斥。答案是:
查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 200000;
int mu[200005], prime[100005], tot;
bool v[200005];
int calc(int n) {
int res = 0;
for (int i = 1; i * i <= n; ++i)
res += mu[i] * (n / (i * i));
return res;
}
int main(void) {
mu[1] = 1;
for (int i = 2; i <= N; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= N; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
mu[i * prime[j]] = -mu[i];
}
}
int T, n; scanf("%d", &T);
while (T--) {
scanf("%d", &n);
int L = 0, R = 2000000001;
while (L + 1 != R) {
int mid = L + (R - L) / 2;
if (calc(mid) >= n) R = mid;
else L = mid;
}
printf("%d\n", R);
}
return 0;
}
[Luogu P2257] YY 的 GCD
根之前的推法是一样的,不过外面要多枚举一个 。
推完之后有这么一个东西:
这个东西可以直接在 的时间预处理。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const int N = 10000000;
int mu[10000005], prime[4000005], tot;
i64 f[10000005];
bool v[10000005];
int main(void) {
mu[1] = 1;
for (int i = 2; i <= N; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= N; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
mu[i * prime[j]] = -mu[i];
}
}
for (int i = 1; i <= tot; ++i)
for (int j = 1; j * prime[i] <= N; ++j)
f[j * prime[i]] += mu[j];
for (int i = 2; i <= N; ++i) f[i] += f[i - 1];
int T; scanf("%d", &T); while (T--) {
int n, m; scanf("%d%d", &n, &m);
if (n > m) swap(n, m);
i64 ans = 0;
for (int l = 1, r; l <= n; l = r + 1) {
r = min(n / (n / l), m / (m / l));
ans += (f[r] - f[l - 1]) * (n / l) * (m / l);
} printf("%lld\n", ans);
}
return 0;
}
[SDOI2015] 约数个数和
关键是:
然后随便推就可以了。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const int N = 50000;
int prime[20005], tot;
bool v[50005];
int mu[50005], g[50005];
void init(void) {
mu[1] = 1;
for (int i = 2; i <= N; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= N; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
mu[i * prime[j]] = -mu[i];
}
}
for (int i = 1; i <= N; ++i) mu[i] += mu[i - 1];
for (int i = 1; i <= N; ++i) {
i64 res = 0;
for (int l = 1, r; l <= i; l = r + 1) {
r = i / (i / l);
res += 1ll * (r - l + 1) * (i / l);
}
g[i] = res;
}
}
int main(void) {
int T, n, m; scanf("%d", &T); init();
while (T--) {
scanf("%d%d", &n, &m);
if (n > m) swap(n, m);
i64 ans = 0;
for (int l = 1, r; l <= n; l = r + 1) {
r = min(n / (n / l), m / (m / l));
ans += 1ll * (mu[r] - mu[l - 1]) * g[n / l] * g[m / l];
} printf("%lld\n", ans);
}
return 0;
}
[国家集训队] Crash 的数字表格
来推式子!
设 ,则原式为 。
后面那坨也可以通过数论分块计算。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int P = 20101009;
const int N = 10000000;
int n, m, mu[10000005], prime[4000005], tot;
bool v[10000005];
int F(int n) { return 1ll * n * (n + 1) / 2 % P; }
int sum(int n, int m) {
int res = 0;
for (int l = 1, r; l <= n; l = r + 1) {
r = min(n / (n / l), m / (m / l));
res = (res + 1ll * (mu[r] - mu[l - 1]) * F(n / l) % P * F(m / l) % P) % P;
}
return res;
}
int main(void) {
mu[1] = 1;
for (int i = 2; i <= N; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= N; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
mu[i * prime[j]] = -mu[i];
}
}
for (int i = 2; i <= N; ++i) mu[i] = (mu[i - 1] + 1ll * i * i % P * mu[i] % P) % P;
scanf("%d%d", &n, &m); if (n > m) swap(n, m);
int ans = 0;
for (int l = 1, r; l <= n; l = r + 1) {
r = min(n / (n / l), m / (m / l));
ans = (ans + 1ll * (l + r) * (r - l + 1) / 2 % P * sum(n / l, m / l) % P) % P;
}
printf("%d\n", (ans + P) % P); return 0;
}
[CF1780F] Three Chairs
将 排序,要求的就是:
其中 表示是 的倍数的数的下标位置。简单转化一下贡献,后面那一坨就是:
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const int N = 300000;
int n;
int a[300005], mu[300005];
int prime[100005], tot, pos[300005], idx[300005];
bool v[300005];
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
sort(a + 1, a + n + 1); mu[1] = 1;
for (int i = 1; i <= n; ++i) pos[a[i]] = i;
for (int i = 2; i <= N; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= N; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
mu[i * prime[j]] = -mu[i];
}
}
i64 ans = 0;
for (int d = 1; d <= N; ++d) if (mu[d]) {
int k = 0;
for (int i = d; i <= N; i += d)
if (pos[i]) idx[++k] = pos[i];
i64 res = -1ll * k * (k - 1) / 2;
for (int i = 1; i <= k; ++i)
res += 1ll * idx[i] * (2 * i - k - 1);
ans += mu[d] * res;
}
printf("%lld\n", ans);
return 0;
}
[Luogu P4844] LJJ 爱数数
方程两边同乘 可以得到 。
设 ,则 ,可以证明 。
需要满足 ,设 ,则有 。最后让答案 ,再加上一个 即可。
由于 ,因此 的每一种质因子只能都给 或都给 ,这样有 ,因此 ,设 ,则答案为 ,莫比乌斯反演碾过去即可,时间复杂度为 。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
i64 n, R[1000005]; int m;
int mu[1000005], prime[400005], tot;
bool v[1000005];
void init(int n) {
mu[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!v[i]) prime[++tot] = i, mu[i] = -1;
for (int j = 1; j <= tot && i * prime[j] <= n; ++j) {
v[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
mu[i * prime[j]] = -mu[i];
}
}
}
int main(void) {
cin >> n; if (n == 1) return puts("0"), 0;
for (int i = 2; i <= n; ++i) {
R[i] = n / i - i; if (i - 1 < R[i]) R[i] = i - 1;
if (R[i] <= 0) break;
m = i;
} init(m);
i64 ans = 0;
for (int d = 1; d <= m; ++d) if (mu[d]) {
for (int i = d; i <= m; i += d)
ans += mu[d] * (R[i] / d);
}
printf("%lld\n", ans * 2 + 1); return 0;
}