天天看点

基础数论知识总结

1.  费马小定理与扩展欧几里得在乘法逆元上的运用

费马小定理

a phi(n)−1 ≡1(modn) 

扩展欧几里得

a⋅x≡1(modn) 

乘法逆元

针对 ba modn  这种除法取模,将它转换为乘法取模,我们需要用到之前的费马小定理和扩展欧几里得,这两种方法都可以将除法取模变为乘法取模,什么时候用哪种,后续会说到

费马小定理处理乘法逆元

f(n)=ba modn(1) 

a phi(n)−1 ≡1(modn)(2) 

1=h(n)=a phi(n)−1 modn(3)←(2) 

由 f(n)  和 h(n)  相乘可以得到如下公式:

f(n)⋅h(n)=f(n)⋅1=b⋅a phi(n)−2 modn(4) 

其中 phi(n)  为欧拉函数即小于n并且与n互质的个数

如果 n  为质数的话,phi(n) 很明显为 n−1  ,因为任意正整数都要质数互质,所以公式就变成了 b⋅a n−2  

如此我们就可以通过快速幂来处理 b⋅a n−2  

扩展欧几里得处理乘法逆元

f(n)=ba modn(1) 

a⋅x≡1(modn)(2) 

1=h(n)=a⋅xmodn(3)←(2) 

由 f(n)  和 h(n)  相乘可以得到如下公式:

f(n)⋅h(n)=f(n)⋅1=b⋅xmodn(4) 

求出 x  后就可以将ba modn 求出.

在求解 x  的过程中(2) 式可以看成是 a⋅x+n⋅y=1  .

a⋅x≡1(modn)(2) 

a⋅x−n⋅y=1(5)←(2) 

a⋅x+n⋅y=1(6)←(5) 

由 (6)  式可以看出要想 x  和y 存在整数解 gcd(a,b)  必须等于 1 

不管是费马小定理还是扩展欧几里得如果要进行乘法逆元的话必须要满足一个条件gcd(a,n)==1 即 a  和n 必须要互质否则两个公式都是不能够使用的. [  扩展欧几里得算法下述有讲解] 

何时使用处理乘法逆元

1.  当 n  为质数的时候一般用费马小定理,因为 b n−2 modn 

2.  当时间限制比较紧的时候使用费马小定理,因为它可以使用快速幂进行求解

3.  其它的情况则是依照题目要求而定,当然,使用费马小定理处理乘法逆元的比较多,扩展欧几里得在求解方程式的整数解上有着强大的作用

2.  扩展欧几里得真解

欧几里得算法

在讲解扩展欧几里得之前

讲解欧几里得算法是必要的,所谓的欧几里得即求解最大公约数。

算法代码:

/*
    作用:求解最大公约数(欧几里得)
    方法:辗转相除法
*/
int gcd(int a, int b){
    return b ? gbc(b, a % b) : a;
}
           
扩展欧几里得算法

扩展欧几里得可以求解出 a⋅x+b⋅y=gcd(a,b)  中一个满足方程式的 [  整数] 特解

g=gcd(a,b)(1) 

a⋅x+b⋅y=g(2) 

b⋅x1+(amodb)⋅y1=g(3) 

我们需要针对 (2)  和 (3)  找出他们之间的规律

很明显

amodb=a−⌊ab ⌋⋅b(4) 

所以由 (3)  和 (4)  可得 (5) 

b⋅x1+(a−⌊ab ⌋⋅b)⋅y1=g(5) 

化简 (5)  为 (6) 

a⋅y1+b⋅(x1−⌊ab ⌋⋅y1)=g(6) 

我们联合 (2)  和 (6)  ,可以得出下述公式:

x=y1 

y=x1−⌊ab ⌋⋅y1 

其中我们书写代码的终止条件:

gcd(a,0)=a  为其终止点

a⋅1+0⋅0=a 

所以当 b=0  时, x=1  , y=0 

