算法中的基础数学知识(一)—初等数论

一、知识框架

  • 数论
  • 组合计数
  • 高斯消元
  • 简单博弈论

  本篇有关内容是数论有关的内容。

二、质数

  质数是针对所有大于1的自然数定义的,如果它的约数只有1和它本身,那么这个数就是一个质数(素数)。

1、质数的判定—试除法

  原始版本:

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i < x; ++i)
    {
        if (x % i == 0) return false;
    }
    return true;
}

  这是一个O(n)的算法,我们可以这样优化,定义d | n表示d能整除n(如3能整除12),有如下性质:
d ∣ n − > n d ∣ n d | n->\frac{n}{d} | n dn>dnn
  因为n的所有约数都是成对出现的,所以我们只要枚举所有小的数,就会通过这个转化枚举完大的数,即d <= n / d的数即可。

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; ++i)
    // 不推荐 i <= sqrt(x) sqrt比较慢
    // 不推荐 i * i <= x i * i存在溢出风险
    // 推荐写法 i <= x / i
    {
        if (x % i == 0) return false;
    }
    return true;
}

  时间复杂度:O(sqrt(n))

  题解:

#include <iostream>
using namespace std;

bool isPrime(int n)
{
    if (n < 2) return false;
    for (int i = 2; i <= n / i; ++i)
    {
        if (n % i == 0) return false;
    }
   return true;
}

int main()
{
    int n, x;
    cin >> n;
    while (n--)
    {
        cin >> x;
        if (isPrime(x)) puts("Yes");
        else puts("No");
    }
    return 0;
}

2、分解质因数—试除法

n = p 1 k 1 ∗ . . . ∗ p m k m n = p_{1}^{k_{1}}*...*p_{m}^{k_m} n=p1k1...pmkm

  基本思路:从小到达枚举n的所有数i,如果i | n,则定义s = 0,然后让n去把其次数除尽。

void divide(int n)
{
    for (int i = 2; i <= n; ++i)
    {
        // 因为 n /= i,所以当我们枚举到i时,
        // 已经把所有2 ~ i - 1的质因子都除干净了
        // n中不包含任何2 ~ i - 1的质因子了
        // 而 n % i == 0 因此i也不包含任何 2 ~ i - 1的质因子。
        // 因此此时i一定是质数。
        if (n % i == 0)
        {
            int s = 0;
            while (n % i == 0)
            {
                s++;
                n /= i;
            }
            // 打印这个因子的次数
            printf("%d %d\n", i, s);
        }
    }
}

  现在我们的时间复杂度是O(n),对应n是质数的情况。

  优化:注意到一个性质:n中最多只有一个大于sqrt(n)的质因子,证明就是考虑如果有两个大于sqrt(n)的质因子,那么它们乘在一起一定会大于n

  所以我们可以先把小于sqrt(n)的质因子枚举出来:i <= n / i。如果最后n还大于1,那么这是的n就是剩下的那个大于sqrt(n)的质因子,单独提出即可if (n > 1) printf("%d %d\n", n, 1);

#include <iostream>
using namespace std;

void divide(int n)
{
    for (int i = 2; i <= n / i; ++i)
    {
        if (n % i == 0)
        {
            int cnt = 0;
            while (n % i == 0)
            {
                ++cnt;
                n /= i;
            }
            printf("%d %d\n", i, cnt);
        }
    }
    // 如果n > 1 那么它就是剩下的那个大于sqrt(n)的质因子
    if (n > 1) printf("%d %d\n", n, 1);
}

int main()
{
    int n;
    cin >> n;
    int x;
    while (n--)
    {
        cin >> x;
        divide(x);
        cout << endl;
    }
    return 0;
}

时间复杂度:O(sqrt(n))

3、筛质数

1 朴素筛法

  朴素做法的原理就是把从2开始,把所有自己的倍数都筛掉,从头往后晒,最后剩下的数就是质数。

  它的原理如下:p没被筛掉,说明2p-1的任何一个数都没有把p筛掉,即2p-1的任何一个数都不是p的因子,所以p是素数。

int getprime(int n)
{
    for (int i = 2; i <= n; ++i)
    {
        // 如果没被筛过 它就是素数
        if (!st[i]) prime[sz++] = i;
        // 然后把它的倍数筛掉
        for (int j = i; j <= n; j += i) st[j] = true;
    }
    return sz;
}

