Skip to content

数学

向上取整

ab=a+b1b

C++
int ceil = (a + b - 1) / b
int ceil = (a + b - 1) / b

加法原理

完成一个工程可以有 n 类办法, ai(1in) 代表第 i 类方法的数目。那么完成这件事共有 S=a1+a2++an 种不同的方法。

乘法原理

完成一个工程需要分 n 个步骤,ai(1in) 代表第 i 个步骤的不同方法数目。那么完成这件事共有 S=a1×a2××an种不同的方法。

算术基本定理

任何一个大于 1正整数都能分解为有限个质数的乘积:

n=p1α1×p2α2××pkαk,p1<p2<<pk

其中 αiZpi 都是质数,且 p1<p2<pk ;在不计次序的意义下,该表示唯一。

裴蜀定理

裴蜀定理,又称贝祖定理(Bézout's lemma)。是一个关于最大公约数的定理。

其内容是:

a,b不全为零的整数,则存在整数 x,y ,使得 ax+by=gcd(a,b)

平方和公式

i=1ni2=12+22++n2=n(n+1)(2n+1)6

质数

试除法判质数

模板代码:

Java
boolean isPrime(int n) {
    if (n < 2) return false;
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) return false;
    }
    return true;
}
boolean isPrime(int n) {
    if (n < 2) return false;
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) return false;
    }
    return true;
}
C++
bool is_prime(int n) {
    if (n < 2) return false;
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) return false;
    }
    return true;
}
bool is_prime(int n) {
    if (n < 2) return false;
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) return false;
    }
    return true;
}

时间复杂度:O(n)

试除法分解质因数

分解质因数(Prime Factorization):根据算术基本定理,每个合数都可以写成几个质数相乘的形式,其中每个质数都是这个合数的因数,把一个合数用质因数相乘的形式表示出来,叫做分解质因数。

模板题:【数学】分解质因数

原理:试除法,枚举 2n 中的所有质数,如果 in质因子,就把 n 中的 i 除干净。

模板代码:

Java
void pf(int n) {
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) {
            int s = 0;
            while (n % x == 0) {
                n /= x;
                ++s;
            }
            System.out.println(x + " " + s);
        }
    }
    if (n > 1) System.out.println(n + " 1");
}
void pf(int n) {
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) {
            int s = 0;
            while (n % x == 0) {
                n /= x;
                ++s;
            }
            System.out.println(x + " " + s);
        }
    }
    if (n > 1) System.out.println(n + " 1");
}
C++
void pf(int n) {
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) {
            int s = 0;
            while (n % x == 0) {
                n /= x;
                ++s;
            }
            printf("%d %d\n", x, s);
        }
    }
    if (n > 1) printf("%d 1\n", n);
}
void pf(int n) {
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) {
            int s = 0;
            while (n % x == 0) {
                n /= x;
                ++s;
            }
            printf("%d %d\n", x, s);
        }
    }
    if (n > 1) printf("%d 1\n", n);
}

时间复杂度:

  • 最差: O(n)
  • 最好: O(logN)

其中枚举的 i 一定是质数,因为:当枚举到 i 的时候,已经把 [2,i1] 中的质因子都除掉了,当 if (n % i == 0) 成立的时候,n 中已经没有任何在 [2,i1] 范围内的质因子,又因为 ni 的倍数,所以 i 也没有任何在 [2,i1] 范围内的因子,所以此时 i 一定是质数。

筛质数

模板题:【数学】筛质数

朴素筛

原理:

  • 枚举 x[2,n]x 的倍数一定是合数,所以可以在枚举到 x 的时候筛掉所有 小于等于 nx 的倍数
  • 而在枚举到 p[2,n] 的时候,如果 p 还没有被筛掉,说明 p 不是 x[2,p1] 中任意一个数的倍数,所以 p 一定是质数。

模板代码:

Java
int countPrimes(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    for (int x = 2; x <= n; ++x) {
        if (!isNotPrime[x]) primes[cnt++] = x;
        for (int k = 2; k * x <= n; ++k) isNotPrime[k * x] = true;
    }
    return cnt;
}
int countPrimes(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    for (int x = 2; x <= n; ++x) {
        if (!isNotPrime[x]) primes[cnt++] = x;
        for (int k = 2; k * x <= n; ++k) isNotPrime[k * x] = true;
    }
    return cnt;
}

时间复杂度:O(NlogN)

埃氏筛

原理:要得到自然数 n 以内的全部质数,把不大于 n 的所有质数倍数筛除,剩下的就都是质数。

证明:根据算术基本定理每一个大于 1 的正整数都能分解成有限个质数的幂的乘积,且由于是从小到大枚举,在枚举到某个合数之前,一定先会枚举到它的质因数,也就是说所有的合数都会被它的质因数筛掉。

New_Animation_Sieve_of_Eratosthenes.gif

模板代码:

Java
int countPrimes(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    for (int x = 2; x <= n; ++x) {
        if (!isNotPrime[x]) {
            primes[cnt++] = x;
            for (int k = 2; k * x <= n; ++k) isNotPrime[k * x] = true;
        }
    }
    return cnt;
}
int countPrimes(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    for (int x = 2; x <= n; ++x) {
        if (!isNotPrime[x]) {
            primes[cnt++] = x;
            for (int k = 2; k * x <= n; ++k) isNotPrime[k * x] = true;
        }
    }
    return cnt;
}

时间复杂度:O(NloglogN)

线性筛(欧拉筛)

原理:线性筛保证了每个合数都只被其最小质因子筛掉。

模板代码:

Java
int countPrimes(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    for (int x = 2; x <= n; ++x) {
        if (!isNotPrime[i]) primes[cnt++] = x;
        for (int i = 0; primes[i] <= n / x; ++i) {
            isNotPrime[primes[i] * x] = true;
            if (x % primes[i] == 0) break;
        }
    }
    return cnt;
}
int countPrimes(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    for (int x = 2; x <= n; ++x) {
        if (!isNotPrime[i]) primes[cnt++] = x;
        for (int i = 0; primes[i] <= n / x; ++i) {
            isNotPrime[primes[i] * x] = true;
            if (x % primes[i] == 0) break;
        }
    }
    return cnt;
}
C++
int cnt, primes[N];
bool np[N];

int sieve(int n) {
    for (int x = 2; x <= n; ++x) {
        if (!np[x]) primes[cnt++] = x;
        for (int i = 0; primes[i] <= n / x; ++i) {
            np[primes[i] * x] = true;
            if (x % primes[i] == 0) break;
        }
    }
    return cnt;
}
int cnt, primes[N];
bool np[N];

int sieve(int n) {
    for (int x = 2; x <= n; ++x) {
        if (!np[x]) primes[cnt++] = x;
        for (int i = 0; primes[i] <= n / x; ++i) {
            np[primes[i] * x] = true;
            if (x % primes[i] == 0) break;
        }
    }
    return cnt;
}

时间复杂度:O(N)

线性筛是如何保证每个合数只会被其最小质因子筛掉的?

  • x % primes[i] == 0 时,说明此时遍历到的 pi 不是 x 的质因子,那么 **pi 一定小于 x 的最小质因子 **,所以 pix 的最小质因子就是 pi
  • x % primes[i] != 0 时 ,说明 x 的最小质因子是此时的 pi ,因此 pix 的最小质因子依旧应该是 pi ,但如果继续枚举的话,我们就把 pi+1x 这个数筛掉了,虽然这个数也是合数,但是筛掉它的时候并不是用它的最小质因数筛掉的 ,而是利用 pi+1x 把它删掉的,而这个数的最小质因数其实是 pi ,如果不在这里退出循环的话,就会发现有些数是被重复筛了的。

勒让德定理

在正数 n! 的质因数分解中,质数 p 的指数记作 νp(n!) ,则:

νp(n!)=i=1logpnnpk

模板代码:

C++
int count(int n, int p) {
    int res = 0;
    while (n) res += n /= p;
    return res;
}
int count(int n, int p) {
    int res = 0;
    while (n) res += n /= p;
    return res;
}