如此我们就可以写出求解扩展欧几里得算法的递归代码:

void ex_gcd(int a,int b, int &d, int &x, int &y){
    if(!b){
        x = 
        y = ;
        d = a;
        return;
    }
    ex_gcd(b, a % b, d, y, x);
    y -= x * (a / b);
}
           

最终求出的 x  ,y 为 a⋅x+b⋅y=gcd(a,b)  的一个特解

然而解是可能有多个,因为 a⋅x+b⋅y=gcd(a,b)  可能是条直线

所以如何得知其它解呢

我们假设如果存在另外一个解 x2  , y2  ,那么可以列出下列方程:

a⋅(x1−x2)=b⋅(y2−y1) 

两边同时除以g

a ′ ⋅(x1−x2)=b ′ ⋅(y2−y1) 

其中 a ′ =ag   , b ′ =bg   ,且 a ′   和 b ′   一定互素,那么 (x1−x2)  一定为 b ′   的倍数,即 (x1−x2)=k⋅a ′   ,代入公式得

(y2−y1)=k⋅a ′  

所以 a⋅x+b⋅y=gcd(a,b)  的一个特解为 (x0,y0)  的话,那么其任意整数解都可以写成 (x0+k⋅b ′ ,y0−k⋅a ′ ) 

如果我们要求解 a⋅x+b⋅y=c  的整数解呢 ? 

大家也应该想到了答案就是(x0⋅cg ,y0⋅cg ) 

如果 cmodg≠0  则表示没有整数解。

3.  欧拉函数

欧拉函数,缩写 ϕ  ,程序中一般用 phi(n)  来表示,求解出小于n且与n互质的正整数的个数

我们针对一个 n  ,可以将它化为如下式子:

n=p q 1  1 ⋅p q 2  2 ⋅p q 3  3 ⋅⋅⋅⋅p q n  n  

从而可以得出如下公式:

phi(n)=n⋅(1−1p 1  )⋅(1−1p 2  )⋅(1−1p 3  )⋅⋅⋅⋅(1−1p n  ) 

简化:

phi(n)=n⋅(p 1 −1p 1  )⋅(p 2 −1p 2  )⋅(p 3 −1p 3  )⋅⋅⋅⋅(p n −1p n  ) 

通过下述公式我们可以写出如下代码:

/*
 *函数名称:phi
 *参数n:需要求解的欧拉函数值
*/
int phi(int n){
    int res = n;//int -> LL
    for(int i = ;i <= sqrt(n);i ++){
        if(n % i == ){
            res = res / i * (i - );
            while(n % i == ) n /= i;
        }
    }
    if(n > ) res = res / n * (n - );
    return res;
}
           

先进行除法是为了防止中间数据溢出

欧拉函数求出的结果有以下几种性质:

1.  除了 n=2  外, phi(n)≡0(mod2)  即 phi(n)  求出的结果为 [  偶数] 

2.phi(n)  为积性函数,但不是完全积性函数, phi(n⋅m)=phi(n)⋅phi(m)  当且仅当 gcd(n,m)==1  时成立

3.  一个数的所有质因子之和为: phi(n)⋅n2   .因为 gcd(n,i)==1  所以 gcd(n,n−i)==1  ,如此 f(n)=(a 1 +n−a 1 )+(a 2 +n−a 2 )+⋅⋅⋅+(a n +n−a n )  由于 a n   和 n−a n   都是与 n  互质,所以最终的结果f(n)=phi(n)⋅n ,由于算了两倍质数,所以最后要除以 2  ,得出phi(n)⋅n2  .

单单以第一份代码求解的话,求解一个数的欧拉函数值的复杂度为 O(sqrt(n))  ,所以求解n范围内每一个数的欧拉函数值的复杂度为 O(n⋅sqrt(n))  ,很明显,当数据大于 10 5   以上后明显是不够看的。我们可以根据求解大范围素数的方法求解欧拉函数值即筛选法求欧拉函数值.

代码如下:

const int MAXN =  + ;
int phiArr[MAXN];

void phi(void) {
    phiArr[] = ;
    for(int i = ; i < MAXN; i ++) {
        phiArr[i] = i;
    }
    for(int i = ; i < MAXN; i ++) {
        if(phiArr[i] == i) {
            for(int j = i; j < MAXN; j += i) {
                phiArr[j] = phiArr[j] / i * (i - );
            }
        }
    }
}
           
积性函数,何为积性函数?

对于正整数 n  的一个算术函数f(n) ,若 f(1)=1  ,且当 a  ,b 互质时 f(a⋅b)=f(a)⋅f(b)  ,在数论上就称它为积性函数,若对于某积性函数 f(n)  ,就算 a  , b 不互质,也有 f(a⋅b)=f(a)⋅f(b)  ,则称它为完全积性函数

4.  同余与模

1.(a+b)modn=((amodn)+(bmodn))modn 

2.(a−b)modn=((amodn)−(bmodn)+n)modn 

3.(a⋅b)modn=(amodn)⋅(bmodn)modn 

常见同余与模例子:

1234=((1×10+2)×10+3)×10+4 

如果 1234  对 n  取模的话:

1234modn=((1×10modn+2modn)×10modn+3modn)×10modn+4modn 

5.  容斥定理

|A 1 ⋃A 2 ⋃A 3 ⋅⋅⋅A m |=∑ 1≤i≤m |A i |−∑ 1≤i<j≤m |A i ⋂A j |+∑ 1≤i<j<k≤m |A i ⋂A j ⋂A k |+⋅⋅⋅+(−1) m−1 ⋅∑ 1≤i 1 <i 2 <i 3 ⋅⋅⋅i n ≤m |A i 1  ⋂A i 2  ⋂A i 3  ⋅⋅⋅⋂A i n  | 

它的计算规律无非四个字:奇加偶减即奇数个则加,偶数个则减

用法:

容斥定理主要在处理质因子方面有着不菲的作用,大家不要纠结容斥定理的由来,只需记住针对每一个集合,奇加偶减即可

如何实现容斥定理?

1.  二进制状态压缩

代码:

void solve(){
    for(int i = ;i <  << m;i ++){
        int bits = ;
        for(int j = ;j < m;j ++){
            if(i >> j & ){
                bits ++;
                //根据题目而进行操作
            }
        }
        if(bits & ) {
            //奇数进行加法处理
        }
        else{
            //偶数进行减法处理
        }
    }
}
           

2.  DFS处理

代码:待补充

3.  队列数组

代码:待补充

常见用法

1.  求解 [1,r]  中与 n  不互质的个数

int solve(int r) {
    int sum = ;
    for(int i = ; i <  << m; i ++) {
        int bits = , muls = ;
        for(int j = ; j < m; j ++) {
            if(i >> j & ) {
                bits ++;
                muls *= primes[j];
            }
        }
        int cur = r / muls;
        if(bits & ) {
            sum += cur;
        } else {
            sum -= cur;
        }
    }
    return sum;
}
           

6. 矩阵运算

矩阵运算规则

1.  矩阵相乘

⎡ ⎣ ⎢ x1y1z1 x2y2z2 x3y3z3 ⎤ ⎦ ⎥ ⋅⎡ ⎣ ⎢ xx1yt1zz1 xx2yy2zz2 xx3yy3zz3 ⎤ ⎦ ⎥  

针对上述矩阵相乘运算的两个矩阵必须满足一个条件,前者的行等于后者的列,同时如果前者为 n×m  ,后者为 k×n  ,最后形成矩阵的行列则为 k×n 

最后形成矩阵的某一行为 n  某一列为m ,那么这个点的值则为第一个矩阵第 n  行和第二个矩阵第m 列一一相乘,即 mat(n,m)=mat1[n−1][0]⋅mat2[0][m−1]+mat1[n−1][1]⋅mat2[1][m−1]+⋅⋅⋅+mat1[n−1][k]⋅mat2[k][m−1] 