时间复杂度分析:
2 循 环 次 数 为 n 2 , 3 循 环 次 数 为 n 3 , . . . , 总 循 环 次 数 为 n ( 1 2 + 1 3 + . . . ) n − > ∞ 时 , 调 和 级 数 等 于 l n ( n ) + c , c 是 欧 拉 常 数 所 以 时 间 复 杂 度 大 概 是 n l n ( n ) , 又 l n ( n ) < l o g 2 n , 所 以 时 间 复 杂 度 可 以 记 为 n l o g n 2循环次数为\frac{n}{2},3循环次数为\frac{n}{3},...,\\ 总循环次数为n(\frac{1}{2} + \frac{1}{3}+...)\\ n->∞时,调和级数等于ln(n) + c,c是欧拉常数\\ 所以时间复杂度大概是nln(n),又ln(n) < log_2n,所以时间复杂度可以记为nlogn 22n,33n,...,n(21+31+...)n>ln(n)+c,cnln(n)ln(n)<log2n,nlogn

2 优化1—埃氏筛法

  一点点优化,我们其实没必要判断2~p - 1的所有数是否为p的因子,因为质数的性质,我们只要看看2~p - 1的所有质因子是否为p的因子就行了。

int getprime(int n)
{
    for (int i = 2; i <= n; ++i)
    {
        // 如果没被筛过 它就是素数
        if (!st[i]) 
        {
            // 把质数的倍数都删掉
            prime[sz++] = i;
            for (int j = i; j <= n; j += i) st[j] = true;
        }
    }
    return sz;
}

  速度确实有提升。

  时间复杂度分析:

我们现在只让让质数的倍数被筛掉了,所以这里调和级数就变成了质数为分子,根据质数定理,1~n大概有n / ln(n)个质数,所以本来我们要求和n个数,现在只需要求n / ln(n)个数,原时间复杂度nln(n),现在变成nln(n) / ln(n),时间复杂度大概变成O(n),但这只是一个估计,真实的时间复杂度是O(nloglogn),调和级数中只算质数的话,求和大概是loglogn,这数很小,和O(n)大概是1个级别的。

3 优化2—线性筛法

  思路也是把每个合数用它的质因子筛掉,但是想让每个合数只被筛一次。

  它的核心是对任何一个合数n,它只会被它的最小质因子筛掉。

  我们先上代码,再做讲解:

void getprime(int n)
{
    for (int i = 2; i <= n; ++i)
    {
        if (!st[i]) Prime[sz++] = i;
        for (int j = 0; Prime[j] <= n / i; ++j)
        {
            st[Prime[j] * i] = true;
            if (i % Prime[j] == 0) break;
        }
    }
}

1. 当 i % P j = = 0 时 , P j 一 定 是 i 的 最 小 质 因 数 , 那 么 它 也 一 定 是 i ∗ P j 的 最 小 质 因 子 2. 当 i % P j ! = 0 时 , P j 小 于 i 的 所 有 质 因 子 , 所 以 P j 也 是 P j ∗ i 的 最 小 质 因 子 综 上 , 不 论 什 么 情 况 , P j 一 定 是 P j ∗ i 的 最 小 质 因 子 对 于 一 个 合 数 x , 假 设 P j 是 x 的 最 小 质 因 子 , 当 i 枚 举 到 x / P j 时 , x 就 会 被 s t [ P j ∗ i ] 给 筛 掉 所 以 任 何 合 数 都 会 被 它 的 最 小 质 因 子 筛 掉 又 因 为 每 个 数 只 有 一 个 最 小 质 因 子 , 所 以 该 算 法 时 间 复 杂 度 是 线 性 的 。 1.当i\%Pj==0时,Pj一定是i的最小质因数,那么它也一定是i * Pj的最小质因子\\ 2.当i\%Pj !=0时,Pj小于i的所有质因子,所以Pj也是Pj*i的最小质因子\\ 综上,不论什么情况,Pj一定是Pj*i的最小质因子\\ 对于一个合数x,假设Pj是x的最小质因子,当i枚举到x/Pj时,x就会被st[Pj * i]给筛掉\\ 所以任何合数都会被它的最小质因子筛掉\\ 又因为每个数只有一个最小质因子,所以该算法时间复杂度是线性的。 1.i%Pj==0,PjiiPj2.i%Pj!=0PjiPjPji,PjPjixPjxix/Pjxst[Pji]线

4 leetcode204. 计数质数

原题链接204. 计数质数

// 朴素筛法
class Solution {
public:
    int countPrimes(int n) 
    {
        int sz = 0;
        const int N = 5e6 + 10;
        int prime[N];
        vector<bool> st(n, false);
        for (int i = 2; i < n; ++i)
        {
            if (!st[i])
            {
                prime[sz++] = i;
            }
            for (int j = i; j < n; j += i) st[j] = true;
        }
        return sz;
    }
};

