惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
爱范儿
爱范儿
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
雷峰网
雷峰网
WordPress大学
WordPress大学
L
LangChain Blog
D
DataBreaches.Net
The GitHub Blog
The GitHub Blog
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
腾讯CDC
GbyAI
GbyAI
罗磊的独立博客
Blog — PlanetScale
Blog — PlanetScale
月光博客
月光博客
F
Fortinet All Blogs
Y
Y Combinator Blog
V
V2EX
A
About on SuperTechFans

博客园 - RonChen

Sunday 算法 康托展开 多源 BFS 抽屉原理 区间合并 距离和的最小值 归并排序与逆序对 快速排序与快速选择 同余分析 差分约束 Treap 点分治 莫队算法 分块 扫描线 错排问题 Sprague-Grundy (SG) 函数及其应用 容斥原理 卢卡斯定理 线性基 高斯消元 勒让德公式 次短路 分层图最短路 01 图最短路 洪水填充 双向搜索 迭代加深搜索 剪枝 最小表示法
排列组合与二项式定理
RonChen · 2026-09-19 · via 博客园 - RonChen

排列组合

排列数:\(A_n^m = n \times (n-1) \times \cdots \times (n-m+1) = \dfrac{n!}{(n-m)!}\)

组合数:\(C_n^m = \dfrac{A_n^m}{m!} = \dfrac{n!}{m!(n-m)!}\)

组合数与杨辉三角:\(C_n^m = C_{n-1}^{m-1} + C_{n-1,m}\),特殊地,\(C_0^0 = 1, C_i^0 = i\)

组合数常用等式:\(C_n^m = C_n^{n-m}, \ \sum \limits_{i=0}^n C_n^i = 2^n\)

\(n,m\) 较小(允许 \(O(nm)\) 做法),模数不一定是质数时,可以使杨辉三角递推计算组合数。

\(n,m\) 较大但比模数小(只允许 \(O(n)\)\(O(n \log n)\) 做法)且模数是质数时,可以使用杨辉三角递推计算组合数。

\(m\) 很小时,可以用 \(\dfrac{n \times (n-1) \times \cdots \times (n-m+1)}{m!}\) 暴力乘除(如果需要取模且模数是质数,除法用乘以逆元替代)。

例题:P2822 [NOIP 2016 提高组] 组合数问题

\(t\) 组数据,\(k\) 都是相同的,所以可以预处理出所有 \(n,m \le 2000\) 组合数 \(\bmod k\) 的结果,本题的 \(k\) 不一定是质数,所以用杨辉三角递推预处理。

为了快速求出一组数据的答案,可以令 \(a_{i,j}\) 表示 \(C_i^j \bmod k\) 是否为 \(0\),特殊地,如果 \(i \lt j\),则 \(a_{i,j}=0\)

\(a\) 数组做二维前缀和,单组数据求解的时间复杂度就是 \(O(1)\) 了。

总的时间复杂度为 \(O(nm+t)\)

参考代码
#include <cstdio>
const int N = 2005;
int c[N][N], a[N][N];
int main()
{
    int t, k; scanf("%d%d", &t, &k);
    for (int i = 1; i <= 2000; i++) {
        c[i][0] = 1; a[i][0] = a[i - 1][0];
        for (int j = 1; j < i; j++) {
            c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % k;
            a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + (c[i][j] == 0);
        }   
        c[i][i] = 1;
        for (int j = i; j <= 2000; j++) a[i][j] = a[i][j - 1];
    }
    for (int i = 1; i <= t; i++) {
        int n, m; scanf("%d%d", &n, &m);
        printf("%d\n", a[n][m]);
    }
    return 0;
}

习题:B3717 组合数问题

解题思路

因为模数 \(p\) 是质数,且 \(n,m\) 均比模数小,可以用 \(C_n^m = \dfrac{n!}{m!(n-m)!}\) 求解。

预处理阶乘数组,\(f_i\) 表示 \(i! \bmod p\),阶乘逆元数组,\(g_i\) 表示 \(i!\) 的乘法逆元。

预处理阶乘逆元时,可以先求 \(f_n\) 的逆元,再倒着求每个阶乘的逆元,\(g_i = g_{i+1} \times (i+1) \bmod p\),这样求所有逆元的时间复杂度是线性的。

每次询问的答案就是 \(f_n \times g_m \times g_{n-m}\),注意取模。

总时间复杂度为 \(O(N+T)\)

