数论
数论 —— 解题能力特训营
式子真的好难写
费马小定理
若 \(p\) 是一个质数, 且 \(a\) 不是 \(p\) 的倍数。
这个定理一般不用,因为太简单。
欧拉定理
\(a,p\) 互质,
欧拉函数:\(\phi(p)\) 表示的是小于等于 \(p\) 中和 \(p\) 互质的数的个数。
单个欧拉函数的求解, 其中 \(s_i\) 为 \(p\) 的质因数。
如果数 \(x,y\) 互质, 则可以 \(\phi(xy)=\phi(x)\phi(y)\)。
常用于对指数取模。
举个例子:\(5 ^ 4 \equiv 1 (\mod 12)\)
\(5 ^ {40001} = (5^4)^{10000} \times 5 \equiv 1^{10000} \times 5(\mod 12)\)
拓展欧拉定理:
第三个式子似乎与第一个式子的区别,就是为了应对 $ \gcd(a, p) \not= 1, b >= \phi(p)$的情况。
(手玩一下:将\(6^6 \mod 6\) 分别代入式子1和式子3)
欧拉线性筛:
const int maxn = 1e7+5;
int prime[maxn]; //素数表
int phi[maxn]; // phi[]数组就是1~maxn的欧拉函数值
int visit[maxn];
void ol()
{
int cnt = 0;
for(int i=2;i<=maxn;i++)
{
if(!visit[i])
{
prime[++cnt]=i;
phi[i]=i-1;//质数直接能求出
}
for(int j=1;j<=cnt&&prime[j]*i<=maxn;j++)
{
visit[i*prime[j]] = 1;
if(i%prime[j]==0)//px不互质
{
phi[i*prime[j]]=phi[i]*prime[j];
break;
}
else phi[i*prime[j]]=phi[i]*(prime[j]-1);//px互质
}
}
}
[BZOJ 3884] 上帝与集合的正确用法
递归调用, 用扩展欧拉定理。
对指数取模, 利用扩展欧拉定理。
每次都将 2 的指数看做一个整体,会发现这是一个递归结构,
我们递归调用,当 $\phi(p) $ 为 \(1\) 时,直接返回答案,因为任何数\(\mod 1\) 都是原数。
每进行两次递归,\(p\) 至少会减半,所以只会递归 \(log(p)\)。
/*
Date:2021.8.21
Source:luogu 4139
konwledge:扩展欧拉定理
*/
#include <iostream>
#include <cstdio>
#define orz cout << "AK IOI"
using namespace std;
const int maxn = 1e7 + 10;
inline int read()
{
int f = 0, x = 0; char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
inline void print(int X)
{
if(X < 0) {X = ~(X - 1); putchar('-');}
if(X > 9) print(X / 10);
putchar(X % 10 + '0');
}
int T, mod;
int cnt, vis[maxn], phi[maxn], prime[maxn];
void ol()
{
prime[1] = 1, phi[1] = 1;
for(int i = 2; i <= maxn; i++)
{
if(!vis[i]) {prime[++cnt] = i; phi[i] = i - 1;}
for(int j = 1; i * prime[j] <= maxn && j <= cnt; j++)
{
vis[i * prime[j]] = 1;
if(i % prime[j] == 0)
{
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
else phi[i * prime[j]] = phi[i] * (prime[j] - 1);
}
}
}
int pow(long long a, int n, int mod)
{
long long ans = 1;
while(n)
{
if(n & 1) ans = ans * a % mod;
a = a * a % mod;
n >>= 1;
}
return ans;
}
int solve(int mod)
{
if(mod == 1) return 0;
return pow(2, solve(phi[mod]) + phi[mod], mod);
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
T = read();
ol();
while(T--)
{
mod = read();
printf("%d\n", solve(mod));
}
return 0;
}
互质的数之和
给你 \(n\),需要求出 \([1,n]\) 中与 \(n\) 互质的数的和。
\(n\le 10^{14}\)
题解:
由于 \(\gcd(i,n)=\gcd(n-i,n)\)
把与 \(n\) 互质的数放在一起,发现我们总能找到两个和 \(n\) 互质的数相加等于 \(n\)。
发现答案为:\(n\times \frac{\phi(n)}{2}\)。
[BZOJ2190] 仪仗队
\(\sum_{1\le i,j\le N} [\gcd(i,j)=1]\)
\(1\le N\le 40000\)
题解:
$\phi $函数的前缀和, 乘以2。
/*
Date:
Source:luogu 2158
konwledge:求互质的数的对数 phi函数前缀和
*/
#include <cstdio>
#include <iostream>
#define orz cout << "AK IOI" <<"\n"
using namespace std;
const int maxn = 40010;
inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48);ch = getchar();}
return x * f;
}
int n, cnt, vis[maxn], phi[maxn], prime[maxn];
long long ans;
void ol()
{
phi[1] = 1, prime[1] = 1;
for(int i = 2; i <= maxn; i++)
{
if(!vis[i]){prime[++cnt] = i; phi[i] = i - 1;}
for(int j = 1; i * prime[j] <= maxn && j <= cnt; j++)
{
vis[i * prime[j]] = 1;
if(i % prime[j] == 0)//只被最小的筛
{
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
else phi[i * prime[j]] = phi[i] * (prime[j] - 1);
}
}
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
n = read();
if(n == 1) {puts("0"); return 0;}
ol();
for(int i = 1; i <= n - 1; i++) ans += 1ll * phi[i];
printf("%lld", ans * 2 + 1);
return 0;
}
Prime Swaps
给出长度为 \(N\) 的排列,每次你可以选择两个位置 \(i,j\) 并交换上边的数,前提是 \(j - i + 1\) 是质数。
要求在 \(5N\) 次操作内,把这个序列排号,输出具体排列的操作。
题解:
哥德巴赫猜想: 任意大于二的偶数,都可表示成两个素数之和。
那么我们每次放心的移动就好了!
那我们贪心的按照 \(1, 2, 3, 4\) …… 的顺序来排,(就是让 \(1\) 到第一位置, \(2\) 到第二位置),哥德巴赫猜想保证了我们每次都可以移得动。
每次我们把一个数往前移动 \(x\) 距离时,每步先移动一个尽量大的质数距离即可。
然后由于质数密度是 \(ln(n)\),所以可以保证在 \(5N\) 次一定可以完成。
代码中预处理哪些数是质数,以及每个数比它小的最大质数是多少。
/*
Date:2021.8.21
Source:CF432C
konwledge:哥德巴赫猜想:任意大于二的偶数,都可表示成两个素数之和。
*/
#include <iostream>
#include <cstdio>
#include <vector>
#include <map>
#define orz cout << "AK IOI"
using namespace std;
const int maxn = 1e5 + 10;
inline int read()
{
int f = 0, x = 0; char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
inline void print(int X)
{
if(X < 0) {X = ~(X - 1); putchar('-');}
if(X > 9) print(X / 10);
putchar(X % 10 + '0');
}
int n, a[maxn];
map<int, int> m;
int cnt, flag[maxn];
bool vis[maxn], prime[maxn];
vector<pair<int, int> > ans;
void init()
{
prime[1] = 1;
for(int i = 2; i <= maxn; i++)
{
if(!vis[i])
{
prime[i] = 1;
for(int j = i; j <= maxn; j += i) vis[j] = 1;
}
}
for(int i = 2; i <= maxn; i++)
if(prime[i]) flag[i] = i;
else flag[i] = flag[i - 1];
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
n = read();
init();
for(int i = 1; i <= n; i++)
a[i] = read(), m[a[i]] = i;
for(int i = 1; i <= n; i++)
{
while(m[i] != i)
{
int t = m[i] - i + 1; //需要移动的距离
int step = m[i] - flag[t] + 1; //flag[t] 小于需要移动的距离的最大的质数
ans.push_back(make_pair(step, m[i]));
m[a[step]] = m[i]; //交换
swap(a[m[i]], a[step]);
m[i] = step;
}
}
printf("%d\n", ans.size());
for(int i = 0; i < ans.size(); i++)
printf("%d %d\n", ans[i].first, ans[i].second);
return 0;
}
GCD
求\(\sum _{1<= i, j <= n}[gcd(i,j) \ is \ prime]\)。
题解:
对于每一个质数,对答案的贡献是 \(\sum _{1 <= i, j, <= \frac n p}[gcd(i, j) = 1]\)
/*
Date:2021.8.21
Source:luogu 2568
konwledge:同仪仗队 ,不过要枚举质数。
*/
#include <iostream>
#include <cstdio>
#define orz cout << "AK IOI"
#define int long long
using namespace std;
const int maxn = 1e7 + 10;
inline int read()
{
int f = 0, x = 0; char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
inline void print(int X)
{
if(X < 0) {X = ~(X - 1); putchar('-');}
if(X > 9) print(X / 10);
putchar(X % 10 + '0');
}
int n, ans, sum[maxn];
int cnt, vis[maxn], phi[maxn], prime[maxn];
void ol()
{
prime[1] = 1, phi[1] = 1;
for(int i = 2; i <= maxn; i++)
{
if(!vis[i]) {prime[++cnt] = i; phi[i] = i - 1;}
for(int j = 1; i * prime[j] <= maxn && j <= cnt; j++)
{
vis[i * prime[j]] = 1;
if(i % prime[j] == 0)
{
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
else phi[i * prime[j]] = phi[i] * (prime[j] - 1);
}
}
}
signed main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
n = read();
ol();
for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + phi[i];
for(int i = 1; i <= cnt && prime[i] <= n; i++)
ans += ((sum[n / prime[i]]) * 2) - 1;
printf("%lld", ans);
return 0;
}
longge的问题
求\(\sum _{1 <= i <= n} gcd(i, N)\)。
题解:
枚举所有的 d,然后用质因数分解求 \(\phi\)即可。
奇数国
题解:
建一棵线段树。
考虑\(\phi\)函数的定义,维护区间对应的乘积和乘积有哪些质因数。
用 long long 进行压位。
最大公约数与欧几里得算法
最小公倍数
例题:
操场是 \(n\) 个格子围城的圆形,
有 \(m\) 个同学,步长 \(a_i\),求不会经过的格子数。
题解:
明显答案为
最大公约数,最小公倍数性质
扩展欧几里得算法
void gcd(int a, int b, int &d, int &x, int &y)//扩展欧几里得
{
if(b == 0){d = a; x = 1; y = 0;}
else {gcd(b, a % b, d, y, x); y -= (a / b) * x;}
}
假如知道了一组解 \((x, y)\), 那么通解 \((X,Y)\) 是
中国剩余定理
int n;
int m[maxn], a[maxn];
void gcd(int a, int b, int &d, int &x, int &y)//扩展欧几里得
{
if(b == 0){d = a; x = 1; y = 0;}
else {gcd(b, a % b, d, y, x); y -= (a / b) * x;}
}
int china(int n, int *m, int *a)
{
int M = 1, d, y, x = 0;
for(int i = 1; i <= n; i++) M *= m[i];
for(int i = 1; i <= n; i++)
{
int w = M / m[i];
gcd(m[i], w, d, d, y);
x = (x + y * w * a[i]) % M;
}
return (x + M) % M;
}
signed main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
n = read();
for(int i = 1; i <= n; i++)
m[i] = read(), a[i] = read();
printf("%lld", china(n, m, a));
return 0;
}
整数分块
数论中用的很多的小技巧。
因为 \(\lfloor \frac ni \rfloor\) 只有 \(O(√n)\) 种不同的取值,遍历每一个\(\lfloor \frac ni \rfloor\) 相同的区间即可。
这个性质经常被用来优化复杂度,特别是在一些积性函数的题目中。
代码实现简单优雅,比如上述题目 \(f(a) = a^5, sum(i) = \frac {(1+i)i}2\) 。
for(int i = 1, last; i <= n; i = last + 1)
{
int a = n / i;
last = n / a;
ret += f(a) * (sum(last) - sum(i - 1));
}
卢卡斯定理
如果 \(p\) 是一个质数,有公式:
\(C^n_m ≡ C^{n\mod p}_{m\mod p} \times C^{\lfloor \frac n p⌋}_{⌊\frac mp⌋}(\mod p)\)
当组合数太大,需要取模的时候用到!