【讲课】基础的数论知识
翻车是必然的
gcd
口胡吧。。。
inline int gcd (int a, int b) {
return b ? gcd (b, a % b) : a;
}
exgcd
先裴蜀
x,y∈Z+x,y∈Z+, 使得 ax+by=max+by=m 成立的充要条件是 gcd(a,b)|mgcd(a,b)|m
之前讲的有 gcd(a,b)=gcd(b,b mod a)gcd(a,b)=gcd(b,b mod a) 来找。。。
继续设 s=gcd(a,b)s=gcd(a,b)
假如真就有解
明显就有 s|ax, s|bys|ax, s|by
然后证明就完了
放在exgcd里
只考虑 x,y∈Z+ ax+by=gcd(a,b)x,y∈Z+ ax+by=gcd(a,b)
当 b=0b=0 时,gcd(a,b)=agcd(a,b)=a , 答案很显然了
b≠0b≠0 时,
就很显然的得出
x=y2,y=x2−a/b∗y2x=y2,y=x2−a/b∗y2
inline int exgcd (int &x, int &y, int a, int b) { // 取地址为了方便随时修改
if (!b) {
x = 1, y = 0;
return a;
}
int gcd = exgcd (x, y, b, a % b);
int t = x;
x = y;
y = t - a / b * y;
return gcd;
} // 顺便处理了一下gcd
放个题
由此得出最后三条新关系式,我们暴力枚举 k , s ,从而算出整数 t (有可能不是整数),判断 gcd(t,s)=1gcd(t,s)=1,带入得到 x,yx,y ,符合题意就 ans+=8 (四个象限,(x,y)(y,x)算两个)。
最后 ans+=4(r为整数,坐标轴还有4个)
#include <bits/stdc++.h>
#define ll long long
using namespace std;
template <typename T>
inline void read (T &a) {
T x = 0, f = 1;
char ch = getchar ();
while (! isdigit (ch)) {
if (ch == '-') f = 0;
ch = getchar ();
}
while (isdigit (ch)) {
x = (x << 1) + (x << 3) + (ch ^ '0');
ch = getchar ();
}
a = f ? x : -x;
}
inline ll gcd (ll a, ll b) {
if (!b) return a;
return gcd (b, a % b);
}
ll r, ans, x, y;
inline void blanc (ll k) {
for (ll s = 1, t; s * s <= r / k; s++) {
t = sqrt (r / k - s * s);
if (s * s + t * t == r / k) {
if (gcd (s, t) == 1) {
x = k * s * t;
y = (t * t - s * s) / 2 * k;
if (x > 0 and y > 0 and x * x + y * y == r / 2 * (r / 2)) {
// 只考虑一象限,判断整数和是否满足条件,r先除后乘防爆ll
ans += 8;
}
}
}
}
}
signed main () {
read (r);
r *= 2;
for (ll k = 1; k * k <= r; k++) {
if (! (r % k)) { // 优先级!优先级!!!!!
blanc (k);
if (r / k != k) blanc (r / k);
}
}
printf ("%lld", ans + 4);
}
再放一个
讲完逆元再讲,记得提醒我
#include <bits/stdc++.h>
#define mod 19260817
#define ll long long
using namespace std;
template <typename T>
inline void read (T &a) {
T x = 0, f = 1;
char ch = getchar ();
while (! isdigit (ch)) {
if (ch == '-') f = 0;
ch = getchar ();
}
while (isdigit (ch)) {
x = (x << 1) + (x << 3) + (ch ^ '0');
x %= mod;
ch = getchar ();
}
a = f ? x : -x;
}
ll x, y;
inline void exgcd (ll &x, ll &y, ll a, ll b) {
if (!b) {
x = 1, y = 0;
return ;
}
exgcd (x, y, b, a % b);
ll t = x;
x = y;
y = t - a / b * y;
}
ll a, b;
signed main() {
read (a);
read (b);
if (b) {
exgcd(x, y, b, mod);
x = (x % mod + mod) % mod;
printf("%lld\n", a * x % mod);
return 0;
}
cout << "Angry!" << endl;
}
素数
- 这一栏 copy 自大半年前的课件
素数是啥不用说
这里说一说怎么找素数,也就是素数筛法
最朴素的
int pri[MAXN];
int cnt;
inline void prime (int n) {
cnt = 0;
pri[++cnt] = 2;
int i,j;
for (i = 3; i <= n; ++i) {
for (j = 2; j < i; ++j) {
if (i % j == 0)
break;
}
if (j >= i)
pri[++cnt] = i;
}
}
// 如你所见,就是纯暴力
过于垃圾,无法接受,直接pass
好一点的
埃氏筛
埃式筛法的基本思想就是,当我们遍历到一个素数时,把所有该素数的倍数都筛选出来。
int cnt = 0;
int pri[20000];
bool v[20000];
int n;
inline void prime () {
read (n);
for (int i = 2; i <= n; ++i) {
if (! v[i]) {
pri[++cnt] = i;
for (int j = i; j <= n; j += i) {
v[j] = true;
}
}
}
for (int i = 1; i <= cnt; i++) cout << pri[i] << " ";
}
埃式筛法很容易理解,并且在效率上也比较优秀,时间复杂度为 O(N logN)O(N logN),
埃氏筛在筛选时有重复,或许我们可以通过某种方法避免这种重复
目前最好的
欧拉筛
int prime[MAXN];//它存的是最小素因子
bool vis[MAXN];
inline void phigros (int n) {
for (int i = 2; i <= n; ++i) {
if (! vis[i]) {
prime[++prime[0]] = i;
} // 判断素数
for (int j = 1; j < prime[0] and i * prime[j] < n; j++) {
vis[i * prime[j]] = true;//筛数
if(i % prime[j] == 0)//时间复杂度为O(n)的关键!
break;
}
}
}
为什么 i % prime[j] == 0 就break?
当 i是prime[j]的倍数时,i = k * prime[j],如果继续运算 prime[j + 1],
i * prime[j + 1] = prime[j] * (k * prime[j + 1]),
这里prime[j]是最小的素因子,
当i = k * prime[j+1]时,同样会在里循环中运算到,会重复,所以才跳出循环
线性筛还有其他用法
线性筛
筛素数
bool vis[N];
int pri[N], cnt;
inline void OSU () {
for (int i = 2; i <= n; i++) {
if (! vis[i]) pri[++cnt] = i;
for (int j = 1; j <= cnt and i * pri[j] <= N; i++) {
vis[i * pri[j]] = 1;
if (i % pri[j] == 0) break;
}
}
}
筛欧拉函数
容斥证明以下式子:
φφ 积性函数证明:
线性筛 φφ:
-
(i,pri[j])=1(i,pri[j])=1 过于简单,不予讨论
-
ii 为质数,过于简单,不予讨论
-
(i,pri[j])=pri[j]φ(i)=i∏k=1(1−1pk)pri[j]∈pφ(i∗pri[j])=i∗pri[j]∏k=1(1−1pk)=φ(i)∗pri[j](i,pri[j])=pri[j]φ(i)=i∏k=1(1−1pk)pri[j]∈pφ(i∗pri[j])=i∗pri[j]∏k=1(1−1pk)=φ(i)∗pri[j]
bool vis[N];
int pri[N], cnt;
int phi[N];
inline void Arcaea () {
for (int i = 2; i <= n; i++) {
if (! vis[i]) pri[++cnt] = i, phi[i] = i - 1;
for (int j = 1; j <= cnt and i * pri[j] <= N; i++) {
vis[i * pri[j]] = 1;
if (i % pri[j] == 0) {
phi[i * pri[j]] = phi[i] * pri[j];
break;
}
phi[i * pri[j]] = phi[i] * (pri[j] - 1); // 互质,积性函数
}
}
}
筛约数个数和,约数和
个数
d(n)d(n) 为 nn 的约数个数
num(n)num(n) 为 nn 的最小质因子个数
因为 pripri 从小到大枚举,所以 pri[j]pri[j] 一定是 i×pri[j]i×pri[j] 的最小质因子,毕竟线性筛的原理就在于此。
唯一分解定理:
每个 pipi 都可选择 [0,ki][0,ki] 个,彼此相乘,组成新约数。
故 nn 的约数个数为:
-
ii 是质数,答案显然
-
i%pri[j]!=0i%pri[j]!=0 相当于新加了一个质因子
d(i×pri[j])=d(i)×d(pri[j])=2×d(i)num(i×pri[j])=1d(i×pri[j])=d(i)×d(pri[j])=2×d(i)num(i×pri[j])=1 -
i%pri[j]==0i%pri[j]==0 之前出现过
d(i×pri[j])=(1+k1+1)∏i=2(ki+1)d(i×pri[j])=(1+k1+1)∏i=2(ki+1)之前的 numnum 起到了作用。
d(i×pri[j])=d(i)/(num(i)+1)×(num(i)+2)d(i×pri[j])=d(i)/(num(i)+1)×(num(i)+2)
约数和
sd(n)sd(n) 表示 nn 的约数和(不是质因子和
num(i)num(i) 表示
根据算数基本定理有:
记录最小质因子那一项,即 (1+p1+p21+⋯+pr11)(1+p1+p21+⋯+pr11)。
用 num(n)num(n) 表示。
-
ii 是质数
sd(i)=i+1num(i)=i+1sd(i)=i+1num(i)=i+1 -
i%pri[j]!=0i%pri[j]!=0 显然
sd(i∗pri[j])=sd(i)∗sd(pri[j])num(i∗pri[j])=pri[j]+1sd(i∗pri[j])=sd(i)∗sd(pri[j])num(i∗pri[j])=pri[j]+1 -
i%pri[j]==0i%pri[j]==0 显然 num(i∗pri[j])=num(i)∗pri[j]+1num(i∗pri[j])=num(i)∗pri[j]+1
sd(i∗pri[j])=sd(i)/num(i)∗num(i∗pri[j])sd(i∗pri[j])=sd(i)/num(i)∗num(i∗pri[j])
代码略。。。(懒得打了
筛莫比乌斯函数
由定义式:
知,
- ii 为质数,mu(i)=−1mu(i)=−1.
- i%pri[j]==0i%pri[j]==0,mu(i∗pri[j])=0mu(i∗pri[j])=0.
- 否则 mu(i∗pri[j])=−mu(i)mu(i∗pri[j])=−mu(i).
莫得代码
米勒拉宾检验,这里不讲
CRT
求解同余方程组
举例
一个数 nn ,除以3余2,除以5余3,除以7余2,求 nn。
m1,m2,m3m1,m2,m3两两互质 求共解
先不找最小解
考虑
n=n1+n2+n3n=n1+n2+n3 即是一个解
n mod lcm(m1,m2,m3)n mod lcm(m1,m2,m3) 就是最小解
问题是咋求出来满足条件的 n1,n2,n3n1,n2,n3?
用逆元(inv)
举例
EXCRT
CRT 是 EXCRT 的特殊情况。
模数之间再无任何瓜葛
嘶~最后的式子有些眼熟啊,像不像内个,对,就是内个!ax+by=cax+by=c
如果 g∤cg∤c 的话。。。那就无解。-_-
有解,求 k2×m2+k1×m1=gk2×m2+k1×m1=g 的 k1k1。
因为 g∣cg∣c,所以 k1∗=(c/g)k1∗=(c/g)。
防止爆炸,k1%=m2k1%=m2。
于是可以反推 xx。
但我想各位已经发觉到了,原方程式是 k2×m2−k1×m1=ck2×m2−k1×m1=c。
于是 x=−k1×m1+a1x=−k1×m1+a1,我们暂且设这个 xx 为 x0x0。
通解就是 x=x0+k×lcm(m1,m2)x=x0+k×lcm(m1,m2)。
于是这两个同余方程合并成了一个:
x=x0 (mod lcm(m1,m2))x=x0 (mod lcm(m1,m2))。
其他方程间以此类推。
最后剩下一个方程,他的 x0x0 的最小非负整数解,就是最终答案。
例题:P4777 ~~这题怎么是个紫的?快降蓝啊(doge
#include <bits/stdc++.h>
#define int long long
#define N 1000005
using namespace std;
template <typename T>
inline void read (T &a) {
T x = 0, f = 1;
char ch = getchar ();
while (! isdigit (ch)) {
(ch == '-') and (f = 0);
ch = getchar ();
}
while (isdigit (ch)) {
x = (x << 1) + (x << 3) + (ch ^ '0');
ch = getchar ();
}
a = f ? x : -x;
}
int n, x, y, A, B, C;
int m[N], a[N], ans;
inline int qmul(int a, int b, int mo) {
int ans = 0, base = a;
while (b) {
if (b & 1) {
ans = (ans + base) % mo;
}
base = (base + base) % mo;
b >>= 1;
}
return ans;
} // 龟速乘(不是快速幂)目的是防止两数相乘的时候溢出爆炸
inline int exgcd(int a, int b, int &x, int &y) {
if (!b) {
x = 1;
y = 0;
return a;
}
int g = exgcd(b, a % b, x, y);
int tx = x;
x = y;
y = tx - (a / b) * y;
return g;
} // 不多言,都知道
inline void excrt() {
for (int i = 2; i <= n; i++) {
A = m[1], B = m[i], C = a[i] - a[1];
C = (C % B + B) % B;
// 防止 C 是个负的,用 B 是因为在同余式中 B 是模数
int g = exgcd(A, B, x, y);
x = qmul(x, (C / g), B); // 这里的 x 就是博客里的 k1
x = (x % B + B) % B;
// k1 不能是个负的,负的难算还可能会错
a[1] = a[1] + qmul(m[1], x, m[1] * (m[i] / g));
// 原博客 x = -k1 * m1 + a1
m[1] = m[1] * (m[i] / g); // 模数变为 lcm (m1, m2)
a[1] = (a[1] % m[1] + m[1]) % m[1]; // 防爆
}
}
signed main() {
read (n);
for (int i = 1; i <= n; i++) {
read (m[i]), read (a[i]);
}
excrt();
printf("%lld", a[1]);
}
逆元
扩欧
最常用的,经常在各大题解里见到。。。(比如上面那篇代码
a∗inv(a)=1(mod p)a∗inv(a)=1(mod p)
根据 exgcd 可以变形成 a∗inv(a)+k∗p=1a∗inv(a)+k∗p=1
这说明只有 a 和 p 互质才存在逆元
inline int exgcd (int &x, int &y, int a, int b) { // 取地址为了方便随时修改
if (!b) {
x = 1, y = 0;
return a;
}
int gcd = exgcd (x, y, b, a % b);
int t = x;
x = y;
y = t - a / b * y;
return gcd;
} // 顺便处理了一下gcd
inline long long inv (int a, int p) {
long long x, y;
long long d = exgcd (x, y, a, p);
return d == 1 ? (x % p + p) % p : -1;
}
欧拉
这个比扩欧快一点,但不指望你们掌握,(下边有更好的)
aφ(p)−1 即是。
inline ll phi (int p) {
int a, b;
a = b = p;
for (int i = 2; i * i <= a; ++i) { // 根号就够了
if (a % i == 0) {
b -= b / i; // 等价于 b = b * (1 - 1 / i), 精度问题
while (a % i == 0) {
a /= i; // 把这几个质因子全去掉。。。
}
}
}
if (a > 1) { // 应对素数情况 , 同时将最后一个没有因数用掉
b = b - b / a;
}
return b;
}
inline ll fpow (ll a, ll p, ll mod) {
ll ans = 1, cnt = a % mod;
while (p) {
if (p & 1) ans = ans * cnt % mod;
cnt = cnt * cnt % mod;
p >>= 1;
}
return ans;
}
inline ll inv (ll a, ll p) {
return fpow (a, phi (p), p);
}
递推
线性
套用小绿书上的
p 是模数, i 为待求逆元的数, 我们现在求 i−1 在 mod p 意义下的值
i−1 就是 inv[i] ,
ll inv[p+5];
inline void Inv (ll p) {
inv[1] = 1;
for (int i = 2; i < p; i++) {
inv[i] = (p - p / i) * inv[p % i] % p;
// (p - p / i) 在 mod p 意义下等价于 -(p/i);
}
}
适用于 p 是不大的素数且逆元被多次调用
预处理 O(p),单次查询 O(1)。
递归
把上面的 for 变一下
inline ll inv (int i) {
if (i == 1) return 1;
return (p - p / i) * inv (p % i) % p;
}
单次查询 O(log p).
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ASP.NET Core 模型验证消息的本地化新姿势
· 对象命名为何需要避免'-er'和'-or'后缀
· 开发的设计和重构,为开发效率服务
· 从零开始开发一个 MCP Server!
· Ai满嘴顺口溜,想考研?浪费我几个小时
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想