约数

试除法求所有约数

模板代码:

C++
set<int> get_divisors(int n) {
    set<int> divisors;
    for (int i = 1; i <= n / i; ++i) {
        if (n % i == 0) {
            divisors.insert(i);
            divisors.insert(n / i);
        }
    }
    return divisors;
}
set<int> get_divisors(int n) {
    set<int> divisors;
    for (int i = 1; i <= n / i; ++i) {
        if (n % i == 0) {
            divisors.insert(i);
            divisors.insert(n / i);
        }
    }
    return divisors;
}

时间复杂度:O(n)

int 范围内,约数最多的数其约数有 1536 个。

约数个数

求约数个数

模板题:【数学】约数个数

原理:

  • 根据算术基本定理n 质因数分解为:n=p1α1×p2α2××pkαk
  • 那么 n 的约数个数为:f(n)=i=1k(αi+1)=(α1+1)(α2+1)(αk+1)

证明:

  • 显然, p1α1 的约数为 p10,p11,,p1k ,同理: piαi 的约数为 pi0,pi1,,pii
  • 实际上 n 的约数是在 p1α1,p2α2,pkαk 每个数的约数分别挑一个相乘得来的,那么 n 的每个约数就都可以被分解为 d=p1β1×p2β2××pkβk (其中 0βiαi );
  • 又根据算术基本定理:每一个数的质因数分解是唯一的,那么 βi 的所有组合就对应了 n 的所有约数
  • 而每个 βi 都有 0αiαi+1 种选择,所以 d 共有 i=1k(αi+1) 个组合,那么 n 也就有这么多个约数。

模板代码:

C++
// 求一个数的约数个数
int count_divisors(int n) {
    int cnt = 1;
    for (int i = 2; i <= n / i; ++i) {
        if (n % i == 0) {
            int s = 0;
            while (n % i == 0) {
                n /= i;
                ++s;
            }
           	cnt *= s + 1;
        }
    }
    if (n > 1) cnt *= 2;
    return cnt;
}
// 求一个数的约数个数
int count_divisors(int n) {
    int cnt = 1;
    for (int i = 2; i <= n / i; ++i) {
        if (n % i == 0) {
            int s = 0;
            while (n % i == 0) {
                n /= i;
                ++s;
            }
           	cnt *= s + 1;
        }
    }
    if (n > 1) cnt *= 2;
    return cnt;
}

时间复杂度:

  • 最差: O(n)
  • 最好: O(logN)
一些与约数个数相关的性质
  • 1N 中所有数的约数个数之和总和约为 NlogN 个;
  • [0,2×109] 范围内的约数个数最多的数的约数约有 1600 个。

约数之和

模板题:【数学】约数之和

原理:

  • 根据算术基本定理n 质因数分解为:n=p1α1×p2α2××pkαk

  • 那么 n 的约数之和为:

    σ(n)=i=1k(j=0αkpij)=(p10+p11+p12++p1α1)(p20+p21+p22++p2α2)(pk0+pk1+pk2++pkαk)

证明:

  • 显然, p1α1 的约数为 p10,p11,,p1k ,同理: piαi 的约数为 pi0,pi1,,pii

  • 实际上 n 的约数是在 p1α1,p2α2,pkαk 每个数的约数分别挑一个相乘得来的;

  • 可知共有 (α1+1)(α2+1)(αk+1) 种挑法,即约数个数。

  • 乘法原理可知它们的和为:

    σ(n)=i=1k(j=0αkpij)=(p10+p11+p12++p1α1)(p20+p21+p22++p2α2)(pk0+pk1+pk2++pkαk)

模板代码:

C++
// 求一个数的约数之和
int sum_divisors(int n) {
    int sum = 1;
    for (int i = 2; i <= n / i; ++i) {
        if (n % i == 0) {
			int tmp = 1;
            while (n % i == 0) {
                n /= i;
                tmp = i * tmp + 1;
            }
            sum *= tmp;
        }
    }
    if (n > 1) sum *= n + 1;
    return sum;
}
// 求一个数的约数之和
int sum_divisors(int n) {
    int sum = 1;
    for (int i = 2; i <= n / i; ++i) {
        if (n % i == 0) {
			int tmp = 1;
            while (n % i == 0) {
                n /= i;
                tmp = i * tmp + 1;
            }
            sum *= tmp;
        }
    }
    if (n > 1) sum *= n + 1;
    return sum;
}

最大公约数

原理:辗转相除法(欧几里得算法): gcd(a,b)=gcd(b,amodb)

证明:

  • 已知 amodb=aabb ,设 c=ab ,则现在要证明 gcd(a,b)=gcd(b,acb)
  • 对于 gcd(a,b) , 设 da,db
    • 则显然有 dax+by,x,yZ ,当 x=1,y=c 时, dacb
    • db,dacb 同时成立,则有 gcd(a,b)gcd(b,acb)
  • 对于 gcd(b,acb) ,设 db,dacb
    • 则显然有 dacb+cbda
    • da,db 同时成立,则有 gcd(b,acb)gcd(a,b)
  • gcd(a,b)=gcd(b,acb) 得证,同时 gcd(a,b)=gcd(b,amodb) 也成立。

模板代码:(递归实现)

C++
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

欧拉函数

公式求欧拉函数

详细介绍及原理:欧拉函数 - OI Wiki

若在算术基本定理中:n=p1α1×p2α2××pkαk

那么 n 的欧拉函数:ϕ(n)=n×i=1k(11pi)=n×p11p1×p21p2××pk1pk

模板代码:

C++
typedef long long LL;

// 求一个数的欧拉函数
LL phi(int n) {
    LL res = n;
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) {
            while (n % x == 0) n /= x;
            res = res * (x - 1) / x;
        }
    }
    if (n > 1) res = res * (n - 1) / n;
    return res;
}
typedef long long LL;

// 求一个数的欧拉函数
LL phi(int n) {
    LL res = n;
    for (int x = 2; x <= n / x; ++x) {
        if (n % x == 0) {
            while (n % x == 0) n /= x;
            res = res * (x - 1) / x;
        }
    }
    if (n > 1) res = res * (n - 1) / n;
    return res;
}

时间复杂度: O(N)

筛法求欧拉函数之和

1n 中每个数的欧拉函数之和:

Java
long sumEulers(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1], phi = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    phi[1] = 1;
    for (int i = 2; i <= n; ++i) {
        if (!isNotPrime[i]) {
            primes[cnt++] = i;
            phi[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; ++j) {
            isNotPrime[primes[j] * i] = true;
            if (i % primes[j] == 0) {
                phi[primes[j] * i] = phi[i] * primes[j];
                break;
            }
            phi[primes[j] * i] = phi[i] * (primes[j] - 1);
        }
    }
    long res = 0L;
    for (int i = 1; i <= n; ++i) res += phi[i];
    return res;
}
long sumEulers(int n) {
    int cnt = 0;
    int[] primes = new int[n + 1], phi = new int[n + 1];
    boolean[] isNotPrime = new boolean[n + 1];
    phi[1] = 1;
    for (int i = 2; i <= n; ++i) {
        if (!isNotPrime[i]) {
            primes[cnt++] = i;
            phi[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; ++j) {
            isNotPrime[primes[j] * i] = true;
            if (i % primes[j] == 0) {
                phi[primes[j] * i] = phi[i] * primes[j];
                break;
            }
            phi[primes[j] * i] = phi[i] * (primes[j] - 1);
        }
    }
    long res = 0L;
    for (int i = 1; i <= n; ++i) res += phi[i];
    return res;
}

时间复杂度: O(N)

快速幂

详细介绍及原理:快速幂 - OI Wiki

模板代码:

Java
long kasumi(long base, long exp, long mod) {
    long res = 1L;
    while (exp != 0) {
        if ((exp & 1) != 0) res = res * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return res;
}
long kasumi(long base, long exp, long mod) {
    long res = 1L;
    while (exp != 0) {
        if ((exp & 1) != 0) res = res * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return res;
}
C++
typedef long long LL;

LL kasumi(LL base, LL exp, LL mod) {
    LL res = 1LL;
    while (exp) {
        if (exp & 1) res = res * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return res;
}
typedef long long LL;

LL kasumi(LL base, LL exp, LL mod) {
    LL res = 1LL;
    while (exp) {
        if (exp & 1) res = res * base % mod;
        base = base * base % mod;
        exp >>= 1;
    }
    return res;
}

慢速乘

防止乘数及模数很大但还在 long long 范围内时直接做乘爆 long long 的问题。

Java
long slowMul(long a, long b, long m) {
    long c = 0L;
    while (b > 0) {
        if ((b & 1) != 0) c = (c + a) % m;
        a = (a + a) % m;
        b >>= 1;
    }
    return c;
}
long slowMul(long a, long b, long m) {
    long c = 0L;
    while (b > 0) {
        if ((b & 1) != 0) c = (c + a) % m;
        a = (a + a) % m;
        b >>= 1;
    }
    return c;
}
C++
typedef long long LL;

LL slow_mul(LL a, LL b, LL m) {
    LL c = 0LL;
    while (b) {
        if (b & 1) c = (c + a) % m;
        a = (a + a) % m;
        b >>= 1;
    }
    return c;
}
typedef long long LL;

LL slow_mul(LL a, LL b, LL m) {
    LL c = 0LL;
    while (b) {
        if (b & 1) c = (c + a) % m;
        a = (a + a) % m;
        b >>= 1;
    }
    return c;
}

扩展欧几里得算法

扩展欧几里得算法(Extended Euclidean algorithm, EXGCD),常用于求 ax+by=gcd(a,b)裴蜀定理)的一组可行解。

过程

设:

ax1+by1=gcd(a,b)

bx2+(amodb)y2=gcd(b,amodb)

欧几里得算法可知: gcd(a,b)=gcd(b,amodb)

所以:

ax1+by1=bx2+(amodb)y2ax1+by1=bx2+(aabb)y2ax1+by1=bx2+ay2baby2ax1+by1=ay2+b(x2aby2)

因为 a=a,b=b ,所以:{x1=y2y1=x2aby2

x2,y2 不断代入递归求解,直至 b=0 时: ax+0y=a ,显然 {x=1y=0 是一组解,此时退出递归。

模板代码

Java
int x, y;
    
int exgcd(int a, int b) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    int d = exgcd(b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return d;
}
int x, y;
    
int exgcd(int a, int b) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    int d = exgcd(b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return d;
}

线性同余方程

定义

形如:

axb(modn)

的方程称为 线性同余方程(Congruence Equation)。其中,a,b,n 为给定整数,x 为未知数,需要从区间 [0,n1] 中求解,当解不唯一时需要求出全体解。

用扩展欧几里得算法求解

根据以下两个定理,可以求出线性同余方程 axb(modn) 的解。

定理 1

axb(modn) 可以改写为 ax=ny+b ,移项得 axny=b 。设 y=y ,则有 ax+ny=b 。其中 xy 是未知数。这两个方程是等价的,有整数解的充要条件gcd(a,n)b (因为 ax+ny 一定要是 gcd(a,n) 的倍数 )。

很容易发现,经过转换后的方程很像裴蜀定理的结论( ax+by=gcd(a,b) ),于是我们可以先用扩展欧几里得算法求出一组 x0,y0 ,也就是 ax0+ny0=gcd(a,n) ,然后两边同时除以 gcd(a,n) ,再乘 b 。就得到了方程:

abgcd(a,n)x0+nbgcd(a,n)y0=b

于是找到方程的一个解:

{x0=bgcd(a,n)x0y0=bgcd(a,n)y0
定理 2

gcd(a,n)=1 ,且 x0,y0 为方程 ax+ny=b 的一组解,设 t=ngcd(a,n) ,则该方程的任意解可表示为:

{x=x0+nty=y0at

并且对任意整数 t 都成立。

根据定理 2,可以从已求出的一个解,求出方程的所有解。实际问题中,往往要求出一个最小整数解,也就是一个特解(例如使用扩展欧几里得算法求解乘法逆元):

xmin=(x0modt+t)modt

因为 gcd(a,n)=1 ,所以 t=ngcd(a,n)=n ,于是就有:

xmin=(x0modn+n)modn

模板题:

模板代码
Java
int x, y;

int exgcd(int a, int b) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return d;
}

// ax === b (mod n) 的解
int solve(int a, int b, int n) {
    int d = exgcd(a, n);
    if (b % d != 0) return -1; // 不存在整数解
    else return (int) (1L * x * (b / d) % n);
}
int x, y;

int exgcd(int a, int b) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return d;
}

// ax === b (mod n) 的解
int solve(int a, int b, int n) {
    int d = exgcd(a, n);
    if (b % d != 0) return -1; // 不存在整数解
    else return (int) (1L * x * (b / d) % n);
}

乘法逆元(模逆元)

定义

若整数 b,p 互质[1],且对于任意一个整数 a ,如果满足 ba [2] ,则存在一个整数 x ,使得 abax(modp) [3] 。称 xb 在模 p 意义下的乘法逆元,记作 b1(modp)

作用

在要除以一个数,再取模时,把除法变成乘法运算,然后再取模

快速幂求逆元

只能在模数为质数的情况下使用。

根据逆元的定义,有: abab1(modp)

两边同乘 b 得: aabb1(modp)

所以: bb11(modp) ①;

根据费马小定理 :若 p 为质数且 b,p 互质,则 bp11(modp)

从上式 bp1 中拆出一个 b 得到: bbp21(modp)

结合①式得到:b1bp2(modp)

综上,在 b 存在乘法逆元的条件下,求出 bp2modp 即为 b 在模 p 意义下的乘法逆元

bp2 使用快速幂求解。

扩展欧几里得算法求逆元

根据逆元的定义,有: abab1(modp)

两边同乘 b 得: aabb1(modp)

所以: bb11(modp)

a=b,x=b1,n=p ,上式表达为: ax1(modn) ,需要求解式中的 x ,其实就是求解线性同余方程 axb(modn)b=1 的特殊情况下的解。根据逆元的定义有: gcd(a,n)=1 ,参考求解线性同余方程 - 用扩展欧几里得算法求解 - 定理 2x 的一个最小整数解为:

x=(xmodn+n)modn

模板代码:

Java
int x, y;

int exgcd(int a, int b) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return d;
}

// a在模n意义下的逆元
int inv(int a, int n) {
    exgcd(a, n);
    return (x % n + n) % n;
}
int x, y;

int exgcd(int a, int b) {
    if (b == 0) {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return d;
}

// a在模n意义下的逆元
int inv(int a, int n) {
    exgcd(a, n);
    return (x % n + n) % n;
}

组合数

n 个不同元素中,任取 m(mn) 个元素组成一个集合,叫做从 n 个不同元素中取出 m 个元素的一个组合;从 n 个不同元素中取出 m(mn) 个元素的所有组合的个数,叫做从 n 个不同元素中取出 m 个元素的组合数。用符号 Cnm 来表示,组合数也常用 (nm) 表示,读作「nm」,即 Cnm=(nm)

组合数计算公式:

Cnm=n!m!(nm)!

特别地,规定当 m>n 时,Anm=Cnm=0

递推求组合数

模板题:求组合数 I

原理: Cnm=Cn1m+Cn1m1

证明:

n 个物品中选择 m 个物品( Cnm ),假如当前拿出来了一个物品:

  • 如果该物品不在要选择的 m 个物品中,那么我们还需要在 n1 个物品中选择 m 个物品( Cn1m );
  • 如果该物品要选择的 m 个物品中,那么我们还需要在 n1 个物品中选择 m1 个物品( Cn1m1 )。

综上, Cnm=Cn1m+Cn1m1 成立。

适用数据范围:1T10000,1mn2000

模板代码:

Java
static final int N;
static int[][] c = new int[N][N];

static void comb() {
    for (int n = 0; n < N; ++n) {
        for (int m = 0; m <= n; ++m) {
			if (m == 0) c[n][m] = 1;
            else c[n][m] = c[n - 1][m] + c[n - 1][m - 1];
        }
    }
}

// ans: c[a][b]
static final int N;
static int[][] c = new int[N][N];

static void comb() {
    for (int n = 0; n < N; ++n) {
        for (int m = 0; m <= n; ++m) {
			if (m == 0) c[n][m] = 1;
            else c[n][m] = c[n - 1][m] + c[n - 1][m - 1];
        }
    }
}

// ans: c[a][b]

预处理阶乘求组合数

模板题:求组合数 II

原理:Cnm=n!×(m!)1×(n!m!)1(mod109) (逆元的简单应用)。

适用数据范围:1T10000,1mn105

模板代码:

Java
// MOD必须是质数才能用快速幂求逆元
static final int N, MOD;
// fact[i]表示i的阶乘(% MOD)  infact[i]表示i的阶乘的逆元(% MOD)
static int[] fact = new int[N], infact = new int[N];

// 预处理1~N的阶乘及其逆元
static {
    fact[0] = 1;
    for (int i = 1; i < N; ++i) {
        fact[i] = (int) (1L * fact[i - 1] * i % MOD);
        infact[i] = (int) qmi(fact[i], MOD - 2, MOD);
    }
}

// 快速幂 用作求逆元
static long qmi(long base, long exp, long mod) {
    long res = 1L;
    while (exp > 0) {
        if ((exp & 1) == 1) res = (res * base) % mod;
        base = (base * base) % mod;
        exp >>= 1;
    }
    return res;
}

static int C(int n, int m) {
    return n == m ? 1 : (int) (1L * fact[n] * infact[m] % MOD * infact[n - m] % MOD);
}

// ans: C(a, b)
// MOD必须是质数才能用快速幂求逆元
static final int N, MOD;
// fact[i]表示i的阶乘(% MOD)  infact[i]表示i的阶乘的逆元(% MOD)
static int[] fact = new int[N], infact = new int[N];

// 预处理1~N的阶乘及其逆元
static {
    fact[0] = 1;
    for (int i = 1; i < N; ++i) {
        fact[i] = (int) (1L * fact[i - 1] * i % MOD);
        infact[i] = (int) qmi(fact[i], MOD - 2, MOD);
    }
}

// 快速幂 用作求逆元
static long qmi(long base, long exp, long mod) {
    long res = 1L;
    while (exp > 0) {
        if ((exp & 1) == 1) res = (res * base) % mod;
        base = (base * base) % mod;
        exp >>= 1;
    }
    return res;
}

static int C(int n, int m) {
    return n == m ? 1 : (int) (1L * fact[n] * infact[m] % MOD * infact[n - m] % MOD);
}

// ans: C(a, b)

卢卡斯定理求组合数

模板题:求组合数 III

原理:

  • a<p&b<p ,那么直接从定义出发,使用公式求解:
Cnm=n!m!(nm)!=i=nm+1nim!=n(n1)(nm+2)(nm+1)1×2××m
  • 否则代入 Lucas 公式 :Cnmmodp=CnmodpmmodpCn/pm/pmodp递归计算。

适用数据范围:1T20,1mn1018,1p105

模板代码:

Java
// 快速幂 用作且求逆元
static long qmi(long base, long exp, long mod) {
    long res = 1L;
    while (exp > 0) {
        if ((exp & 1) == 1) res = (res * base) % mod;
        base = (base * base) % mod;
        exp >>= 1;
    }
    return res;
}

// 求组合数
static long C(long n, long m, long p) {
    long nume = 1L, deno = 1L;
    for (long a = n, b = 1; b <= m; --a, ++b) {
        nume = nume * a % p;
        deno = deno * b % p;
    }
    return nume * qmi(deno, p - 2, p) % p;
}

// 卢卡斯定理
static long lucas(long n, long m, long p) {
    if (n < p && m < p) return C(n, m, p);
    else return lucas(n % p, m % p, p) * lucas(n / p, m / p, p) % p;
}

// ans: lucas(a, b, p)
// 快速幂 用作且求逆元
static long qmi(long base, long exp, long mod) {
    long res = 1L;
    while (exp > 0) {
        if ((exp & 1) == 1) res = (res * base) % mod;
        base = (base * base) % mod;
        exp >>= 1;
    }
    return res;
}

// 求组合数
static long C(long n, long m, long p) {
    long nume = 1L, deno = 1L;
    for (long a = n, b = 1; b <= m; --a, ++b) {
        nume = nume * a % p;
        deno = deno * b % p;
    }
    return nume * qmi(deno, p - 2, p) % p;
}

// 卢卡斯定理
static long lucas(long n, long m, long p) {
    if (n < p && m < p) return C(n, m, p);
    else return lucas(n % p, m % p, p) * lucas(n / p, m / p, p) % p;
}

// ans: lucas(a, b, p)

高精度+质因数分解求组合数

模板题:求组合数 IV

原理:

  • 根据公式: Cnm=n!m!(nm)! ,分别把 n!,m!,(nm)! 质因数分解,对于 1N 中的每个质数,维护 mp[i] 表示: n! 质因数分解后 i 的次数 m! 质因数分解后 i 的次数 (nm)! 质因数分解后 i 的次数。最后把 mp 中有次数的质数按照其次数乘起来就能得到答案;

  • 快速计算 n 的阶乘的质因数分解中质因子 x 的数量:勒让德定理

  • 高精度 Java 用 BigInteger 包。

适用数据范围:1mn5000

Java
// 预处理筛出1~N之间的所有质数
static final int N;
static int[] primes = new int[N];
static int cnt = 0;
static boolean[] np = new boolean[N + 1];
// 线性筛
static {
    for (int x = 2; x <= N; ++x) {
        if (!np[x]) primes[cnt++] = x;
        for (int i = 0; primes[i] <= N / x; ++i) {
            np[primes[i] * x] = true;
            if (x % primes[i] == 0) break;
        }
    }
}

// 求n!中因子p的数量
static int count(int n, int d) {
    int cnt = 0;
    while (n > 0) cnt += n /= d;
    return cnt;
}

static BigInteger C(int n, int m) {
    BigInteger res = BigInteger.valueOf(1);
    for (int i = 0; i < cnt; ++i) {
        int p = primes[i];
        // 求每个质因子的幂次数
        int cnt = count(n, p) - count(m, p) - count(n - m, p);
        BigInteger bp = BigInteger.valueOf(p);
        while (cnt-- > 0) res = res.multiply(bp);
    }
    return res;
}

// ans: C(a, b)
// 预处理筛出1~N之间的所有质数
static final int N;
static int[] primes = new int[N];
static int cnt = 0;
static boolean[] np = new boolean[N + 1];
// 线性筛
static {
    for (int x = 2; x <= N; ++x) {
        if (!np[x]) primes[cnt++] = x;
        for (int i = 0; primes[i] <= N / x; ++i) {
            np[primes[i] * x] = true;
            if (x % primes[i] == 0) break;
        }
    }
}

// 求n!中因子p的数量
static int count(int n, int d) {
    int cnt = 0;
    while (n > 0) cnt += n /= d;
    return cnt;
}

static BigInteger C(int n, int m) {
    BigInteger res = BigInteger.valueOf(1);
    for (int i = 0; i < cnt; ++i) {
        int p = primes[i];
        // 求每个质因子的幂次数
        int cnt = count(n, p) - count(m, p) - count(n - m, p);
        BigInteger bp = BigInteger.valueOf(p);
        while (cnt-- > 0) res = res.multiply(bp);
    }
    return res;
}

// ans: C(a, b)

卡特兰数

模板题:满足条件的01序列

C2nnC2nn1=C2nnn+1
  1. gcd(b,m)=1↩︎

  2. a 能被 b 整除( ab 是一个整数 )。 ↩︎

  3. ab 在模 p同余于 axabmodp=axmodp )。 ↩︎