// 埃氏筛法
class Solution {
public:
    int countPrimes(int n) 
    {
        int sz = 0;
        const int N = 5e6 + 10;
        int prime[N];
        vector<bool> st(n, false);
        for (int i = 2; i < n; ++i)
        {
            if (!st[i])
            {
                prime[sz++] = i;
                for (int j = i; j < n; j += i) st[j] = true;
            }
        }
        return sz;
    }
};

//线性筛法
class Solution {
public:
    int countPrimes(int n) 
    {
        const int N = 5e6 + 10;
        bool st[N] = {0};
        int prime[N];
        int sz = 0;
        for (int i = 2; i < n; ++i)
        {
            if (!st[i]) prime[sz++] = i;
            for (int j = 0; j < sz && prime[j] <= n / i; ++j)
            {
                st[prime[j] * i] = true;
                if (i % prime[j] == 0) break;
            }
        }
        return sz;
    }
};

  可以看出速度还是有所区别的。

三、约数

1、试除法求约数

  一个数的约数也是成对出现的,所以求约数只要枚举比较小的约数就行了,另一个可以n / i自行求出。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

vector<int> get_divisors(int n)
{
    vector<int> res;
    for (int i = 1; i <= n / i; ++i)
    {
        if (n % i == 0)
        {
            res.push_back(i);
            // 防止重复
            if (n / i != i) res.push_back(n / i);
        }
    }
    sort(res.begin(), res.end());
    return res;
}

int main()
{
    int cnt;
    cin >> cnt;
    int x;
    while (cnt--)
    {
        cin >> x;
        vector<int> a = get_divisors(x);
        for (int t : a) cout << t << ' ';
        cout << endl;
    }
    return 0;
}

2、约数个数与约数之和

约数个数
根 据 算 术 基 本 定 理 : 任 何 一 个 正 整 数 N 都 可 以 表 示 为 : N = p 1 a 1 ∗ . . . . ∗ p k a k N 的 任 何 一 个 约 数 d 都 可 以 表 示 为 : d = N = p 1 b 1 ∗ . . . . ∗ p k b k 所 以 约 数 的 个 数 可 以 根 据 乘 法 定 理 , 个 数 为 : ( a 1 + 1 ) ( a 2 + 1 ) ∗ . . . ∗ ( a k + 1 ) 根据算术基本定理:\\ 任何一个正整数N都可以表示为:N=p_1^{a_{1}} * ....*p_k^{a_{k}}\\ N的任何一个约数d都可以表示为: d = N=p_1^{b_{1}} * ....*p_k^{b_{k}}\\ 所以约数的个数可以根据乘法定理,个数为:(a_1 + 1)(a_2 + 1)*...*(a_k + 1) :N:N=p1a1....pkakNd:d=N=p1b1....pkbk:(a1+1)(a2+1)...(ak+1)
我们这样就把

int范围内,约数个数最多的一个数它的约数个数为1500个。

约数之和:

有了刚刚的分解,约数的和就是:
( p 1 0 + p 1 1 + . . . + p 1 a 1 ) ∗ . . . ∗ ( p k 0 + p k 1 + . . . + p k a k ) (p_1^0 + p_1^1 + ...+p_1^{a_1}) *...*(p_k^0 +p_k^1+ ...+p_k^{a_k}) (p10+p11+...+p1a1)...(pk0+pk1+...+pkak)
验证用乘法分配律展开即可。

  思路就是分解乘积的质因数等于分解每个因子的质因数,然后累记一下,然后套公式就行了。

#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 1e9 + 7;
typedef unsigned long long ULL;

int main()
{
    unordered_map<int, int> hash;// <pj, ai>
    int n;
    int x;
    cin >> n;
    while (n--)
    {
        cin >> x;
        // 分解x的质因数
        for (int i = 2; i <= x / i; ++i)
        {
            if (x % i == 0)
            {
                while (x % i == 0)
                {
                    ++hash[i];
                    x /= i;
                }
            }
        }
        if (x > 1) ++hash[x];
    }
    ULL res = 1;
    for (auto p : hash) res = (res * (p.second + 1)) % N;
    cout << res << endl;
    return 0;
}

  思路同上,分解每个因子的质因数后,套入约数之和的公式即可。

#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 1e9 + 7;
typedef unsigned long long ULL;