这里要记清楚,两矩阵相乘,前者的行等于后者的列,所有都为 k 

2. 矩阵相加和相减

⎡ ⎣ ⎢ x1y1z1 x2y2z2 x3y3z3 ⎤ ⎦ ⎥ +/−⎡ ⎣ ⎢ xx1yt1zz1 xx2yy2zz2 xx3yy3zz3 ⎤ ⎦ ⎥  

运算规则为对应的行列相加或相减

矩阵快速幂

和单纯的求数字的幂的快速幂的原理是一样的

代码:

const int mod = ;
typedef long long LL;
typedef vector<LL>vec;
typedef vector<vec>mat;

/*
 *函数名称:mul
 *参数A:进行矩阵乘法的第一个矩阵
 *参数B:进行矩阵乘法的第二个矩阵
*/
mat mul(mat &A, mat &B) {
    mat C(A.size(), vec(B[].size()));
    for(int i = ; i < A.size(); i ++) {
        for(int k = ; k < B.size(); k ++) {
            for(int j = ; j < B[].size(); j ++) {
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % mod;
            }
        }
    }
    return C;
}
/*
 *函数名称:pow
 *参数A:进行快速幂矩阵
 *参数n:幂的次数
*/
mat pow(mat A, LL n) {
    mat B(A.size(), vec(A.size()));
    for(int i = ; i < A.size(); i ++) B[i][i] = ;
    while(n) {
        if(n & ) B = mul(B, A);
        A = mul(A, A);
        n >>= ;
    }
    return B;
}
           

7.  康托展开及其逆运算

用法

给予一个目标序列,求解他的全排列过程中,目标序列是第几个排列

康托展开–演示案例

找出序列 [4,5,2,3,1]  在这个排列中的是第几个排列

整个序列中比 4  小的数有3 个

整个序列中比 5  小的数有4 个但 4  已经在之前出现过了所以是3 个

整个序列中比 2  小的数有1 个

整个序列中比 3  小的数有两个但2 已经在之前出现过了所以是 1  个

整个序列中比1 小的数有 0  个

那么[4,5,2,3,1] 在这个排列中的顺序是 3⋅4!+3⋅3!+1⋅2!+1⋅1!+0⋅0!+1=94 

我们可以发现,所谓的第几个则是在这个数出现之前未出现的数中少于某个这个数的个数乘以该位的阶乘。

将问题简化,我们最终就是求 [  该位之后有多少个比该位小的数] 。

实现代码:

typedef long long LL;
LL F[ + ];
void init(){
    F[] = ;
    for(int i = ;i <= ;i ++){
        F[i] = F[i - ] * i;
    }
}
/*
 *函数名称:cd_cal
 *参数A:进行判断序列
 *参数n:序列长度
*/
LL cd_cal(int *A, int n){
    LL ret = L;
    for(int i = ;i < n;i ++){
        int num = ;
        for(int j = i + ;j < n;j ++){
            if(A[i] > A[j]) num ++;
        }
        ret += F[n - i - ] * num;
    }
    return ret;
}
           
康托逆展开

所谓康托逆展开即给予一个长度为n的序列,让你求出第m个排列是什么

康托逆展开–演示案例

一个数量为 5  的排列,现在要你找出第96 种排序序列是什么

首先用 96−1  得到 95  (将他本身这一个种给删掉,可以直接判断)

用 95  去除 4!  得到 3  余23 

用 23  去除 3!  得到 3  余5 

用 5  去除2! 得到 2  余1 

用 1  去除1! 得到 1  余0 

有 3  个数比它小的数是4 

所以第一位是 4 

有3 个数比它小的数是 4  但4 已经在之前出现过了所以是 5  (因为4 在之前出现过了所以实际比 5  小的数是3 个)

有 2  个数比它小的数是3 

有 1  个数比它小的数是2 

最后一个数只能是 1 

