数论复习笔记
stripe_python · · 算法·理论
质数
定义一个正整数
质数的判定
枚举法
可以枚举
这里给出一个
inline bool isprime(int x) {
if (x <= 1) return false;
if (x == 2 || x == 3) return true;
if (x % 6 != 1 && x % 6 != 5) return false;
for (int i = 5; i * i <= x; i += 6) {
if (x % i == 0 || x % (i + 2) == 0) return false;
}
return true;
}
Miller-Rabin 法
先引入二次探测定理,即:若
简证:由
x^2 \equiv 1 \pmod p 得(x-1)(x+1) \equiv 0 \pmod p ,即可得出。
接下来使用费马小定理:若
设
在 long long 范围内,只需选取
inline long long power(long long a, long long b, long long p) {
long long res = 1;
for (a %= p; b; b >>= 1) {
if (b & 1) res = res * a % p;
a = a * a % p;
}
return res;
}
const long long BASE[] = {2, 325, 9375, 28178, 450775, 9780504, 1795265022LL};
inline bool miller_rabin(long long x) {
if (x < 3 || !(x & 1)) return x == 2;
long long u = x - 1, t = 0;
while (!(u & 1)) u >>= 1, t++;
for (long long a : BASE) {
long long v = power(a, u, x);
if (a % x == 0 || v == 1) continue;
long long s = 0;
for (; s < t; s++) {
if (v == x - 1) break;
v = v * v % x;
}
if (s == t) return false;
}
return true;
}
威尔逊定理
若
其逆定理也成立。即若
算术基本定理
任何一个合数
存在性:用反证法,假设
唯一性:用反证法,假设
推论
设
推论1:
推论2:
分解质因数
将一个数分解质因数最简单的方法就是试除法,复杂度
int p[N], c[N];
int decompose(int n) {
int m = 0;
for (int i = 2; i * i <= n; i++) {
if (n % i) continue;
p[++m] = i, c[m] = 0;
while (n % i == 0) n /= i, c[m]++;
}
if (n > 1) p[++m] = n, c[m] = 1;
return m;
}
质数筛
埃氏筛
小学课本上学过,我们每遍历到一个质数,就把它的倍数划去,最后剩下的未被划去的就是质数。埃氏筛使用 bitset 优化后速度快于线性筛。
bitset<N> isprime;
void solve() {
isprime.set(), isprime[0] = isprime[1] = false;
for (int i = 2; i * i <= N; i++) { // 一个常数优化:筛到 O(sqrt(N)) 即可
if (!isprime[i]) continue;
for (int j = 1LL * i * i; j < N; j += i) isprime[j] = false;
}
}
复杂度为
线性筛
线性筛虽然跑不过 bitset 埃氏筛,但是它的思想值得学习。
埃氏筛复杂度到不了线性的原因是它会把一个合数划掉两遍。具体地,在标记
bitset<N> isprime;
int tot, prime[N];
void solve() {
isprime.set(), isprime[0] = isprime[1] = false;
for (int i = 2; i < N; i++) {
if (isprime[i]) prime[++tot] = i;
for (int j = 1; j <= tot; j++) {
if (i * prime[j] >= N) break;
isprime[i * prime[j]] = false;
if (i & prime[j] == 0) break;
}
}
}
区间筛
在一些题目中,我们可能需要求出
观察到
bitset<N> s, p;
void solve(int a, int b) { // 筛出[a, b)之间的质数,用 p[i - a] 判断i是否为质数
s.set(), p.set(), s[0] = s[1] = false;
if (a <= 1) p[1 - a] = false;
int z = (int) sqrt(b) + 1;
for (int i = 2; i < z; i++) {
for (int j = 2; i * j < z; j++) s[i * j] = false;
}
for (int i = 2; i * i <= b; i++) {
if (!s[i]) continue;
for (int j = max(i * i, (a + i - 1) / i * i); j < b; j += i) p[j - a] = false;
}
}
例题
P1147 连续自然数和
由等差数列求和:
枚举
解得
显然,
int n;
void _main() {
cin >> n; n *= 2;
for (int i = sqrt(n); i >= 2; i--) {
if (n % i) continue;
int j = n / i;
if ((i & 1) + (j & 1) == 1) cout << (j - i + 1) / 2 << ' ' << (j + i - 1) / 2 << '\n';
}
}
P1865 A % B Problem
由
P7960 [NOIP2021] 报数
质数筛的思想延伸。把含有
const int N = 1e7 + 1000; // 注意这里要开大点
int t, x, nxt[N];
bitset<N> dis;
inline bool check(int x) {
for (; x != 0; x /= 10) if (x % 10 == 7) return true;
return false;
}
void _main() {
int last = 0;
dis.reset();
for (int i = 1; i < N; i++) {
if (dis[i]) continue;
if (check(i)) {
for (int j = 1; j * i < N; j++) dis[j * i] = 1;
continue;
}
nxt[last] = i, last = i;
}
for (cin >> t; t--; ) {
cin >> x;
cout << (dis[x] ? -1 : nxt[x]) << '\n';
}
}
P1069 [NOIP 2009 普及组] 细胞分裂
形式化题意:给出
显然这题
这样就得到了
接下来我们求出
int n, m1, m2, a[N], tot;
int solve(int x) {
int res = 0;
for (int i = 1; i <= tot; i++) {
if (x % p[i]) return INT_MAX;
int cnt = 0;
while (x % p[i] == 0) x /= p[i], cnt++;
res = max(res, c[i] / cnt + (c[i] % cnt != 0));
}
return res;
}
void _main() {
cin >> n >> m1 >> m2;
for (int i = 1; i <= n; i++) cin >> a[i];
tot = decompose(m1); // 这个函数文章前面有
for (int i = 1; i <= tot; i++) c[i] *= m2;
int res = INT_MAX;
for (int i = 1; i <= n; i++) res = min(res, solve(a[i]));
cout << (res == INT_MAX ? -1 : res);
}
最大公约数
求解 GCD
STL
C++ STL 中有函数 std::__gcd,复杂度为
分解质因数法
将两数分解质因数:
这证明:求 gcd 和求最小值有共通之处。
辗转相除法
GCD 有如下性质:
利用此可递归计算 gcd,出口为
int gcd(int a, int b) {return b == 0 ? a : gcd(b, a % b);}
更相减损法
GCD 有如下性质:
如果直接递归计算,复杂度为
若
Stein 算法可以借助二进制内置函数优化,称作 Binary GCD。这份代码的速度比 std::__gcd 更快。可以卡过 P5435。
constexpr int gcd(int a, int b) {
int az = __builtin_ctz(a), bz = __builtin_ctz(b), z = min(az, bz);
b >>= bz;
while (a) {
a >>= az;
int d = a - b;
az = __builtin_ctz(d);
b = min(a, b), a = abs(d);
}
return b << z;
}
ExGCD
ExGCD 用于求解形如
裴蜀定理
对于任意非零整数
不定方程
由
递归式:
void exgcd(int a, int b, int& x, int& y) {
if (b == 0) return x = 1, y = 0, void();
exgcd(b, a % b, x, y);
int t = x; x = y, y = t - a / b * y;
}
乘法逆元
求
移项得:
线性方程组
这里的线性方程组是指求一个最小的
两两考虑。比如我们把前两个方程变成不定方程:
至于为什么不说 CRT,因为我觉得 exCRT 比 CRT 更容易理解,适用范围更广,代码还好写。
例题
P1072 [NOIP 2009 提高组] Hankson 的趣味题
有一个结论:若
证明:考虑反证法,设
由题可得:
由上述结论得:
int a0, a1, b0, b1;
void _main() {
cin >> a0 >> a1 >> b0 >> b1;
int cnt = 0;
for (int x = 1; x * x <= b1; x++) {
if (b1 % x) continue;
if (x % a1 == 0 && __gcd(x / a1, a0 / a1) == 1 && __gcd(b1 / x, b1 / b0) == 1) cnt++;
int y = b1 / x;
if (x == y) continue;
if (y % a1 == 0 && __gcd(y / a1, a0 / a1) == 1 && __gcd(b1 / y, b1 / b0) == 1) cnt++;
}
cout << cnt << '\n';
}
P4549 【模板】裴蜀定理
扩展裴蜀定理。由
而题目中记
int n, res, a;
int main() { // 两年前的马蜂
scanf("%d", &n);
while (n--) scanf("%d", &a), a = abs(a), res = __gcd(res, a);
printf("%d", res);
return 0;
}
P1082 [NOIP 2012 提高组] 同余方程
这东西我们在之前的乘法逆元讲过方法了,就是用 exGCD 解方程
template <class T> void exgcd(T a, T b, T& x, T& y) {
if (b == 0) return x = 1, y = 0, void();
exgcd(b, a % b, y, x), y -= a / b * x;
}
long long a, b;
void _main() {
cin >> a >> b;
long long x, y; exgcd(a, b, x, y);
cout << (x % b + b) % b;
}
P5656 【模板】二元一次不定方程 (exgcd)
首先由裴蜀定理判无解,若
设
方程两边同乘
故原方程的一组解为
接下来考虑构造通解形式,设
不难发现
考虑求解数与最值。找
由于式中的值均为整数,故
由于
于是
解数就是
至此本题在
在实现中,我们可以将
template <class T> void exgcd(T a, T b, T& x, T& y) {
if (b == 0) return x = 1, y = 0, void();
exgcd(b, a % b, y, x), y -= a / b * x;
}
long long a, b, c;
void _main() {
cin >> a >> b >> c;
long long d = __gcd(a, b);
if (c % d) return cout << -1 << '\n', void();
a /= d, b /= d, c /= d;
long long x0, y0; exgcd(a, b, x0, y0);
long long x1 = c * x0, y1 = c * y0;
int xmin = (x1 > 0 && x1 % b != 0) ? x1 % b : x1 % b + b;
int ymax = (c - xmin * a) / b;
int ymin = (y1 > 0 && y1 % a != 0) ? y1 % a : y1 % a + a;
int xmax = (c - ymin * b) / a;
if (xmax <= 0) cout << xmin << ' ' << ymin << '\n';
else cout << (ymax - ymin) / a + 1 << ' ' << xmin << ' ' << ymin << ' ' << xmax << ' ' << ymax << ' ' << '\n';
}
P1516 青蛙的约会
设相遇时两只青蛙跳了
即求解
template <class T> void exgcd(T a, T b, T& x, T& y) {
if (b == 0) return x = 1, y = 0, void();
exgcd(b, a % b, y, x), y -= a / b * x;
}
long long x, y, m, n, L;
void _main() {
cin >> x >> y >> m >> n >> L;
long long a = n - m, b = L, c = x - y;
if (a < 0) a = -a, c = -c;
long long d = __gcd(a, b);
if (c % d) return cout << "Impossible", void();
a /= d, b /= d, c /= d;
long long x0, y0; exgcd(a, b, x0, y0);
long long x1 = c * x0;
cout << ((x1 > 0 && x1 % b != 0) ? x1 % b : x1 % b + b);
}
P4777 【模板】扩展中国剩余定理(EXCRT)
解线性方程组的方法一般称作 CRT。套用上面的 exGCD 法即可。
const int N = 1e5 + 5;
template <class T> void exgcd(T a, T b, T& x, T& y) {
if (!b) return x = 1, y = 0, void();
exgcd(b, a % b, y, x), y -= a / b * x;
}
template <class T> T crt(int n, const T* a, const T* m) {
T x = 0, y = 0, p = m[1], res = a[1];
for (int i = 2; i <= n; i++) {
T a0 = p, b0 = m[i], c = (a[i] - res % b0 + b0) % b0, g = __gcd(a0, b0);
if (c % g) return -1;
exgcd(a0, b0, x, y);
x = (__int128) x * c / g % b0, res += x * p, p = p / __gcd(p, b0) * b0, res = (res % p + p) % p;
}
return res;
}
int n;
long long a[N], m[N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> m[i] >> a[i];
cout << crt(n, a, m);
}
P3868 [TJOI2009] 猜数字
对于
细节上注意 long long,不过上面板子里已经开了 __int128 了。
const int N = 15;
int n;
long long a[N], b[N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 1; i <= n; i++) a[i] = (a[i] % b[i] + b[i]) % b[i];
cout << crt(n, a, b);
}
欧拉函数
定义
欧拉函数
若由算术基本定理可将
由此有一个
inline int phi(int n) {
if (n == 1) return 1;
int res = n;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
res = res / i * (i - 1);
while (n % i == 0) n /= i;
}
}
if (n > 1) res = res / n * (n - 1);
return res;
}
性质
性质 1:若
性质 2:若
性质 3:
性质 4:对于
性质 5:若
筛法
使用性质 2 并结合线性筛法,可以在
int len, phi[N], prime[N];
bitset<N> isprime;
inline void eular(int n) {
phi[1] = 1, isprime.set(), isprime[0] = isprime[1] = false;
for (int i = 2; i <= n; i++) {
if (isprime[i]) prime[++len] = i, phi[i] = i - 1;
for (int j = 1; j <= len && i * prime[j] <= n; j++) {
isprime[i * prime[j]] = false;
if (i % prime[j] == 0) {
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
phi[i * prime[j]] = phi[i] * phi[prime[j]];
}
}
}
例题
SP4141 ETF - Euler Totient Function
线性筛欧拉函数板子。直接上代码:
const int N = 1e6 + 5;
void _main() {
eular(N - 1);
int t, x; cin >> t;
while (t--) cin >> x, cout << phi[x] << '\n';
}
UVA10179 Irreducable Basic Fractions
题意翻译:求
我们发现最简分数
P2158 [SDOI2008] 仪仗队
由样例图可以发现能看见的位置关于对角线对称,只需要考虑一个三角形即可。以左下角为原点
所求即为
这是因为
用线性筛筛出欧拉函数即可,复杂度
int n;
void _main() {
cin >> n;
if (n == 1) return cout << 0, 0;
int res = 1;
eular(40000);
for (int i = 1; i < n; i++) res += 2 * phi[i];
cout << res;
}
P2398 GCD SUM
这题做法很多,这里写一个欧拉函数做法。
可以发现
和上面那题是一样的,因为
然后我们用线性筛处理出
const int N = 1e5 + 5;
int n;
long long pre[N];
void _main() {
cin >> n;
eular(n);
for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + phi[i];
long long res = 0;
for (int i = 1; i <= n; i++) res += 1LL * i * (2 * pre[n / i] - 1);
cout << res;
}
双倍经验:P1390。这个题没有对称性,把系数
常用定理
费马小定理
若
变形式:
证明:取一个不为
考虑每一个
又有
欧拉定理
欧拉定理是费马小定理的扩展形式。若
推论 1:若
推论 2:若
扩展欧拉定理
扩展欧拉定理在 OI 中常用于对幂取模的情况。
例题
P4139 上帝与集合的正确用法
题里给的那个东西实际上是
由于指数塔是无限层,因此
long long p;
long long power(long long a, long long b, long long p) {
long long res = 1; for (a %= p; b; a = a * a % p, b >>= 1) {
if (b & 1) res = res * a % p;
} return res;
}
long long f(long long p) {return p == 1 ? 0 : power(2, f(phi[p]) + phi[p], p);}
void _main() { // 这里已经筛过phi[n]了
cin >> p; cout << f(p) << '\n';
}
P10496 The Luckiest Number
设答案为
设
因此,
由欧拉定理的推论 2,我们求出
long long power(long long a, long long b, long long p) {
long long res = 1; for (a %= p; b; a = (__int128) a * a % p, b >>= 1) {
if (b & 1) res = (__int128) res * a % p;
} return res;
}
vector<long long> factors(long long n) {
vector<long long> res;
for (long long i = 1; i * i <= n; i++) {
if (n % i) continue;
res.emplace_back(i);
if (n / i != i) res.emplace_back(n / i);
}
sort(res.begin(), res.end());
return res;
}
long long n;
void _main() {
for (int kase = 1; ; kase++) {
cin >> n;
if (n == 0) break;
cout << "Case " << kase << ": ";
n = 9 * n / __gcd(n, 8LL);
if (__gcd(n, 10LL) != 1) {cout << "0\n"; continue;}
vector<long long> f = factors(phi(n));
for (long long x : f) {
if (power(10, x, n) == 1) {cout << x << '\n'; break;}
}
}
}
数论分块
数论分块用于快速计算形如
的式子,其中
原理
以函数
我们注意到对于每个固定的
证明:设
因此,
板子
template <class F_t, class G_t>
long long sqrt_decomposition(long long n, long long k, F_t f, G_t g) {
long long res = 0;
for (long long l = 1, r = 0; l <= n; l = r + 1) {
r = (k / l) ? min(n, (k / (k / l))) : n;
res += f(r, l - 1) * g(k / l);
}
return res;
}
这个板子第一个参数传入
例题
UVA11526 H(n)
【模板】数论分块。
int n;
void _main() {
cin >> n;
cout << sqrt_decomposition(n, n,
[](long long x, long long y) {return x - y;}, // f(x) = 1
[](long long x) {return x;} // g(x) = x
) << '\n';
}
P2261 [CQOI2007] 余数求和
首先有一个结论是
后面这个东西可以数论分块来做。
long long n, k;
void _main() {
cin >> n >> k;
cout << n * k - sqrt_decomposition(n, k,
[](long long x, long long y) {return (x - y) * (x + y + 1) / 2;}, // f(x) = x
[](long long x) {return x;} // g(x) = x
);
}
P2260 [清华集训 2012] 模积和
和上面的题很像。
推柿子:
最后这个
另外本题一堆取模,笔者用了自己写的 modint。
long long n, m;
modint pre(modint n) {return n * (n + 1) * (n * 2 + 1) / modint(6);}
modint k1() { // 这里没法套板子,自己写一个
modint res = 0;
for (long long l = 1, r = 0; l <= min(n, m); l = r + 1) {
r = min(n / (n / l), m / (m / l));
res += modint(n / l) * modint(m / l) * (pre(r) - pre(l - 1));
}
return res;
}
void _main() {
cin >> n >> m; long long k = min(n, m);
modint n0 = n, m0 = m;
modint n1 = n0 * n0 - sqrt_decomposition(n, n,
[](modint x, modint y) {return (x - y) * (x + y + 1) / 2;},
[](long long x) {return x;}
);
modint m1 = m0 * m0 - sqrt_decomposition(m, m,
[](modint x, modint y) {return (x - y) * (x + y + 1) / 2;},
[](long long x) {return x;}
);
modint n2 = sqrt_decomposition(k, n,
[](modint x, modint y) {return (x - y) * (x + y + 1) / 2;},
[&](long long x) {return m0 * x;}
);
modint m2 = sqrt_decomposition(k, m,
[](modint x, modint y) {return (x - y) * (x + y + 1) / 2;},
[&](long long x) {return n0 * x;}
);
cout << n1 * m1 - n0 * m0 * k + n2 + m2 - k1();
}
P2424 约数和
首先求
然后套板子去求即可。
long long g(long long n) {
if (n <= 1) return n;
return sqrt_decomposition(n, n,
[](long long x, long long y) {return (x - y) * (x + y + 1) / 2;},
[](long long x) {return x;}
);
}
long long l, r;
void _main() {
cin >> l >> r;
cout << g(r) - g(l - 1);
}