int main()
{
    unordered_map<int, int> hash;// <pj, ai>
    int n;
    int x;
    cin >> n;
    while (n--)
    {
        cin >> x;
        // 分解x的质因数
        for (int i = 2; i <= x / i; ++i)
        {
            if (x % i == 0)
            {
                while (x % i == 0)
                {
                    ++hash[i];
                    x /= i;
                }
            }
        }
        if (x > 1) ++hash[x];
    }
    ULL res = 1;
    for (auto p : hash)
    {
        ULL sum = 0;
        ULL base = 1;
        for (int i = 0; i <= p.second; ++i)
        {
            sum = (sum + base) % N;
            base = (base * p.first) % N;
        }
        res = (res * sum) % N;
    }
    cout << res << endl;
    return 0;
}

3、最大公约数—欧几里得算法

根本原理就是(a, b) = (b, a mod b),证明来说就是左边的公约数集合等于右边的公约数集合。

#include <iostream>
using namespace std;

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

int main()
{
    int n;
    cin >> n;
    int a, b;
    while (n--)
    {
        cin >> a >> b;
        cout << gcd(a, b) << endl;
    }
    return 0;
}

时间复杂度:O(logn)

四、欧拉函数

  什么是欧拉函数呢?
φ ( n ) : 1 − n 中 与 n 互 质 的 数 的 个 数 。 φ(n):1-n中与n互质的数的个数。 φ(n):1nn
  互质指的是公约数只有1的一对自然数。

1 公式法求欧拉函数

  关于欧拉函数,有如下定理:

证明:

为了得到1~N中和N互质的数的个数,

  1. 先从1~N中去掉p1,p2,...pk的所有倍数,他们的个数是N/pi

N − N p 1 − N P 2 − . . . − N p k N - \frac{N}{p_1} - \frac{N}{P_2}-...-\frac{N}{p_k} Np1NP2N...pkN

  1. 有的数既是pi的倍数,又是pj的倍数,这样被减了两次,再加回来:

N − N p 1 − N P 2 − . . . − N p k + N p 1 p 2 + N p 1 p 3 + . . . N - \frac{N}{p_1} - \frac{N}{P_2}-...-\frac{N}{p_k}+\frac{N}{p_1p_2} + \frac{N}{p_1p_3}+... Np1NP2N...pkN+p1p2N+p1p3N+...

  1. 有的数既是pi的倍数,又是pj的倍数,还是pk的倍数,它们会在上上一轮被减三次,在上一轮被加三次,但是实际上我们是要减去它们的,因此再减去所有pi*pj*pk的倍数:

N − N p 1 − N P 2 − . . . − N p k + N p 1 p 2 + N p 1 p 3 + . . . − N p 1 p 2 p 3 − . . . . N - \frac{N}{p_1} - \frac{N}{P_2}-...-\frac{N}{p_k}+\frac{N}{p_1p_2} + \frac{N}{p_1p_3}+...-\frac{N}{p_1p_2p_3}-.... Np1NP2N...pkN+p1p2N+p1p3N+...p1p2p3N....

  这样就找到了一个规律,后面再加上所有四个素数相乘,再减去所有五个素数相乘…。

  发现把式子:
N ( 1 − 1 p 1 ) ( 1 − 1 p 2 ) . . . . ( 1 − 1 p k ) N(1-\frac{1}{p_1})(1-\frac{1}{p_2})....(1-\frac{1}{p_k}) N(1p11)(1p21)....(1pk1)
  展开即为答案。

  首先我们要获得每个数的质因数,前面一节学习过了,时间复杂度是sqrt(n),然后套公式就可以获得每个数的欧拉函数。

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
    int x;
    while (n--)
    {
        cin >> x;
        int res = x;
        // 分解质因数的同时套用欧拉函数 
        // 为了不出现小数 每一项改为a / p * (p - 1)
        for (int i = 2; i <= x / i; ++i)
        {
            if (x % i == 0)
            {
                res = res / i * (i - 1);
                while (x % i == 0)
                {
                    x /= i;
                }
            }
        }
        if (x > 1) res = res / x * (x - 1);
        cout << res << endl;
    }
    return 0;
}

2 筛法求欧拉函数

  我们在某些情况下需要求1-n每个数的欧拉函数,如果我们对每个数套一遍公式,那么时间复杂度会是O(n*sqrt(n))非常的慢,我们可以用筛法来获得欧拉函数,它的时间复杂度可以降低到O(n).