所以这个数是[4,5,3,2,1] 

在这里我们又可以将问题简化为求解 [  从1开始计数有多少个没有被访问过的数] .

实现代码:

typedef unsigned long long uLL;
uLL F[ + ],B[ + ];
bool vis[ + ];
void init() {
    F[] = ;
    for(int i = ; i <= ; i ++) {
        F[i] = F[i - ] * (uLL)i;
    }
}
/*
 *函数名称:cd_cal
 *参数A:排列结果保存数组
 *参数n:序列长度
 *参数v:第几种排列
*/
void cd_cal(uLL *A, uLL n, uLL v) {
    memset(vis, false, sizeof(vis));
    v --;
    for(int i = ; i < n; i ++) {
        uLL k = v / F[n - i - ];
        v %= F[n - i - ];
        int c = k + , a = , f = -;
        while(a <= c) {
            f ++;
            if(!vis[f]) a ++;
        }
        A[i] = f;
        vis[f] = true;
    }
}
           

8.  错排原理

何为错排?

给定一个 n  长的序列,让每一个数都不在原来位置上的摆放次数.

错排公式

D(n)=(n−1)⋅[D(n−2)+D(n−1)] 

其中 D(n)  表示将 n  个物品进行错排可以有多少种摆放方式.

错排公式原理讲解
  1. 第一种理解方法

    分为两种情况:

    1. 在原来i−1 个已经进行了错排处理的物品的基础上多加了一个元素 k  ,这个元素可以放在i−1 个已经进行了错排处理的序列中任意一个位置,所以有 (i−1)⋅D(i−1)  种.
      • 如果 i−1  个数中有一个没有进行错排,那么这个数与 k  进行交换,接着剩下的所有元素进行错排有(i−1)⋅D(i−2) 个.
      • 如此答案为 D(n)=(n−1)⋅[D(n−2)+D(n−1)]  .
      • 第二种理解方法

        第一步,将第 n  个元素放在任意一个位置,比如位置k ,一共有 n−1  种放的方法.

        第二步,由于位置 k  处已经被第n 个元素给占据了,所以原本在位置 k  的元素需要放置在其他位置.

        这里分为两种情况:

        1. 将原本位置k 的元素直接放置在原本第 n  个元素所在的位置,则有(i−1)⋅D(n−2) 种.
        2. 在第一种情况的基础上,将已经放置在位置 k  的第n 个元素和这个位置 k  给删除掉,如此我们又重新得到一个新的序列,这个序列的长度减少了一个,同时没有了第n 个元素,原本在位置 k  的元素取代了第n 个元素,将这 n−1  个元素进行错排后再将原本删除掉的位置 k  和第n 个元素放回来就构成了 (i−1)⋅D(n−1)  种.
        3. 如此答案为 D(n)=(n−1)⋅[D(n−2)+D(n−1)]  .

          9.  lucas  定理

          何为 lucas  定理
          用于求解 C(nm)%p  ,其中 p  是质数.
          公式

          lucas(n,m,p)=lucas(np ,mp ,p)×C(n%pm%p) 

          lucas(n,0,p)=1 

          证明
          没明白,事后补上
          代码模板
          LL mod_pow(LL x, LL n, LL mod){
              LL ret = ;
              while(n > ){
                  if(n & ) ret = ret * x % mod;
                  x = x * x % mod;
                  n >>= ;
              }
              return ret;
          }
          
          LL C(LL m, LL n, LL p){
              if(n < m) return ;
              if(m > n - m){
                  m = n - m;
              }
              LL up = , down = ;
              for(LL i = ;i < m;i ++){
                  up = up * (n - i) % p;
                  down = down * (i + ) % p;
              }
              return up * mod_pow(down, p - , p) % p;// 
          }
          
          LL lucas(LL m, LL n, LL p){
              if(m == ) return ;
              return C(m % p, n % p, p) * lucas(m / p, n / p, p) % p;
          }
                     
          知识点未完,敬请期待!