参考代码
#include <cstdio>
const int N = 5000005;
const int MOD = 998244353;
int f[N], g[N];
int qpow(int x, int y) {
    int res = 1;
    while (y > 0) {
        if (y & 1) res = 1ll * res * x % MOD;
        x = 1ll * x * x % MOD;
        y >>= 1;
    }
    return res;
}
void init(int n) {
    f[0] = g[0] = 1;
    for (int i = 1; i <= n; i++) f[i] = 1ll * f[i - 1] * i % MOD;
    g[n] = qpow(f[n], MOD - 2);
    for (int i = n - 1; i > 0; i--) g[i] = 1ll * g[i + 1] * (i + 1) % MOD;
}
int main()
{
    int t, maxn; 
    scanf("%d%d", &t, &maxn);
    init(maxn);
    int ans = 0;
    for (int i = 1; i <= t; i++) {
        int n, m; scanf("%d%d", &n, &m);
        int res = 1ll * f[n] * g[m] % MOD * g[n - m] % MOD;
        ans ^= res;
    }
    printf("%d\n", ans);
    return 0;
}

二项式定理

\((x+y)^n = \overbrace{(x+y) \times (x+y) \times \cdots \times (x+y)}^\text{共 n 项} = C_n^0 x^n y^0 + C_n^1 x^{n-1}y^1 + \cdots + C_n^r x^{n-r}y^r + \cdots + C_n^n x^0 y^n\),对于 \(C_n^r x^{n-r} y^r\) 这一项来说,相当于有 \(r\)\((x+y)\) 提供了 \(x\),有 \(n-r\)\((x+y)\) 提供了 \(y\)

\((1+(-1))^n = C_n^0 (-1)^0 + C_n^1 (-1)^1 + \cdots = -C_n^0 + C_n^1 - C_n^2 + C_n^3 - \cdots = 0\),所以 \(\sum C_n^{奇数} = \sum C_n^{偶数} = 2^{n-1}\)

\((1+2)^n = \sum \limits_{k=0}^n \binom{n}{k} 1^{n-k} \cdot 2^k = \sum\limits_{k=0}^n \binom{n}{k} 2^k = 3^n\)

例题:P1313 [NOIP 2011 提高组] 计算系数

把括号展开再合并同类项,\(x^ny^m\) 这一项相当于有 \(n\) 个括号选了 \(ax\) 这一项,其它括号选了 \(by\) 这一项,因此其系数为 \(C_k^n \times a^n \times b^m\)

本题这个组合数,可以用杨辉三角递推,也可以选择预处理阶乘和阶乘逆元来做。

参考代码
#include <cstdio>
#include <vector>
using std::vector;
const int MOD = 10007;
int main()
{
    int a, b, k, n, m;
    scanf("%d%d%d%d%d", &a, &b, &k, &n, &m);
    int ans = 1;
    for (int i = 1; i <= n; i++) ans = 1ll * ans * a % MOD;
    for (int i = 1; i <= m; i++) ans = 1ll * ans * b % MOD;
    vector<vector<int>> c(k + 1, vector<int>(n + 1));
    for (int i = 1; i <= k; i++) {
        c[i][0] = 1;
        if (i <= n) c[i][i] = 1;
        for (int j = 1; j < i && j <= n; j++) 
            c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % MOD;
    }
    ans = 1ll * ans * c[k][n] % MOD;
    printf("%d\n", ans);
    return 0;
}

例题:P5390 [Cnoi2019] 数学作业

对二进制下每一位分别讨论。

枚举当前位,假设有 \(k\) 个数的二进制在当前位上取 \(1\)

显然当前位为 \(0\) 的数取不取无所谓,当且仅当选取的子集中有奇数个 \(1\) 的数才对答案有贡献。

\((C_k^1 + C_k^3 + \cdots) \times 2^{n-k} = 2^{k-1+n-k} = 2^{n-1}\),也就是说只要这些数在第 \(i\) 位上有 \(1\),就会产生 \(2^{n-i} \times 2^i\) 的贡献。

参考代码
#include <cstdio>
#include <vector>
using std::vector;
const int MOD = 998244353;
void solve() {
    int n; scanf("%d", &n);
    vector<int> v(n + 1);
    int num = 0, p = 1;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &v[i]); num |= v[i];
        if (i > 1) p = 1ll * p * 2 % MOD;
    }
    int i = 0, ans = 0;
    while (num > 0) {
        if (num & 1) ans += (1 << i);
        num >>= 1; i++;
    }
    ans = 1ll * ans * p % MOD;
    printf("%d\n", ans);
}
int main()
{
    int t; scanf("%d", &t);
    for (int i = 1; i <= t; i++) solve();
    return 0;
}