LL get_Eulers(int n)
{
    Euler[1] = 1;// 和1互质的数只有它自己 就1个
    for (int i = 2; i <= n; ++i)
    {
        if (!st[i])
        {
            // 走到这里 i是质数 质数的欧拉函数就是i - 1
            // 1 ~ i - 1都是和它互质的数
            Euler[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; ++j)
        {
            // 线性筛法中证明了 不论哪种情况 prime[j]是i * prime[j]的最小质因子
            st[primes[j] * i] = true;
            if (i % primes[j] == 0)
            {
                // 走到这里 primes[j] * i的质因子和i的质因子一样
                // 那么他们的欧拉函数的差别就是一个primes[j]
                Euler[i * primes[j]] = Euler[i] * primes[j];
                break;
            }
            // 否则i中没有primes[j]这个质因子 差一个(primes[j] - 1) / primes[j] * primes[j]
            Euler[primes[j] * i] = Euler[i] * (primes[j] - 1);
        }
    }
}

模板题:

#include <iostream>
#include <numeric>
using namespace std;

typedef long long LL;
const int N = 1e6 + 10;

bool st[N];
int primes[N];
int sz = 0;
int Euler[N];

LL get_Euler(int n)
{
    Euler[1] = 1;// 与1互质的数只有它自己
    // 走一个线性筛法
    for (int i = 2; i <= n; ++i)
    {
        if (!st[i])
        {
            // 记录素数
            primes[sz++] = i;
            // i 是素数 它的欧拉函数等于i - 1
            Euler[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; ++j)
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0)
            {
                // 到这里 首先primes[j]是i * primes[j]的最小质因数
                // 并且i中有primes[j]这个因子 
                // 所以primes[j] * i的欧拉函数仅仅比i的欧拉函数多一个primes[j]
                Euler[primes[j] * i] = Euler[i] * primes[j];
                break;
            }
            // 到这里 i中没有primes[j]的因子 但是primes[j]是i * primes[j]的最小质因子
            // 所以他俩的欧拉函数差了一个 primes[j] * (primes[j] - 1) / primes[j] = primes[j] - 1
            Euler[primes[j] * i] = Euler[i] * (primes[j] - 1);
        }
    }
    LL res = accumulate(Euler + 1, Euler + n + 1, (LL)0);
    return res;
}

int main()
{
    int n;
    cin >> n;
    cout << get_Euler(n) << endl;
    return 0;
}

3 Acwing201.可见的点

  意识到如果x和y都不等于0时,可见点的x和y一定是互质的,并且一对互质的(x, y)可以对应出两个点(对角线除外),所以对每个数i : [1, n],我们可以求出来1~i中所有与它互质的点的个数Euler(i),这就相当于在第一象限的一半位置的某个x上计算可视的点,然后把他们求和再乘2,这里因为(1, 1)这个点在对角线上,所以它乘二后相当于除了计算了自己补上了一个坐标轴上的1个可视点,然后再+1就可以在补上另一个坐标轴上的可视点,这样就够了。

#include <iostream>
#include <numeric>
using namespace std;
typedef unsigned long long ULL;
const int N = 1010;
int primes[N];
int sz = 0;
bool st[N];
int Euler[N];

void get_Euler(int n)
{
    Euler[1] = 1;
    for (int i = 2; i <= n; ++i)
    {
        if (!st[i])
        {
            Euler[i] = i - 1;
            primes[sz++] = i;
        }
        for (int j = 0; primes[j] <= n / i; ++j)
        {
            st[i * primes[j]] = true;
            if (i % primes[j] == 0)
            {
                Euler[i * primes[j]] = Euler[i] * primes[j];
                break;
            }
            Euler[i * primes[j]] = Euler[i] * (primes[j] - 1);
        }
    }
}

int main()
{
    get_Euler(N - 1);
    int cnt, n;
    cin >> cnt;
    for (int i = 1; i <= cnt; ++i)
    {
        cin >> n;
        ULL res = accumulate(Euler + 1, Euler + n + 1, (ULL)0) * 2 + 1;
        printf("%d %d %llu\n", i, n, res);
    }
    return 0;
}

4 数论中的欧拉定理

  若a与n互质,则
