【学习笔记】初等数论
[学习笔记]初等数论
最大公约数
对于两个整数
欧几里得算法(辗转相除法):
代码:
int gcd(int a, int b){
return b ? gcd(b, a%b) : a;
}
或者直接使用 __gcd(a, b)
。
辗转相减法:
推广到
注意:因为
线性筛
欧拉筛。不多讲了。
int n;
int prime[N], cnt;
bool is_prime[N];
void getprime(){
for(int i=2; i<=n; i++) is_prime[i] = 1;
for(int i=2; i<=n; i++){
if(is_prime[i]){
prime[++cnt] = i;
}
for(int j=1; j<=cnt && i*prime[j]<=n; j++){
is_prime[i*prime[j]] = 0;
if(i%prime[j] == 0) break;
}
}
}
积性函数
数论函数:
数论函数指定义域为正整数的函数。数论函数也可以视作一个数列。
积性函数:
若一个数论函数
此外,如果对所有
性质:都可以通过筛法来快速求解。
常见的积性函数:
欧拉函数
表示
欧拉公式:记
性质:
莫比乌斯函数
性质:
单位函数
筛
从欧拉筛出发考虑。下面的
-
当
是质数时,显然 。 -
当
时,即 互质时,根据积性函数定义得: 。 -
当
时,即 是 的一个因子时。设 唯一分解中 的指数是 ,因为 ,因此 。解释:
-
对于
, 内有 个数与其互质。所以 内有 个数与其互质。 -
对于
。因为 ,又因为 与 互质,所以 。又因为 。所以将 和 两项拆开消元后即可得到 。也可通过欧拉公式(欧拉函数的定义)理解。
-
代码实现:
int n;
int prime[N], phi[N], cnt;
bool is_prime[N];
void getprime(){
phi[1] = 1;
for(int i=2; i<=n; i++) is_prime[i] = 1;
for(int i=2; i<=n; i++){
if(is_prime[i]){
phi[i] = i-1;
prime[++cnt] = i;
}
for(int j=1; j<=cnt && i*prime[j]<=n; j++){
is_prime[i*prime[j]] = 0;
if(i%prime[j] == 0){
phi[i*prime[j]] = prime[j]*phi[i];
break;
}
else phi[i*prime[j]] = (prime[j]-1)*phi[i];
}
}
}
筛
还是考虑三种情况。
- 当
是质数时,显然 。 - 当
时,即 互质时,根据积性函数定义得: 。 - 当
时,即 是 的一个因子时。根据该函数定义得 。
代码实现应该差不多。
P2568 GCD
首先我们枚举质数:
变形:
接下来改变
此时发现
可以前缀和求
时间复杂度:
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e7 + 5;
int prime[N], phi[N], cnt;
bool is_prime[N];
ll ans, sum[N];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n; cin>>n;
phi[1] = 1;
for(int i=2; i<=n; i++) is_prime[i] = 1;
for(int i=2; i<=n; i++){
if(is_prime[i]){
phi[i] = i-1;
prime[++cnt] = i;
}
for(int j=1; j<=cnt && i*prime[j]<=n; j++){
is_prime[i*prime[j]] = 0;
if(i%prime[j] == 0){
phi[i*prime[j]] = prime[j]*phi[i];
break;
}
else phi[i*prime[j]] = (prime[j]-1)*phi[i];
}
}
for(int i=1; i<=n; i++) sum[i] = sum[i-1]+phi[i];
for(int i=1; i<=cnt; i++) ans += 2*sum[n/prime[i]]-1;
cout<<ans;
return 0;
}
P10463 Interval GCD
辗转相减法(前置知识):
推广到
看到题意中把
令差分完之后的数组为
以下用线段树维护差分数组的
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e5+5;
#define lowbit(x) x&(-x)
#define ls(x) ((x)<<1)
#define rs(x) ((x)<<1|1)
#define gcd(x, y) abs(__gcd((x), (y)))
int n, m;
ll a[N], cha[N], c[N];
struct node{
int l, r;
ll v;
}tr[N<<2];
void add(int x, ll y){
while(x <= n){
c[x] += y;
x += lowbit(x);
}
}
ll sum(int x){
ll ret = 0;
while(x){
ret += c[x];
x -= lowbit(x);
}
return ret;
}
void pushup(int x){
tr[x].v = gcd(tr[ls(x)].v, tr[rs(x)].v);
}
void buildtr(int x, int l, int r){
tr[x].l = l, tr[x].r = r;
if(l == r){
tr[x].v = cha[l];
return;
}
int mid = (l+r) >> 1;
buildtr(ls(x), l, mid);
buildtr(rs(x), mid+1, r);
pushup(x);
}
void upd(int x, int tar, ll d){
if(tr[x].l > tar || tr[x].r < tar)
return;
if(tr[x].l == tr[x].r && tr[x].l == tar){
tr[x].v += d;
return;
}
int mid = (tr[x].l+tr[x].r) >> 1;
if(tar<=mid) upd(ls(x), tar, d);
upd(rs(x), tar, d);
pushup(x);
}
ll query(int x, int l, int r){
if(tr[x].l > r || tr[x].r < l)
return 0;
if(tr[x].l >= l && tr[x].r <= r)
return tr[x].v;
return gcd(query(ls(x), l, r), query(rs(x), l, r));
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=1; i<=n; i++){
cin>>a[i];
cha[i] = a[i] - a[i-1];
add(i, cha[i]);
}
buildtr(1, 1, n);
while(m--){
char op; int l, r; cin>>op>>l>>r;
if(op == 'C'){
ll d; cin>>d;
upd(1, l, d);
upd(1, r+1, -d);
add(l, d);
add(r+1, -d);
} else if(op == 'Q'){
cout<<gcd(sum(l), query(1, l+1, r))<<"\n";
}
}
return 0;
}
USACO08DEC Patting Heads S
从枚举倍数的角度思考:对于一个数
注意最后要减去一个对自己的贡献。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6+5;
int a[N], w[N], c[N];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int m = 0; // 值域
int n; cin>>n;
for(int i=1; i<=n; i++){
cin>>a[i];
c[a[i]]++;
m = max(m, a[i]);
}
for(int i=1; i<=m; i++){
for(int j=i; j<=m; j+=i){
w[j] += c[i];
}
}
for(int i=1; i<=n; i++)
cout<<w[a[i]]-1<<"\n"; // 减去自己对自己的贡献
return 0;
}
Divan and Kostomuksha (easy version)
考虑
首先计算出序列
接下来考虑转移,假设是从
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e6;
ll cnt[N+5], a[N+5];
ll dp[N+5];
//cnt 表示 a 中能被 i 整除的数的个数
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n, m = 0; cin>>n; // m 是值域
for(int i=1; i<=n; i++){
cin>>a[i];
cnt[a[i]]++;
m = max(m, a[i]);
}
for(int i=1; i<=m; i++){
for(int j=i*2; j<=m; j+=i){
cnt[i] += cnt[j];
}
}
ll ans = 0;
for(int i=m; i>=1; i--){
dp[i] = cnt[i]*i;
for(int j=i*2; j<=m; j+=i){
dp[i] = max(dp[i], dp[j]+1ll*(cnt[i]-cnt[j])*i);
}
ans = max(ans, dp[i]);
}
cout<<ans;
return 0;
}
费马小定理
若
所以可以求得
裴蜀定理
设
简单来说,我们设
扩展欧几里得算法(Exgcd)
应用:
- 求解不定方程。
- 求解模意义下的逆元。
- 求解线性同余方程。
求解
设当前的式子为
根据欧几里得算法,
所以得到
当
求解二元一次不定方程
根据裴蜀定理得:
若
过程:
-
用
的特解来求 的特解。设
,已知 ,两边同乘 ,得 ,所以 就是原方程的一组解。 -
用这组特解
找出方程所有的解。设
,展开得 ,所以只需要让 。继续设
,我们让 ,即可成立。所以只要任取一个
,就可以算出相应的 ,进而推出 ,即可得到通解。
求解线性同余方程
以下内容摘自OI_wiki。
定理 1:线性同余方程
(注:
其中
应用扩展欧几里德算法可以求解该线性不定方程。根据定理 1,对于线性不定方程
于是找到方程的一个解。
定理 2:若
并且对任意整数
根据定理 2,可以从已求出的一个解,求出方程的所有解。实际问题中,往往要求出一个最小整数解,也就是一个特解
其中有
如果仔细考虑,用扩展欧几里得算法求解与用逆元求解,两种方法是等价的。
P5656 【模板】二元一次不定方程 (exgcd)
在上述求解二元一次不定方程的基础上还要增加第三步:考虑解的最大或最小值。
令
可以看出,当
假设任选的
若限制
若限制
因为
- 对不等式左侧解释,因为
,而 一定为整数,所以分式分子要 ,排除 刚好为整数的可能,右侧同理。
若
- 此时,答案即为没有正整数解但有整数解,
为正整数且最小即为 时 的值, 为正整数且最小即为 时 的值。
否则就有正整数解,正整数解个数即为
- 此时
的最大值和 的最小值都是在 最大时取到,而
的最小值和 的最大值都是在 最小时取到,直接计算即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll x, y;
ll exgcd(ll a, ll b){
if(b == 0){
x = 1, y = 0;
return a;
}
ll d = exgcd(b, a % b);
ll t = x;
x = y;
y = t - a/b * y;
return d;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int T; cin>>T;
while(T--){
ll a, b, c; cin>>a>>b>>c;
ll d = exgcd(a, b);
if(c % d != 0){
cout<<"-1\n";
continue;
}
x = x*c/d, y = y*c/d;
ll dx = b/d, dy = a/d;
ll down = ceil((-x+1)*1.0/dx), up = floor((y-1)*1.0/dy);
if(down > up){
cout<<x+down*dx<<" "<<y-up*dy<<"\n";
continue;
}
cout<<up-down+1<<" "<<x+down*dx<<" "<<y-up*dy<<" "<<x+up*dx<<" "<<y-down*dy<<"\n";
}
return 0;
}
中国剩余定理(CRT)
以下内容大部分摘自OI_wiki。
中国剩余定理可求解如下形式的一元线性同余方程组(其中
求解过程:
-
计算所有模数的积
。 -
对于第
个方程:a. 计算
;b. 计算
在模 意义下的逆元 ;c. 计算
(不要对 取模)。 -
方程组在模
意义下的唯一解为: 。
证明:
我们需要证明上面算法计算所得的
枚举
即对于任意
因为我们没有对输入的
另外,若
故系数列表
P1495 【模板】中国剩余定理(CRT)
板子。注意求逆元方式。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n;
int a[100005], b[100005];
ll sum = 1, x, y;
ll ans;
void exgcd(ll a, ll b){
if(b == 0){
x = 1, y = 0;
return;
}
exgcd(b, a%b);
ll z = x;
x = y, y = z - a/b * y;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n;
for(int i=1; i<=n; i++){
cin>>a[i]>>b[i];
sum *= a[i];
}
for(int i=1; i<=n; i++){
ll m = sum/a[i];
x = 0, y = 0;
exgcd(m, a[i]);
ans = (ans + (__int128)b[i]*m*(x<0 ? x+a[i] : x)) % sum;
}
cout<<ans;
return 0;
}
扩展中国剩余定理(ExCRT)
给定
先考虑只有两个方程的情况。设两个方程分别是
将它们转化为不定方程:
由裴蜀定理,当
其他情况下,可以通过扩展欧几里得算法解出来一组可行解
则原来的两方程组成的模方程组的解为
多个方程用上面的方法两两合并即可,相当于跑
P4777 【模板】扩展中国剩余定理(EXCRT)
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define i128 __int128
i128 x, y;
i128 a1, a2, m1, m2;
i128 exgcd(i128 a, i128 b){
if(b == 0){
x = 1, y = 0;
return a;
}
i128 d = exgcd(b, a % b);
i128 t = x;
x = y, y = t - a/b * y;
return d;
}
void merge(){
i128 c = a2 - a1;
i128 d = exgcd(m1, m2);
if(c % d != 0){
cout<<"-1";
exit(0);
}
i128 M = m1 * m2 / d;
x = (x * c / d % (m2 / d) + (m2 / d)) % (m2 / d);
a1 = ((m1 * x + a1) % M + M) % M;
m1 = M;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n; cin>>n;
for(int i=1; i<=n; i++){
ll A_, B_; cin>>A_>>B_; // A 是模数
m2 = A_, a2 = B_;
if(i != 1) merge();
else m1 = A_, a1 = B_;
}
cout<<(ll)a1;
return 0;
}
同余最短路
当出现形如「给定
同余最短路利用同余来构造一些状态,可以达到优化空间复杂度的目的。
类比差分约束方法,利用同余构造的这些状态可以看作单源最短路中的点。同余最短路的状态转移通常是这样的
P3403 跳楼机
题目大意:给定
不妨假设
首先利用同余的方式分类,即将所有楼层模
因此,我们只需要求出每类楼层中最低可到达的楼层,即可求出这一类中有几层可达。对于每一类楼层都用一个点来代替,比如点
以点
接下来在图中连边,点
上述两种边分别代表通过向上跳
用 Dijkstra 算法(SPFA 也可)即可求出每类楼层中最低可达的楼层(也就是最短距离),最后再统计答案即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll unsigned long long
ll h, dis[100005], ans;
int x, y, z;
struct node{
int fi; ll se;
friend bool operator<(node a, node b){
return a.se > b.se;
}
};
vector<node> g[100005];
priority_queue<node> q;
bool vis[100005];
void dij(){
for(int i=1; i<x; i++)
dis[i] = INT64_MAX;
dis[0] = 0;
q.push({0, 0});
while(!q.empty()){
node t = q.top(); q.pop();
if(vis[t.fi]) continue;
vis[t.fi] = true;
for(auto p : g[t.fi]){
if(dis[t.fi]+p.se < dis[p.fi]){
dis[p.fi] = dis[t.fi]+p.se;
q.push({p.fi, dis[p.fi]});
}
}
}
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>h>>x>>y>>z;
h--;
for(int i=0; i<x; i++){
g[i].push_back({(i+y)%x, y});
g[i].push_back({(i+z)%x, z});
}
dij();
for(int i=0; i<x; i++){
if(h >= dis[i])
ans += (h - dis[i]) / x + 1;
}
cout<<ans;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具