数论,是一门研究整数的纯数学的分支,而整数的基本元素是素数(也称质数),所以数论的本质是对素数性质的研究。
素数
一些基本概念就不写了.
注意两点:
- 0、1、负整数既不是素数也不是合数.
- 表示不大于 的素数个数, 有如下近似关系 .
素数判定
如何判定正整数 是否是素数?
很容易想到用每一个小于 且大于 的数来判断它是否是 的约数, 若都不是, 则 为素数, 否则为合数.
这个暴力算法是 的, 但是我们考虑到, 若数 为 的约数, 则 也是 的约数, 所以我们只需用 的数来判定即可, 于是优化一下我们得到一个 的算法.
bool isPrime(int n) { if (n == 1) return false; if (n == 2) return true; for (int i = 2; i * i <= n; ++i) if (n % i == 0) return false; return true;}整数惟一分解定理
若整数 , 那么 一定可以惟一地表示为若干素数的乘积, 形如
vector <int> factor(int n) { vector <int> ret; for (int i = 2; i * i <= n; ++i) while (n % i == 0) { ret.push_back(i); n /= i; } if (n > 1) ret.push_back(n); return ret;}素数筛法
主要是埃氏筛法和欧拉筛法.
Eratosthenes 筛法
Eratosthenes 筛法可以快速列举出给定范围内的所有素数.
一个合数一定可以写成 的形式, 其中 为素数, 为整数, 因此对每一个 , 我们从小到大枚举 , 筛掉相应的 即可.
一点小改进: 当 时, 一定被 的素因子筛过一遍, 所以我们枚举 时从 开始枚举.
时间复杂度:
bool isprime[N];
void Eratos(int n) { memset(isprime, 1, sizeof isprime); isprime[0] = isprime[1] = 0; for (int i = 2; i * i <= n; ++i) if (isprime[i]) { for (int j = i * i; j <= n; j += i) isprime[j] = 0; }}例题
由于这里 的范围很大, 暴力筛完之后枚举求解肯定会 T , 但是 的范围却是在我们的承受范围之内的.
于是我们考虑用 这一小范围的素数筛掉 之间的合数.
假设数 , 其中 的最小质因数为 , 满足 , 则 , 若 , 则 一定存在小于 的质因数, 矛盾, 故而 , 也就是说, 会被漏筛的数 满足 , 为了保证算法的正确性, 我们要让这样的数落在 外, 也就是让 , 于是我们令 即可.
筛法优化质因数分解
利用埃氏筛法可以快速实现素因数分解, 只要在筛的时候记录下每个数的最小素因数即可.
bool isprime[N];int minfac[N];
void Eratos(int n) { memset(isprime, 1, sizeof isprime); isprime[0] = isprime[1] = 0; for (int i = 1; i <= n; ++i) minfac[i] = i; for (int i = 2; i * i <= n; ++i) if (isprime[i]) { for (int j = i * i; j <= n; j += i) { isprime[j] = 0; if (minfac[j] == j) minfac[j] = i; } }}
vector <int> factor(int n) { vector <int> ret; while (n > 1) { ret.push_back(minfac[n]); n /= minfac[n]; } return ret;}Euler 筛法
在埃氏筛中, 30 这个数被 分别筛了三次, 造成了重复, 如果我们能让每个数只被它最小的素因数筛, 就能做到 的时间复杂度.
在 Euler 筛法中, 我们从 枚举每一个数 , 若 是素数, 则加入素数表中, 然后我们再从小到大枚举前面的素数 , 筛掉 , 当 是 的一个素因数时, 那么后面再继续枚举到的素数一定不是所筛的数的最小素因数, 毕竟后面的素数一定大于当前的 , 所以这时可以终止筛选.
时间复杂度:
int prime[N];bool isprime[N];
void Euler(int n) { memset(isprime, 1, sizeof isprime); isprime[0] = isprime[1] = 0; for (int i = 2; i <= n; ++i) { if (isprime[i]) prime[++prime[0]] = i; for (int j = 1; j <= prime[0] && prime[j] * i <= n; ++j) { isprime[i * prime[j]] = 0; if (i % prime[j] == 0) break; } }}约数
整数惟一分解定理的推论
- 的正约数的个数为
- 的约数个数上界为
- 的所有正约数的和为
- 时间复杂度:
例题
这道题只需要知道 内前 12 个素数的积就已经超出 的范围, 所以打个表然后直接爆搜即可.
(亏我想了半天要怎么转化, 完全没考虑只有 12 个素数会被用到)
最大公约数
高精度
更相减损术适用于大整数下求
- 时,
- 时,
- 同为偶数时,
- 为偶数, 为奇数时,
- 为奇数, 为偶数时,
- 同为奇数时,
容斥原理
原理很简单, 记住一个公式即可
错排问题
-
利用容斥原理求解
令集合 表示 的全排列, 则
设子集 表示 排在第 位的排列, 则
同理, 设 表示 这 个数排在相应位置的排列数, 则
每个元素都不排在自己位置的排列数为
由容斥原理可得
-
递推公式
推导过程并不难想, 懒得写了, 直接上公式
欧拉函数
- 若 , 则称 互质(互素), 记作
- 欧拉函数 定义为 中与 互质的数的个数.
- 若 为素数, 则
容斥原理求欧拉函数
根据整数惟一分解定理, 将 分解为
设 中 的倍数的集合为 , 则
对于 , 有
根据容斥原理,
素因数分解求欧拉函数的算法
时间复杂度:
int euler_phi(int n) { int ret = n; for (int i = 2; i * i <= n; ++i) if (n % i == 0) { ret = ret / i * (i - 1);//先除后乘可以一定程度上避免爆 int while (n % i == 0) n /= i; } if (n > 1) ret = ret / n * (n - 1); return ret;}埃氏筛法预处理欧拉函数值
时间复杂度:
int phi[N];
void euler_phi(int n) { for (int i = 2; i <= n; ++i) phi[i] = i; for (int i = 2; i * i <= n; ++i) if (phi[i] == i) { phi[i] = i - 1; for (int j = i; j <= n; j += i)//注意这里 j 的起点是 i 而不是 i * i phi[j] = phi[j] / i * (i - 1); }}欧拉函数的性质
若 , 则 , 即欧拉函数具有积性函数的性质.
上式分别将 用整数惟一分解定理表示, 然后带入欧拉函数计算式即可得到证明.
例题
「POJ 3090」Visible Lattice Points
设某点坐标为 , 若该点能被看见, 则这个点一定是直线 上距离原点最近的一个整数点.
我们设一点 满足 , 则该点显然不可见, 从这个式子中我们可以知道, 必然存在一个公约数, 而 不存在公约数, 即 互质, 因此可见点就是图中所有满足 互质的点再加上 .
固定 , 当 时, 满足条件的点的数量就是 , 当 时, 将点 以 对称过去得到的点就是固定 时 的情况, 根据坐标轴关于 对称的性质, 和 两种情况下的答案数相等, 所以我们只需考虑 的情况, 然后乘 2 即可.
最终的答案为:
考虑满足 的 的个数.
由 可以推出 , 即 互质, 又 , 所以这样的 的个数为 , 也就是 的个数为
所以答案为
同余
剩余系
- 剩余系指模正整数 的余数所组成的集合.
- 完全剩余系: 如果一个剩余系中包含了这个正整数 所有可能的余数 () , 那么就称它为模 的一个完全剩余系, 记作 . 简化剩余系:完全剩余系中与 互质的数的集合, 记作 .
- 同余等价类: 里的每一个元素代表所有模 意义下与它同余的整数, 我们把满足同余关系的所有整数看作一个同余等价类.
- 在 中的四则运算结果全部都要在模 意义下.
模运算
-
如果 且有 , 那么:
-
快速幂
右转 快速幂复习笔记
费马小定理
若 为素数, 且 , 则 .
因为 为素数, 则 与 互质, 且不存在两个数模 同余.
又 , 所以 与 互质, 且不存在两个数模 同余.
因此 与 互质, 且
两边同时除以 得到
- 费马小定理的另一种形式: 若 为素数, 则对任意正整数 有 .
- 应用: 是素数, 互质, 则 .
费马小定理判断素数
- 多次选取 来判断 是否满足费马小定理, 正确的概率随 选取的次数的增加而增加.
- 时间复杂度: 设选取 次 , 每次用快速幂处理的时间为 , 则总时间复杂度为 .
- 由于存在 Carmichael 数, 因此上述算法存在缺陷, 即存在合数满足费马小定理.
- 以内的 Carmichael 数有: 561, 1105, 1729, 2465, 2821, 6601, 8911.
欧拉定理
(在 不是素数的情况下, 可以使用欧拉定理)
对于和 互素的 , 有: .
证明方法和费马小定理差不多.
引理: 若 互质, 满足 的最小正整数 是 的约数.
欧拉定理的推论
互素, 则有 .
例题
设 个 8 连在一起的正整数为 , 则 , 容易求得 .
若 , 即 , 可以得到 , 令 , 则有
于是
根据欧拉定理的引理, 只需求出 , 然后枚举它的约数, 一一验证后取最小的一个数即可.
时间复杂度:
扩展欧几里得算法
裴蜀定理 ( Bézout 定理 )
对任何整数 , 关于未知数 的线性不定方程 ( 称为裴蜀等式 ) 有整数解当且仅当 为 的倍数.
裴蜀等式有解时必有无穷多个解.
推论: 互素等价于 有整数解.
例题
假设 已知, 于是有方程
根据裴蜀定理, 必然是 的倍数, 取 即可.
扩展欧几里得算法
根据裴蜀定理, 有无穷组解, 而扩展欧几里得算法求出来的只是其中一组特解 .
下面考虑求 的一般解.
首先设 ( 为整数 ) , 则使用扩展欧几里得算法求出 的特解 后, 容易得到 特解为 .
由于之后考虑的是 , 所以后面用 代替 .
将方程的所有解按照 的大小排序, 则特解后的下一组解 可以表示为 , 其中 为满足条件的最小正整数.
带入方程后得到 , 变形后有 .
因此方程的一般解可以表示为 .
为什么这里要将 再变形为 呢?
因为 不是最简分数, 这一步变形是化简 , 直接用 来表示特解的话会漏掉一些情况, 如 就不能被 表示.
逆元
模 意义下乘法的逆
如果在 中两元素 满足 , 那么我们就说 互为模 意义下乘法的逆元, 记作 .
逆元
当 互素的时候, 若 , 则称 为 关于模 的逆元, 记作 .
在 的范围内, 逆元是唯一的.
求解逆元
求解逆元等价于解方程
直接使用扩展欧几里得算法即可.
欧拉定理求逆元
因为 互素, 根据欧拉定理, 有 , 因此 为所求逆元.
线性求逆元: 递推法
递推法求解 的逆元.
假设当前求解的是 关于模 的逆元.
根据带余除法, , 两边分别对 取模得到
若 不为 的倍数, 则 不为 0 , 因此 存在.
在上式两边乘 得到 , 即
为了防止答案为负数, 使用 代替 .
时间复杂度:
inv[1] = 1;for (int i = 2; i <= n; ++i) inv[i] = inv[p % i] * (p - p / i) % p;线性求逆元: 倒推法
先直接求解 的逆元, 然后利用 倒推求得 的逆元.
接着再利用 求解 的逆元.
例题
首先利用整数惟一分解定理将 表示为 .
则 约数和为
可以发现, 每一个括号里都是一个等比数列求和, 以 为例, 它的和可以写成 , 然后我们可以利用快速幂求解分子 , 这时若 , 我们会得到 , 这显然是错的, 而模数 正好为素数, 因此考虑用 的逆元与分子的结果相乘来代替除法.
当 为 的倍数时不存在逆元, 这时我们可以发现 , 因此 , 所以 .
线性同余方程
形如 的方程,称为线性同余方程。
- 朴素算法 将 一次带进去验证即可,这个朴素算法的复杂度取决于 。
- 扩展欧几里得算法 可以将方程看作 ,然后利用扩展欧几里得方法求解。 根据裴蜀定理,当且仅当 时方程有解。 令 ,则方程一共有 个解,我们用扩展欧几里得算法求得特解 ,则方程的通解 。
- 欧拉定理 令 ,若 则方程无解。 然后令 ,则 ,原方程变为 ,由欧拉定理, ,在方程两边分别乘上上式,得到 ,即 ,于是我们得到原方程的特解 ,因此原方程的解为 。
例题
线性组合
对应整数数列 是否存在 , 使得 ,其中 。如有,请找出一组整数解 满足 .
题中的方程是 n 元形式的扩展欧几里得方程,我们尝试将 n 元逐步降为二元得到最终解。
设 ,令 ,首先将方程变形为 ,此时满足 。
然后我们考虑求解方程 ,利用扩展欧几里得方法求得特解 ,由于题中仅要求得到一组解,因此我们只取特解即可,然后再以同样的方法求解 便可以实现逐渐降元了。
线性同余方程组(模互素)
考虑形如 的若干个线性同余方程联合得到的方程组,例如
一种可行的解法是令 ,然后将方程 改写成 ,接着再令 代入方程 得到 ,再令 ,最后我们可以得到解 。
中国剩余定理
若线性同余方程组 的模数 两两互素,则 在 下有唯一解。
令 ,有 ,因此 在 下的逆元存在,设其为 ,于是有 。
可以得到解 。
//China Remainer Theoremint CRT(const int a[], const int m[], int n) { int M = 1, ret = 0; for (int i = 1; i <= n; ++i) M *= m[i]; for (int i = 1; i <= n; ++i) { int ti = getinv(m[i], a[i]), Mi = M / m[i]; ret = (ret + a[i] * ti * Mi % M) % M; } return ret;}
//利用 exgcd 求逆元int getinv(int a, int b) { int x, y; exgcd(a, b, x, y); return x;}线性同余方程组(模不互素)
对于模不互素的线性同余方程组
考虑求解方程数量为 2 的情形,当方程数量大于 2 时迭代求解。
设 ,则方程组变为
设 ,根据裴蜀定理,若 ,则方程组无解,否则令 ,有
此时 ,令 ,再由欧拉定理可得 ,于是 ,最后代回去可以得到 。
例题
「POJ 2891」Strange Way to Express Integers
算法和上述一致。
假设我们已经通过迭代求解得到前 个方程的一个解 ,令 ,则 是前 个方程的通解。
考虑第 个方程,我们可以将其改写为 ,即 ,接下来找到一个合适的 即可,也就是求解一个同余方程了,利用欧拉定理求解即可。
高次同余方程
第一类高次同余方程
形如 的方程称为第一类高次同余方程,其中 。
BSGS
解决这一类方程主要采用 BSGS (Baby Step Giant Step) 算法。
将 写成 的形式,则原方程可变形为 。
假设 已知,那么我们只需枚举 ,然后计算相应的 ,判断是否满足变形后的等式即可。
因为在枚举 的过程中 的取值也在变化,但 的取值范围是不变的,因此我们可以预处理所有可能的 值,用一个 Hash 表储存。
由欧拉定理可知,有 ,而 ,因此从 1~p ,模 意义下 的幂至少完成了一次循环,所以最小整数解 ,进而我们可以从 到 枚举 ,而 ,因此时间复杂度为 ,当 时复杂度最小。
时间复杂度:
int BSGS(int a, int b, int p) { b %= p; if (a % p == 0) return b ? 1 : -1; map <int, int> hash; int t = sqrt(p) + 1; for (int i = 0; i < t; ++i) { int val = (long long)b * power(a, i, p) % p;//快速幂 hash[val] = i; } a = power(a, t, p); for (int i = 0; i <= t; ++i) { int val = power(a, i, p); int j = hash.find(val) == hash.end() ? -1 : hash[val]; if (j >= 0 && i * t - j >= 0) return i * t - j; } return -1;}第二类高次同余方程
形如 的方程称为第二类高次同余方程。
首先引入阶和原根的概念,这一部分和抽象代数相关,我目前还不会 QAQ 。
阶
满足同余式 的最小整数 称为 模 的阶,记作 。
由欧拉定理可知,当 时,有 ,因此阶的存在性得证。
性质 1 : 模 两两不同余。
性质 2 :若 ,则 。
推论 :若 ,则 。
性质 3 :设 ,则 的充要条件是 。
性质 4 :设 ,则 。
原根
设 ,若 ,且 ,则称 为模 的原根。
以下基于 为质数的情况进行讨论。
如何判断 是否是 的原根?
由费马小定理,方程 的一个解为 ,因此最小整数解 ,而满足 的整数 是 的原根,所以我们逐个枚举 的约数 ,如果只有当 时满足 ,则 是 的原根。
下面考虑求解 。
由阶的性质 1 ,存在唯一的正整数 ,满足 ,其中 是 的原根, 可以由第一类高次同余方程求出。
同样,存在唯一正整数 ,使得 ,于是上述方程变为 。
由阶的性质 2 的推论, ,于是我们求解这个线性同余方程得到 便可以得到 。