a φ ( n ) ( m o d ) n = = 1 a^{φ(n)}(mod)n == 1 aφ(n)(mod)n==1
证明:
假 设 1 − n 中 , 与 n 互 质 的 数 有 : a 1 , a 2 , . . . , a φ ( n ) 那 么 又 因 为 a 与 n 互 质 , 所 以 a ∗ a 1 , a ∗ a 2 , . . . , a ∗ a φ ( n ) 都 与 n 互 质 有 因 为 a 与 n 互 质 , 所 以 a ∗ a 1 , a ∗ a 2 , . . . , a ∗ a φ ( n ) 在 m o d n 的 意 义 下 同 余 于 a 1 , a 2 , . . . , a φ ( n ) 左 边 就 是 a φ ( n ) ∗ a 1 ∗ a 2 ∗ . . . ∗ a φ ( n ) = = a 1 ∗ a 2 ∗ . . . ∗ a φ ( n ) ( m o d . . n ) 又 因 为 a 1 ∗ a 2 ∗ . . . ∗ a φ ( n ) 与 n 互 质 , 所 以 左 右 两 边 消 掉 有 : a φ ( n ) = = 1 ( m o d . . n ) 即 a φ ( n ) ( m o d ) n = 1 假设1-n中,与n互质的数有:a_1,a_2,...,a_{φ(n)}\\ 那么又因为a与n互质,所以a*a_1,a*a_2,...,a*a_{φ(n)}都与n互质\\ 有因为a与n互质,所以a*a_1,a*a_2,...,a*a_{φ(n)}在modn的意义下同余于a_1,a_2,...,a_{φ(n)}\\ 左边就是a^{φ(n)}*a_1*a_2*...*a_{φ(n)} == a_1*a_2*...*a_{φ(n)}(mod..n)\\ 又因为a_1*a_2*...*a_{φ(n)}与n互质,所以左右两边消掉有:\\ a^{φ(n)}==1(mod..n)\\ 即a^{φ(n)}(mod)n = 1 1nna1,a2,...,aφ(n)anaa1,aa2,...,aaφ(n)nanaa1,aa2,...,aaφ(n)modna1,a2,...,aφ(n)aφ(n)a1a2...aφ(n)==a1a2...aφ(n)(mod..n)a1a2...aφ(n)naφ(n)==1(mod..n)aφ(n)(mod)n=1
  推论:若p是质数,且a和p互质,则
a p − 1 ( m o d ) p = 1 a^{p - 1}(mod)p=1 ap1(mod)p=1
  此推论被称为费马小定理。

五、快速幂算法

1 模板题

  快速幂算法是快速的求出来a^k % p的结果,时间复杂度为O(logk),数据范围为1 <= a,p,k <= 1e9.

  思路就是先预处理出一下内容,然后进行二进制位组合:

  一个例子:

#include <iostream>
using namespace std;
typedef long long LL;

int quick_power(int a, int b, int p)
{
    int res = 1;
    // 本质就是要求b的二进制表示
    while (b != 0)
    {
        // 如果当前b的最低位为1 则让res和a乘一下 记得由于会爆int强转一下
        if (b & 1) res = (LL)res * a % p;
        b >>= 1;
        // a自己乘2 表示走到下一位
        a = (LL)a * a % p;
    }
    return res;
}


int main()
{
    int n;
    scanf("%d", &n);
    int a, b, p;
    while (n--)
    {
        scanf("%d%d%d", &a, &b, &p);
        int res = quick_power(a, b, p);
        printf("%d\n", res);
    }
    return 0;
}

  之前我学习快速幂写出的方法:

#include <iostream>
using namespace std;
typedef unsigned long long LL;
int main()
{
    int n;
    scanf("%d", &n);
    LL base, power, p;
    while (n--)
    {
        scanf("%ld%ld%ld", &base, &power, &p);
        LL res = 1;
        while (power > 0)
        {
            if (power & 1)
            {
                res = (res * base) % p;
                power--;
            }
            power >>= 1;
            base = (base * base) % p;
        }
        printf("%ld\n", res);
    }
    return 0;
}

  测试来看,好像会更快一些:

2 快速幂求模n(n是质数)意义下的逆元

  题目描述比较绕,实际上就是
假 设 b ∣ a , b 与 n 互 质 a / b 因 为 除 法 的 运 算 比 较 麻 烦 , 我 们 希 望 转 而 计 算 乘 法 做 到 ( a / b ) % n = ( a ∗ x ) % n 找 到 这 个 x a / b = = a ∗ b − 1 ( m o d ( n ) ) 假设b|a,b与n互质\\ a/b因为除法的运算比较麻烦,我们希望转而计算乘法\\ 做到(a/b)\%n = (a * x) \%n\\ 找到这个x\\ a / b == a * b^{-1}(mod(n)) ba,bna/b(a/b)%n=(ax)%nxa/b==ab1(mod(n))
  性质:(两边同乘b就可以得证)
b ∗ b − 1 % n = 1 b*b^{-1}\%n = 1 bb1%n=1
  题目中假设了n = p是个质数,由费马小定理:
b p − 1 = = 1 ( m o d ( p ) ) b ∗ b p − 2 = = 1 ( m o d ( p ) ) b^{p - 1} ==1(mod(p))\\ b * b^{p - 2} == 1(mod(p)) bp1==1(mod(p))bbp2==1(mod(p))
  所以逆元就是b^{p - 2}

  所以本题就是考察的a^{p - 2} % p,考察的就是一个快速幂算法。

  一个例子a = 3,p = 5.
a p − 2 = 3 3 = 27 , 27 % 5 = 2 a − 1 = 2 ( m o d 5 ) a ^ {p - 2} = 3 ^ 3 = 27,27\%5 = 2\\ a^{-1} = 2(mod5) ap2=33=27,27%5=2a1=2(mod5)
  注意,当ap不互质时,因为p是质数,所以a % p == 0时,对任何数xa * x == 0(mod p),所以肯定不存在逆元。注意检查边界即可。

#include <iostream>
using namespace std;

typedef long long LL;

int quick_power(int a, int b, int p)
{
    int res = 1;
    while (b > 0)
    {
        if (b & 1)
        {
            res = (LL)res * a % p;
            --b;
        }
        b >>= 1;
        a = (LL)a * a % p;
    }
    return res;
}

int main()
{
    int n;
    scanf("%d", &n);
    int a, p;
    while (n--)
    {
        scanf("%d%d", &a, &p);
        if (a % p) printf("%d\n", quick_power(a, p - 2, p));
        else puts("impossible");
    }
    return 0;
}

六、扩展欧几里得算法

1 裴蜀定理

  有一对正整数a,b,那么一定存在非0整数x,y,使得
a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)
证明:因为a是gcd(a, b)的倍数,b是gcd(a, b)的倍数,所以ax + by组合出来的数一定是gcd(a, b)的倍数。

  只要证明出gcd(a, b)一定能被组合出来就行,这个组合构造的方法就是扩展欧几里得算法。

#include <iostream>
using namespace std;


int exgcd(int a, int b, int& x, int& y)
{
    if (b == 0)
    {
        x = 1, y = 0;
        return a;
    }
    // 递归时求得是b和a%b的系数 分别是y和x
    // 那么本来的系数x和y就可以整理得到:
    // by + (a%b)x = gcd(a, b)
    // by + (a - a / b * b)x = gcd(a, b)
    // ax + b(y - a / b * x) = gcd(a, b)
    //所以本来的x = x,y = y - a / b * x
    int d = exgcd(b, a % b, y, x);
    // x系数不变 y系数减少a / b * x
    y -= a / b * x;
    return d;
}

int main()
{
    int n, a, b, x, y;
    scanf("%d", &n);
    while (n--)
    {
        scanf("%d%d", &a, &b);
        exgcd(a, b, x, y);
        printf("%d %d\n", x, y);
    }
    return 0;
}

  注意本解是不唯一的,当我们求出一组x0y0时,就可以如下再构造出全部解:
a ( x 0 − k ∗ b g c d ( a , b ) ) + b ( y 0 + k ∗ a g c d ( a , b ) ) = g c d ( a , b ) , k 属 于 Z a(x_0 - k*\frac{b}{gcd(a, b)}) + b(y_0 + k*\frac{a}{gcd(a,b)}) = gcd(a, b),k属于Z a(x0kgcd(a,b)b)+b(y0+kgcd(a,b)a)=gcd(a,b),kZ
  全部解的证明思路如下:我们已经获得了特解,根据线性方程组的理论,接下来我们只要求出ax + by = 0的通解,就能得到原方程的通解。
a x + b y = 0 的 通 解 是 x = − b a K , y = K , K 为 任 意 整 数 为 了 让 x 和 y 一 定 是 整 数 , 令 K = a g c d ( a , b ) k 则 得 到 x = − b g c d ( a , b ) k , y = a g c d ( a , b ) k 所 以 原 方 程 通 解 为 : x = x 0 − k ∗ b g c d ( a , b ) , y = y 0 + k ∗ a g c d ( a , b ) ax+by=0的通解是\\ x = -\frac{b}{a}K,y = K,K为任意整数\\ 为了让x和y一定是整数,令K=\frac{a}{gcd(a, b)}k\\ 则得到x = -\frac{b}{gcd(a, b)}k,y=\frac{a}{gcd(a, b)}k\\ 所以原方程通解为:x = x_0 - k*\frac{b}{gcd(a, b)}, y = y_0 + k*\frac{a}{gcd(a,b)} ax+by=0x=abK,y=K,Kxy,K=gcd(a,b)akx=gcd(a,b)bk,y=gcd(a,b)akx=x0kgcd(a,b)b,y=y0+kgcd(a,b)a

2 求解线性同余方程

思路:

