组合数数论小结 · 归纳篇

· · 个人记录

求组合数·水星

水利万物而不争

n和m都较小,mod也没有超出int范围

  1. 因此可以采取直接暴力递推的方式(杨辉三角公式递推)

  2. 需要注意的是,每次都要求取模,因此不能够记忆化

  3. 相反的,还需要每次重置,注意边界

    #include <cstdio>
    #include <algorithm>
    #include <cmath>
    #define lld long long 
    //水利万物而不争
    using namespace std;
    const int maxn = 2003;
    lld c[maxn][maxn];
    int main(){
    
     c[0][0] = 1;
    
     int T;
    
     scanf("%d", &T);
     while(T--){
         int n, m, q;
         scanf("%d%d%d", &n, &m, &q);//不会暴int 
         for(int i = 1; i <= n; i++)
             for(int j = 0; j <= i; j++)
    //              if(!c[i][j])
                     c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % q;
         printf("%lld\n", c[n][m]);
     }
     return 0;
    }

求组合数·金星

锲而不舍,金石可镂

注意这个n已经很大了,但是m仍然比较小

  1. 因此可以采取储存m为下标的数组(本题要求以n为底,输出一系列数字)

  2. 依然采用了递推的方式,不是线性,但是相对于水有了倍增优化

本题具体分析请看这里:组合数小结(一)

#include <cmath>
#include <cstring> 

#define lld long long

using namespace std;
//在苍茫的大海上,狂风卷积着乌云
//在乌云和大海之间,有一只高傲的海燕 
const int maxn = 1003;

lld n, m;
lld f[maxn], g[maxn], q;
//f用来储存答案,g相当于暂时性存储
lld qmul(lld a, lld b) {

    a %= q;
    b %= q;
    lld ans = 0;
    while (b) {
        if (b & 1)
            ans = (ans + a) % q;
        a = (a + a) % q;
        b >>= 1;
    }
    return ans;
}
//留在这里的qmul只是为了提醒自己,never to 不判断条件就用快速乘
void cloud(lld n){

    if(n == 1)
        return;

    if(n % 2 == 0){
//n是偶数时,假设p等于n/2,然后卷积式求和        
        cloud(n >> 1);

        for(int i = 0; i <= m; i++){
            for(int j = 0; j <= i; j++)
                g[i] += f[j] % q * f[i - j] % q;
/*就是这里,用快速乘你就输了!!!!,正如陈老师所说 balabalabalala...*/
/*没错一定要注意这里,需要用数组储存而不是每次递归
(记忆化也不行,正如陈老师所说,对于一个要刷满的表,记忆化不如填表优)*/
//              g[i] = (g[i] + qmul(f[j], f[i - j])) ;// % q;
//              g[i] += qmul(f[j], f[i - j]);
            g[i] %= q;
        }

//              

        for(int i = 0; i <= m; i++)
            f[i] = g[i];//储存本次m的组合数

        memset(g, 0, sizeof g);//就是每次tmp数组的重置啦
    }else{

        cloud(n - 1);
//n为奇数时,先求出 n - 1 为总数的组合数
//然后利用杨辉三角基本转移方程写出下面的转移方程

        for(int i = 1; i <= m; i++)
            g[i] = (f[i - 1] % q + f[i] % q) % q;

        for(int i = 1; i <= m; i++)
            f[i] = g[i];//储存本次m的组合数

        memset(g, 0, sizeof g);//就是每次tmp数组的重置啦
    }
}

int main(){
    int T;
    scanf("%d", &T);
    while(T--){

        memset(f, 0, sizeof f);
        f[0] = 1;
        f[1] = 1;

        scanf("%lld %lld %lld", &n, &m, &q);
        cloud(n);
//注意只要进行一次递归操作(如果不想T死的话),而不是每次输出都进行递归
//虽然n很大,但是递归次数和m都是数组可存大小(尽量不要考虑map的奇葩用法)
//在这里不选择记忆化,因此储存每个f[m]

        for(int i = 0; i <= m; i++)
            printf("%lld ", f[i]);
        puts("");
    }
    return 0;
}

求组合数·地球

淼茫积水非吾土,飘泊浮萍是我身。

  1. 显然,这个n,m都不是数组可存下标范围,而且Q是质数(还是比较小的质数)

  2. 所以我们使用Lucas定理(注意要求取模的数为质数并且不是很大)

    • 还记得吗?

    • 卢卡斯说:

    • C(n, m) % p = C(n / p, m / p) * C(n % p, m % p) % p

      对于C(n / p, m / p),如果n / p 还是很大,可以递归下去,一直到世界的尽头

