算法归纳总结(第五天)(数论、数学知识(第一部分)总结)

目录

一、筛质数(与试除法)

1、普通筛法

2、埃筛法

3、线性筛法

4、试除法

①、试除法代码

二、约数

 1、试除法求约数

2、最大公约数

①、辗转相除法(欧几里得算法)

3、约数个数

4、约数之和

三、欧拉函数

1、普通筛求欧拉函数

①、欧拉函数定义

②、应用公式。

③、代码实现

2、线性筛求欧拉函数

①、线性筛法

②、求欧拉函数

四、快速幂与求逆元

1、快速幂

2、快速幂求逆元

五、扩展欧几里得算法与线性同余方程

1、扩展欧几里得算法

①、裴蜀定理

2、线性同余方程

六、(扩展)中国剩余定理

1、数学推导

2、代码实现

小结

总结


一、筛质数(与试除法)

1、普通筛法

题目链接:筛质数

这种方法很常见,通常用来判断质数,也就是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<

2、埃筛法

题目链接与上面相同。

该题目的代码与上面几乎相同,不同的是,将筛质数的部分加入到了判定质数的部分,

时间复杂度为 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<

3、线性筛法

题目链接与上面相同。

线性筛法中,我们筛选质数是用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<

4、试除法

①、试除法代码

该代码就是简单判断是否是质数。

试除法本身其实就是从 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;
}

二、约数

 1、试除法求约数

题目链接:试除法求约数

#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

2、最大公约数

①、辗转相除法(欧几里得算法)

所谓辗转相除法,就是用后一个数除以,前一个数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;
}

3、约数个数

题目链接:约数个数

此处我们首先需要知道一个公式,一个数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;
}

4、约数之和

题目链接:约数之和

这个我们依然要知道 公式 就是一个数 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);
}

三、欧拉函数

1、普通筛求欧拉函数

①、欧拉函数定义

欧拉函数就是指一个数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<

2、线性筛求欧拉函数

①、线性筛法

我们先把线性筛法模板看一下,我们在这基础上求欧拉函数。

②、求欧拉函数

对比,我们发现了不同,在if( ! st[i])处,多添加了phi[i]数组的定义,这个数组代表着该数的欧拉函数,如果i是质数,那么它的欧拉函数就是 i -1。同时phi[1] = 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;
}

四、快速幂与求逆元

1、快速幂

题目链接:快速幂

例如:求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;
}

2、快速幂求逆元

题目链接:快速幂求逆元

逆元定义等描述请去上面题目链接看。

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;
}

五、扩展欧几里得算法与线性同余方程

1、扩展欧几里得算法

①、裴蜀定理

欧几里得算法: 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<

2、线性同余方程

题目链接:线性同余方程

题目中可以将描述,转化成:

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<

六、(扩展)中国剩余定理

题目链接:表达整数的奇诡方式

该题目相当于对前面一堆知识的总结性运用

1、数学推导

其中 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值。

2、代码实现

首先,我们要背出来扩展欧几里得算法的模板。

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总学习总结的。

你可能感兴趣的:(算法,c++)