#include <iostream>
using namespace std;
typedef long long LL;

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, a, b, m, d, x, y;
    scanf("%d", &n);
    while (n--)
    {
        scanf("%d%d%d", &a, &b, &m);
        d = exgcd(a, m, x, y);
        if (b % d == 0) printf("%d\n", (LL)x * b / d % m);
        else puts("impossible");
    }
    return 0;
}

七、中国剩余定理

1 中国剩余定理

  中国剩余定理的表述如下:

证明:

2 模板题

观 察 同 余 方 程 组 中 的 两 个 方 程 : x % a 1 = m 1 x % a 2 = m 2 转 化 为 : x = k 1 a 1 + m 1 x = k 2 a 2 + m 2 联 立 有 : k 1 a 1 − k 2 a 2 = m 2 − m 1 它 有 解 等 价 于 m 2 − m 1 % g c d ( a 1 , a 2 ) = = 0 假 设 得 到 了 一 组 解 k 10 , k 20 全 部 通 解 就 是 : k 10 + K ∗ a 2 g c d ( a 1 , a 2 ) , k 20 + K ∗ a 1 g c d ( a 1 , a 2 ) 代 入 原 方 程 , 得 到 x = k 10 a 1 + K ∗ [ a 1 , a 2 ] + m 1 = ( m 1 + k 10 a 1 ) + K ∗ [ a 1 , a 2 ] = x 0 + K a 这 样 就 把 两 个 方 程 化 成 了 一 个 方 程 , 这 样 n − 1 次 就 能 把 方 程 变 成 1 个 方 程 , 在 转 化 回 同 余 方 程 : x = = x 0 ( m o d ( a ) ) , 就 是 求 x 0 % a 的 正 的 余 数 就 行 . 观察同余方程组中的两个方程:\\ x\%a_1=m_1\\ x\%a_2=m_2\\ 转化为:\\ x = k_1a_1 + m_1\\ x = k_2a_2 + m_2\\ 联立有:\\ k_1a_1-k_2a_2=m_2-m_1\\ 它有解等价于m_2 - m_1 \% gcd(a_1, a_2) == 0\\ 假设得到了一组解k_{10},k_{20}\\ 全部通解就是:k_{10} + K*\frac{a_2}{gcd(a_1,a_2)}, k_{20} + K*\frac{a_1}{gcd(a_1,a_2)}\\ 代入原方程,得到x=k_{10}a_1+K*[a_1,a_2]+m_1 = (m_1+k_{10}a_1) + K*[a_1,a_2]\\ =x_0+Ka\\ 这样就把两个方程化成了一个方程,这样n-1次就能把方程变成1个方程,在转化回同余方程:\\ x == x_0 (mod(a)),就是求x_0\%a的正的余数就行.\\ :x%a1=m1x%a2=m2:x=k1a1+m1x=k2a2+m2:k1a1k2a2=m2m1m2m1%gcd(a1,a2)==0k10,k20:k10+Kgcd(a1,a2)a2,k20+Kgcd(a1,a2)a1x=k10a1+K[a1,a2]+m1=(m1+k10a1)+K[a1,a2]=x0+Kan11,:x==x0(mod(a)),x0%a.

#include <iostream>
using namespace std;
typedef long long LL;

LL exgcd(LL a, LL b, LL& x, LL& y)
{
    if (b == 0)
    {
        x = 1, y = 0;
        return a;
    }
    LL d = exgcd(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 < n - 1; ++i)
    {
        LL a2, m2;
        cin >> a2 >> m2;
        // 对方程 k1a1 - k2a2 = m2 - m1进行欧几里得算法
        LL k1, k2;
        LL d = exgcd(a1, a2, k1, k2);
        if ((m2 - m1) % d)
        {
            has_answer = false;
            break;
        }
        //得到乘倍数得到解k1
        k1 *= (m2 - m1) / d;
        // 为了防止溢出 先把k1变到最小
        LL t = a2 / d;
        k1 = (k1 % t + t) % t;
        // 得到新方程的m1
        m1 = m1 + k1 * a1;
        // 得到新方程的a1 a1变成a1和a2的最小公倍数
        a1 = abs(a1 / d * a2);
    }
    if (has_answer)
    {
        // 解一个方程x % a1 = m1的最小整数解 m1 % a1的整数解即可
        printf("%lld\n", (m1 % a1 + a1) % a1);
    }
    else puts("-1");
    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
ava实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),可运行高分资源 Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言用于封装代码的单元,可以实现代码的复用和模块化。C语言定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言用于存储同类型数据的结构,可以通过索引访问和修改数组的元素。字符串是C语言用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值