#include <cstdio>
#include <algorithm>
#include <cmath>
#define lld long long
using namespace std;
//天地玄黄,宇宙洪荒
// Lucas定理,大数难挡
lld qpow(lld a, lld b, lld mod) {//快速幂
    lld ans = 1;
    while (b) {
        if (b & 1)
            ans = (ans * a) % mod;
        a = (a * a) % mod;
        b >>= 1;
    }
    return ans;
}
lld comp(lld a, lld b, lld mod) {//求C(b,a)
    if (a < b)
        return 0;
    if (a == b)
        return 1;
    if (b > a - b)//利用组合数性质进行的简单优化
        b = a - b;

    int ans = 1, ca = 1, cb = 1;
    //求出 n * (n-1) * ··· * (n-m+1)
    //求出 m * (m-1) * ··· * 1
    for (lld i = 0; i < b; i++) {//求出a_i,b_i的阶乘
        ca = (ca * (a - i)) % mod;
        cb = (cb * (b - i)) % mod;
    }

    //下面无需证明地给出这个公式(就是费马小定理(二)啦)
    ans = (ca * qpow(cb, mod - 2, mod)) % mod;
    return ans;
}
lld lucas(lld n, lld m, lld mod) {
//Lucas定理具体简单应用
    lld ans = 1;

    while (n && m && ans) {//持续进行同步进制转化(作为一名没有学过进制的蒟蒻,我是现场学的板子)
        ans = (ans * comp(n % mod, m % mod, mod)) % mod;
        n /= mod;
        m /= mod;
    }
    return ans;
}
int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        lld m, n, mod;
        scanf("%lld %lld %lld", &n, &m, &mod);
        printf("%lld\n", lucas(n, m, mod));
    }
    return 0;
}

Get 本人未涉及/不熟悉知识点:

1. 进制转化

    while (n && m && ans) {//持续进行同步进制转化(作为一名没有学过进制的蒟蒻,我是现场学的板子)
        ans = (ans * comp(n % mod, m % mod, mod)) % mod;
        n /= mod;
        m /= mod;
    }

2. 费马小定理求逆元

假如是一个整数,是一个质数,那么是p的倍数,可以表示为

如果a不是p的倍数,这个定理也可以写成 (这个书写方式更加常用)

因此可以得出:b ^ p (mod p)关于 p 的逆元可以写为 b ^ (p-2) (mod p)

得到下式:

    ans = (ca * qpow(cb, mod - 2, mod)) % mod;

例题:上帝与集合的正确用法

求组合数·火星

绿蚁新醅酒,红泥小火炉。晚来天欲雪,能饮一杯无?
  1. 可以看出,n比较大,m仍在数据储存范围内,Q变大了但是Q是质数而且没有特别大(我以为,质数就是在暗示我们写Lucas定理)

    #include <cstdio>
    #include <algorithm>
    #include <cmath>
    #define lld long long
    using namespace std;
    //天地玄黄,宇宙洪荒
    // Lucas定理,大数难挡
    lld qpow(lld a, lld b, lld mod) {
     lld ans = 1;
     while (b) {
         if (b & 1)
             ans = (ans * a) % mod;
         a = (a * a) % mod;
         b >>= 1;
     }
     return ans;
    }
    lld comp(lld a, lld b, lld mod) {
     if (a < b)
         return 0;
     if (a == b)
         return 1;
     if (b > a - b)
         b = a - b;
    
     int ans = 1, ca = 1, cb = 1;
     for (lld i = 0; i < b; i++) {
         ca = (ca * (a - i)) % mod;
         cb = (cb * (b - i)) % mod;
     }
     ans = (ca * qpow(cb, mod - 2, mod)) % mod;
     return ans;
    }
    lld lucas(lld n, lld m, lld mod) {
     lld ans = 1;
    
     while (n && m && ans) {
         ans = (ans * comp(n % mod, m % mod, mod)) % mod;
         n /= mod;
         m /= mod;
     }
     return ans;
    }
    int main() {
     int T;
     scanf("%d", &T);
     while (T--) {
         lld m, n, mod;
         scanf("%lld %lld %lld", &n, &m, &mod);
         printf("%lld\n", lucas(n, m, mod));
     }
     return 0;
    }

陈老师说

“

 这个可以把A(n,m)给拓展一下,一直往后乘就行了

 这个考点是逆元呀

 地没法用逆元做吧

 地可以不用逆元,只用Lucas定理

 呃,不能这么说……应该说要用逆元,只是逆元可以暴力算,不用exgcd或是费马小定理

 大家会通用做法当然是姿雌的啦

 最后一题(土)就是大合成

                                ”

艹我还是不知道为什么可以暴力算逆元

求组合数·木星

沉舟侧畔千帆过,病树前头万木春。

  1. n, m, q均不在可枚举范围,同时q不保证是质数,那么就可以加入中国剩余定理了(需要注意的是,q的每一个因子的幂次都保证不会太大)