目录
一、筛质数(与试除法)
1、普通筛法
2、埃筛法
3、线性筛法
4、试除法
①、试除法代码
二、约数
1、试除法求约数
2、最大公约数
①、辗转相除法(欧几里得算法)
3、约数个数
4、约数之和
三、欧拉函数
1、普通筛求欧拉函数
①、欧拉函数定义
②、应用公式。
③、代码实现
2、线性筛求欧拉函数
①、线性筛法
②、求欧拉函数
四、快速幂与求逆元
1、快速幂
2、快速幂求逆元
五、扩展欧几里得算法与线性同余方程
1、扩展欧几里得算法
①、裴蜀定理
2、线性同余方程
六、(扩展)中国剩余定理
1、数学推导
2、代码实现
小结
总结
题目链接:筛质数
这种方法很常见,通常用来判断质数,也就是i的平方 < x,i < 根号下的x,因为一个数在小于根号下x的范围内依然不是质数,那肯定就不是质数了。
时间复杂度为 n * logn.
#include
using namespace std;
const int N = 1e6 + 10;
bool st[N];
int cnt,prime[N], n;
void get_prime()
{
for(int i = 2; i<=n; i++){
if(!st[i]) prime[cnt++] = i;
for(int j = i; j<=n; j+=i){//合数与质数均用来筛后面的数
st[j] = true;
}
}
}
int main()
{
cin>>n;
get_prime();
cout<
题目链接与上面相同。
该题目的代码与上面几乎相同,不同的是,将筛质数的部分加入到了判定质数的部分,
时间复杂度为 n * logn * logn.
#include
using namespace std;
const int N = 1e6 + 10;
bool st[N];
int cnt,prime[N], n;
void get_prime()
{
for(int i = 2; i<=n; i++){
if(!st[i]){
prime[cnt ++] = i;
for(int j = i; j<=n; j+=i) st[j] = true;//此处直接用质数筛选数。
}
}
}
int main()
{
cin>>n;
get_prime();
cout<
题目链接与上面相同。
线性筛法中,我们筛选质数是用prime[j] 来筛选,也就是最小质数,但是如果i % prime[j] 就break。
举个例子,模拟一下代码运行进程:
n = 12的时候,
i = 2 , prime[0] = 2。 for循环里面,st[2 * 2] = true;
i = 3, prime[0] = 2,prime[1] = 3 。for循环里面,st[2 * 3] = true; st[3 * 3] = true;
i = 4, prime[0] = 2, prime[1] = 3, prime[2] = 4, 然后注意,st[ 2 * 4] = true。
但是,此时i % prime[j] == 0;此时因该跳出,因为,接下来继续筛选的话,就不是用prime[]中的数来筛选,也就是最小质数,而是使用i, 如果使用i 的话,有可能是重复筛过的数,不符合线性筛法每个数只筛一次的特点。
比如我们如果不break,继续往下走,那么st[3 * 4] = true。然后如果我们i == 6得时候,st[6 * 2] = true。此时是不是相当于重复了,st[3 * 4]中,我们用的是i = 4来筛,当我们使用 st[ 6 * 2]时,用的时prime[0] = 2 来筛选,因此我们可以避免冲服筛选。
时间复杂度为 n。
#include
using namespace std;
const int N = 1e6 + 10;
bool st[N];
int cnt,prime[N], n;
void get_prime()
{
for(int i = 2; i<=n; i++){
if(!st[i]) prime[cnt ++] = i;
st[i] = true;
for(int j = 0; prime[j] <= n / i; j++){
st[prime[j] * i] = true;
if(i % prime[j] == 0) break;
}
}
}
int main()
{
cin>>n;
get_prime();
cout<
该代码就是简单判断是否是质数。
试除法本身其实就是从 2 开始, 令i <= n / i。就可以判断1 - n 中是否有非质数,
因为如果一个数的根号下都找不到能让它被整除的数,那比它根号下大的数也肯定不存在。
bool is_prime(int x)
{
if(x == 1) return false;
for(int i = 2; i <= x; i++){
if(x % i == 0) return false;
}
return true;
}
题目链接:试除法求约数
#include
using namespace std;
#include
void get_num(vector& arr, int n)
{
for(int i = 1; i<=n / i; i++){
if(n % i == 0){
arr.push_back(i);
if(i != n / i) arr.push_back(n / i);
}
}
sort(arr.begin(), arr.end());
}
int main()
{
int n; cin>>n;
while(n -- )
{
vector arr;
int x; cin>>x;
get_num(arr, x);
for(int i = 0; i
所谓辗转相除法,就是用后一个数除以,前一个数mod后一个数的余数。
举个例子, a = 3, b= 6。两者最大公约数为3,
第一次 :x = a % b = 3,y = b = 6。 我们令 a = y,即 a = 6; b = x,即 b = 3。
第二次 :x = a % b = 0,y = b = 3。 我们令 a = y,即 a = 3; b = x,即 b = 0。
因为0与任何数的余数都是那个数,所以我们返回结果为3。
我们发现,有重复的交换逻辑,因此代码可以直接抽象成下面这一行。
b为0返回a,b不为0继续递归,a = b, b = a % b。直到 b == 0。
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
题目链接:约数个数
此处我们首先需要知道一个公式,一个数N = 2^k2 + 3^k3 +······ + n^ kn
约数个数的公式就是 (k2 + 1) * (k3 + 1) * ······ *(kn + 1)。
例如 12 = 2 ^ 2 + 3 ^ 1。那么它的约数个数就是(2 + 1)* (1 + 1) = 6。
那我们来数一数,1 - 12中, 1、2、3、4、6、12 都是12的约数,结果也正是6。
我对于这个的理解就是,2^2 代表可以变形出来 2^0、2^1、2^2。
3^1 代表可以变形出来3^0、3^1。
这几个数各自乘一下,发现正好是约数的那几个数。
下面我们代码实现一下。
#include
using namespace std;
typedef long long LL;
#include
const int mod = 1e9 + 7;
int main()
{
int n; cin>>n;
unordered_map prime;
while(n -- )
{
int x; cin>>x;
for(int i = 2; i<=x/i; i++){
while(x % i == 0){
prime[i]++;
x /= i;
}
}
if(x > 1) prime[x]++;
}
LL res = 1;
for(pair primes:prime){
res = res * (primes.second + 1) % mod;
}
printf("%d", res);
return 0;
}
题目链接:约数之和
这个我们依然要知道 公式 就是一个数 N = 2^k2 + 3^k3 +…… + n^kn。
约数之和 = (2^0 + 2^1 + …… + 2^k2) * (n^0 + n^1 + …… + n^kn)。
举例子 12 = 2 ^ 2 + 3 ^ 1 。其约数是1、2、3、4、6、12 。和应该是28,那我们直接套公式。
(1 + 2 + 4 ) (1 + 3) = 7 * 4 = 28。所以公式成立。
代码中实现的过程用的是秦九韶算法。
例如 t = (t * a + 1) % mod部分,1就是a^0。我们还是以2 ^ 2举例
第一次 t = 1, t = (t(1) * a(2) + 1)。 结束 t = 2 ^ 1 + 2 ^ 0。
第二次 t = 2^0 + 2 ^ 1。 t = (t(2^0 + 2 ^ 1) * a(2) + 1) 。结束 t = 2 ^ 2 + 2 ^ 1 + 2 ^ 0。
这就是具体分析过程。
#include
using namespace std;
#include
typedef long long LL;
const int mod = 1e9 + 7;
int main()
{
int n; cin>>n;
unordered_map primes;
while(n -- ){
int x;cin>>x;
for(int i = 2; i <= x / i; i++){
while(x % i == 0){
primes[i] ++;
x /= i;
}
}
if(x > 1) primes[x] ++;
}
LL res = 1;
for(pair prime:primes){
int a = prime.first;int b = prime.second;
LL t = 1;
while( b -- ){
t = (t * a + 1) % mod;
}
res = res * t % mod;
}
printf("%d",res);
}
欧拉函数就是指一个数N,其中1-N中与N互质的数的个数。
比如3。1-3中 1、2是与3互质的,因此3的欧拉函数是2。记作phi[3] = 2。
基于上述,我们还知道任意互质的两个数如 m,n使得prime[m * n] = prime[m] * prime[N]。
例如phi[12] = phi[3] * phi[4] 。
我们只需要会用公式即可,具体证明可Acwing 观看y总视频讲解。
以N = a ^ n1 + b ^ n2 + …… + k ^ nk。
12 = 2 ^ 2 + 3 ^ 1。
我们只需要取出不同底数,然后套用公式。
公式 :phi[N] = N * (1 - 1/a) * (1 - 1 / b) * ……(1 - 1 / k)。
例如 phi[12] = 12 * (1 - 1 / 2) * (1 - 1 / 3) = 4。成立。(公式很重要)。
#include
using namespace std;
typedef long long LL;
int main()
{
int n; cin>>n;
while(n -- )
{
int x; cin>>x;
LL res = x;
for(int i = 2; i <= x / i; i++){
if(x % i == 0){
res = res - res / i;
}
while(x % i == 0) x /= i;
}
if(x > 1) res = res - res / x;
cout<
我们先把线性筛法模板看一下,我们在这基础上求欧拉函数。
对比,我们发现了不同,在if( ! st[i])处,多添加了phi[i]数组的定义,这个数组代表着该数的欧拉函数,如果i是质数,那么它的欧拉函数就是 i -1。同时phi[1] = 1要注意。
然后下面分两种情况,
①、如果i % prime[j] == 0。我们还是举12的例子, i = 6,prime[0] = 2, phi[6] = 2。
此时phi[12] = phi[i * prime[0]] => phi[i] * prime[j] = phi[6] * prime[0] = 4。
这是因为prime[j] 是i 的约数,(1 - 1 / prime[j])的情况在phi[i]中计算过了。所以直接将N变为原来的prime[j]倍即可。然后带入前面的应用公式,就可以求得上面的公式。
②、i % prime[j] != 0。
此时因为prime[j]不是i的约数,因此我们还要乘一个(1 - prime[j])。
phi[ prime[j] * i ] = phi[i] * prime[j] * (1 - prime[j]) = phi[i] * (prime[j] - 1)。
因此我们下面的代码实现如下。
最后将phi中的值全部相加然后输出即可。
LL get_prime(int n)
{
phi[1] = 1;
for(int i = 2; i<=n; i++){
if(!st[i]){
prime[cnt++] = i;
phi[i] = i-1;
}
for(int j = 0; prime[j]<=n/i; j++){
st[prime[j]*i] = true;
if(i%prime[j]==0)
{
phi[prime[j]*i] = phi[i]*prime[j];
break;
}
phi[prime[j]*i] = phi[i]*(prime[j]-1);
}
}
LL res = 0;
for(int i = 0; i<=n; i++) res+=phi[i];
return res;
}
题目链接:快速幂
例如:求a的k次方 mod p。4 的 3次方 mod 9。结果就是1。
3的二进制就是011。拆成2 ^ 1 + 2 ^ 0。4的3次方 mod 9的结果就
相当于4的1次方 mod 9 * 4的2次方 mod 9。
注意a要用LL,同时将 a反复平方的时候要 mod 9防止爆int。
#include
using namespace std;
typedef long long LL;
LL qmi(LL a, int k, int p)
{
LL res = 1;
while(k)
{
if(k&1) res = res * a % p;
k >>= 1;
a = a*a % p;
}
return res;
}
int main()
{
int n; cin>>n;
while( n -- )
{
LL a,k,p;
cin>>a>>k>>p;
printf("%d\n",qmi(a, k, p));
}
return 0;
}
题目链接:快速幂求逆元
逆元定义等描述请去上面题目链接看。
a / b 同于 a * x(mod m)。左右乘b求得b * x 同于 1(mod m)。
由费马小定理 n为质数时,b ^ (n - 1) ≡ 1 (mod n)。
再乘出来个b ,那b * b^(n - 2) ≡ 1 (mod n)。
x = b^(n - 2)。b与m互质时,乘法逆元就是b^(n - 2)。
b为m的倍数时候,b * x == 0。b*任何数都是0。
#include
using namespace std;
typedef long long LL;
LL qmi(LL a, int k, int p)
{
LL res = 1;
while(k)
{
if(k&1) res = res*a%p;
k >>= 1;
a = a*a % p;
}
return res;
}
int main()
{
int n;cin>>n;
while(n--)
{
LL a;int p;
cin>>a>>p;
if(a%p) printf("%d\n",qmi(a,p-2,p));
else printf("impossible\n");
}
return 0;
}
欧几里得算法: gcd(a, b),代表a,b的最大公约数。
裴蜀定理:a * x + b * y = gcd(a,b);(x, y均为整数)。
假设a = 3, b = 6。x = 1 , y = 0。满足条件。
又知道 gcd(a, b) 同于 gcd(b, a%b)。
此时y = 0,x = 1, a % b = 3,b = 6。
b * y + a % b * x = gcd(a, b)。①
***[]代表向下取整。eg: [9 / 4] = 2***
a % b = a - [a / b] * b。②
由①、②公式推导得 b * y + a * x - [a / b] * b * x。 =>
a * x + b * (y - [a / b] * x) = gcd(a, b)。
代码如下:
#include
using namespace std;
//裴蜀定理
//a * x + b * y = gcd(a,b);
//b * y + (a % b) * x = gcd(a,b);
//a % b = a - [a / b] * b;
//变形后: b * y + a * x - [a / b] * b * x => a * x + b * (y - [a / b] * x);
int exgcd(int a, int b, int& x, int& y)
{
if(b == 0){
x = 1; y = 0;
return a;
}
int d = exgcd(b, a%b, y, x);
y -= a / b * x;
return d;
}
int main()
{
int n; cin>>n;
while(n -- )
{
int a,b,x,y;
cin>>a>>b;
exgcd(a, b, x, y);
cout<
题目链接:线性同余方程
题目中可以将描述,转化成:
a * x = m * y’ + b。 => a * x - m * y' = b。令y = -y'。
原式子等于:a * x + m * y = b。
我们求的时候,a * x + m * y = d。 该式子中的结果x 记作 x = x0。
最后转化一下,同时扩大 b / d倍,a * (x0 * b / d) + m * y1 = b。
x1 = x0 * b / d。然后我们引入变量k * a * m。
式子就成了 a * (x0 * b / d + k * m) + m * (y1 - k * a) = b。
通解X = x0 * b / d + k * m。
所以最后结果要 x0 * b / d % m,结果就是 x0 * b / d。即最终解。
代码如下:
#include
using namespace std;
typedef long long LL;
//a * x = m * y + b
//a*x - m*y = b;
//a * x + m * y = b;(等价于一个扩展欧几里得算法)
//求x,y是否存在。也就是求b是否时(a, m)的最大公约数,即b % (a,m) == 0 是否成立。
//同时对结果同时扩大 d / b倍。
//先对 a * x + m * y = d求解,然后记解为X0.
//然后同时扩大 b / d倍,X1 = X0 * b / d;
//带入原方程,a * x1 + m * y1 = b => a * (x0 * b / d) + m * y1 = b;
//变形,加入变量k * m。然后为 a * (x0 * b / d + k * m) + m * (y1 - k * a) = b。
//所以最后的x的通解为 X = x0 * b / d + k * m。当a % m 的时候,最终结果就是 x = x0 * b / d;
//同时x要加上long long 防止爆int。
int exgcd(int a, int b, int& x, int& y)
{
if(b == 0){
x = 1; y = 0;
return a;
}
int d = exgcd(b, a%b, y, x);
y -= a / b * x;
return d;
}
int main()
{
int n; cin>>n;
while(n -- )
{
int a,b,m,x,y; cin>>a>>b>>m;
int d = exgcd(a, m, x, y);
if(b % d == 0) cout<<((LL)x * b / d) % m<
题目链接:表达整数的奇诡方式
该题目相当于对前面一堆知识的总结性运用
其中 a 和 m 为已知值。
x mod a1 = m1 => x = k1 * a1 + m1。
x mod a2 = m2 => x = k2 * a2 + m2。
k1 * a1 + m1 = k2 * a2 + m2 => k1 * a1 - k2 * a2 = m2 - m1。①
因此根据裴蜀定理就有: gcd(a1, a2) | (m2 - m1)。
设 D = gcd(a1, a2)。
我们从①式中求解的k1 = k1 + K * a2 / d (K未知)②
k2 = k2 + K * a1 / d (K未知)③
由①、②、③代入后可知 a1 * (k1 + K * a2 / d) - a2 * (k2 + K * a1 / d) = m2 - ,m1。
整理可知: K * a1 * a2消了,结果还是 ①式,说明求得了通解。
----------------------------------------------------------------------------------------------------
然后我们选取x = k1 * a1 + m1式。把②代入。
x = (k1 + K * a2 / d) * a1 + m1。
令A = a1 * a2 / d (A即为a1,a2得最小公倍数)。
也就是A = [a1, a2] ( [a1, a2] 表示a1 与 a2 得最小公倍数)。
x = a1 * k1 + m1 + K * A。
令x0 = a1 * K1 + m1 + K * A。
x = x0 + K * A。④
知道④式与最开始的x = a1 * k1 + m1 和 x = a2 * k2 + m2…………x = an * kn + mn。
形式非常相似。换句话说,最终这些式子统统可以用④这个通式表达。
通式换个形式就是 x mod A = x0。
最后求得是 x0 mod A值。
首先,我们要背出来扩展欧几里得算法的模板。
LL ecgcd(int a, int b, LL& x, LL& y)
{
if(!b){
x = 1; y = 0;
return a;
}
LL d = ecgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
然后,我们具体代码实现
#include
using namespace std;
#include
typedef long long LL;
LL ecgcd(LL a, LL b, LL& x, LL& y)
{
if(!b){
x = 1; y = 0;
return a;
}
LL d = ecgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
int main()
{
int n; cin>>n;
LL a1, m1; cin>>a1>>m1;
bool has_answer = true;
for(int i = 0; i>a2>>m2;
LL k1, k2;
LL d = ecgcd(a1, a2, k1, k2);
if((m2 - m1) % d){
has_answer = false;
break;
}
//此时 k1 * a1 - k2 * a2 = d。同时扩大(m2 - m1)/ d倍,然后
k1 *= (m2 - m1) / d;
//因为数值比较极限,因此k1 = k1 + K * a2 / d。同时扩大。
LL t = a2 / d;
k1 = (k1 % t + t) % t;//考虑到t为负数的情况。
m1 = a1 * k1 + m1;//m1 == x
a1 = abs(a1 / d * a2);
}
if(has_answer){
cout<<(m1 % a1 + a1) % a1<
这个感觉特别难,想好久都似懂非懂,主要数学推理和代码实现很难。不过静下心花长一点的时间也是能推出来的,也是一种收获。
今天总结的知识是初等数论,都是很不错的知识点,跟着y总学习总结的。