组合计数是组合数学的 “实战核心”,它不像基础概念那样抽象,而是直接对应算法竞赛和实际应用中的各类计数问题 —— 小到给兔子编号、安排牛的位置,大到齿轮组损耗计算、网格三角形计数,本质上都是组合计数的灵活运用。 很多同学学完排列组合的公式后,一碰到具体题目就卡壳,核心原因是没掌握 “问题转化” 的思维:如何把实际问题抽象成组合模型,如何选择合适的计数方法(加法原理、乘法原理、组合数、正难则反等)。 本文将围绕 7 道经典组合计数题目展开,每道题都来自洛谷等权威平台,覆盖从入门到进阶的难度梯度。我们会从题目分析入手,拆解问题本质,推导解题思路,最后给出完整的 C++ 代码和细节注释。无论你是算法新手,还是想巩固组合计数的进阶选手,跟着这篇文章一步步走,都能收获满满!
在开始做题前,先快速回顾几个核心知识点,这些是解题的 “万能钥匙”:

,公式为

(核心性质:

)。

(费马小定理),是组合数取模的关键。
这些知识点看似简单,但要灵活运用到题目中,需要通过具体场景反复锤炼。下面我们逐个攻克 7 道经典题!
题目链接:https://www.luogu.com.cn/problem/P1866
太郎有 N 只兔子,每只兔子 i 想要一个 1 到Mi之间的整数作为编号(可等于 1 或Mi),且所有兔子的编号必须不同。求合法的编号方案数,答案对10^{9}+7取模;若不可能则输出 0。
2
5 835这道题的核心矛盾是 “编号不重复” 和 “每只兔子的编号有范围限制”。直接暴力枚举显然不现实,需要找到一种有序的计数方式。
关键思路:排序后分步计数。
以示例输入为例:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 55;
const int MOD = 1e9 + 7;
int main() {
int n;
cin >> n;
int m[N];
for (int i = 1; i <= n; ++i) {
cin >> m[i];
}
// 排序是关键,保证后续分步计数的合理性
sort(m + 1, m + 1 + n);
LL result = 1;
for (int i = 1; i <= n; ++i) {
// 第i只兔子可选的编号数 = M_i - (i-1)
m[i] -= (i - 1);
if (m[i] <= 0) {
// 没有合法编号,直接输出0
cout << 0 << endl;
return 0;
}
// 乘法原理,累乘结果并取模
result = result * m[i] % MOD;
}
cout << result << endl;
return 0;
}★ 入门级,核心考察 “排序 + 乘法原理” 的结合,学会将无序问题转化为有序问题,是组合计数的基础思维。
题目链接:https://www.luogu.com.cn/problem/P8469

给定长度为 n 的整数序列 a,构造长度为 n 的序列 b,满足 1≤bi≤ai(1≤i≤n),且gcd(b1,b2,...,bn)(最大公约数)最大。求这个最大的 gcd 值,以及对应的合法序列 b 的个数(对109+7取模)。
3
1 2 31 6这道题有两个目标:一是找到最大的 gcd,二是计算对应的方案数。核心是理解 “gcd 最大” 的含义。
关键思路:最大 gcd 是序列 a 的最小值。

。总方案数为所有

的乘积(乘法原理)。
以示例输入为例:
#include <iostream>
#include <climits> // 用于INT_MAX
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
const int MOD = 1e9 + 7;
int main() {
int n;
cin >> n;
int a[N];
int min_a = INT_MAX; // 初始化最小a_i为无穷大
for (int i = 1; i <= n; ++i) {
cin >> a[i];
if (a[i] < min_a) {
min_a = a[i]; // 找到a数组的最小值
}
}
LL count = 1;
for (int i = 1; i <= n; ++i) {
// 每个a_i对应的可选b_i数量:a_i / min_a
count = count * (a[i] / min_a) % MOD;
}
cout << min_a << " " << count << endl;
return 0;
}
,累乘取模。
★ 入门级,核心考察 “最大公约数的性质” 和 “乘法原理”,需要理解 “gcd 最大” 的约束条件如何转化为具体的数值(数组最小值)。
题目链接:https://www.luogu.com.cn/problem/P6191

John 有 N 个位置,要安排公牛和奶牛,要求任意两只公牛之间至少有 K 只奶牛。公牛和奶牛都是相同的,求合法的安排方案数(对 5000011 取模)。
4 26这道题的关键是 “相同元素的排列 + 约束条件(公牛之间至少 K 只奶牛)”。由于元素相同,我们只需要计算 “放多少只公牛” 以及 “放在哪里”。
关键思路:枚举公牛数量,转化为组合数问题。

。

。
以示例输入为例(N=4,K=2):

。

。

,停止枚举。
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
const int MOD = 5000011;
LL f[N]; // 阶乘数组:f[i] = i! mod MOD
LL g[N]; // 阶乘逆元数组:g[i] = (i!)^{-1} mod MOD
// 快速幂:计算a^b mod p
LL qpow(LL a, LL b, LL p) {
LL ret = 1;
while (b) {
if (b & 1) ret = ret * a % p;
a = a * a % p;
b >>= 1;
}
return ret;
}
// 初始化阶乘和逆元数组
void init(int max_n) {
f[0] = 1;
for (int i = 1; i <= max_n; ++i) {
f[i] = i * f[i - 1] % MOD;
}
// 费马小定理求逆元
g[max_n] = qpow(f[max_n], MOD - 2, MOD);
for (int i = max_n - 1; i >= 0; --i) {
g[i] = (i + 1) * g[i + 1] % MOD;
}
}
// 计算组合数C(n, m) mod MOD
LL C(int n, int m) {
if (n < m) return 0; // 不可能的情况,返回0
return f[n] * g[m] % MOD * g[n - m] % MOD;
}
int main() {
int n, k;
cin >> n >> k;
init(n); // 初始化阶乘数组,最大需要n!
LL result = 1; // i=0时的方案数
for (int i = 1; ; ++i) {
// 计算可用位置数:n - (i-1)*k
int available = n - (i - 1) * k;
if (available < i) break; // 无法放下i只公牛,终止
// 累加i只公牛的方案数
result = (result + C(available, i)) % MOD;
}
cout << result << endl;
return 0;
}
,通过预存阶乘和逆元数组,实现 O (1) 查询。
★ 基础级,核心考察 “约束条件转化为组合模型”,学会 “预留位置” 的技巧,将有约束的计数问题转化为无约束的组合数问题。
题目链接:https://www.luogu.com.cn/problem/P8557

铃要炼制一种合金,需要 n 种金属。她有 k 个不同的熔炉,每个熔炉启动时会随机炼出若干种金属(可能没有)。收集所有熔炉炼出的金属,若包含全部 n 种金属,则算一种成功情况。求成功的情况数(对 998244353 取模)。
2 29这道题的关键是 “每个熔炉的选择独立” 和 “最终覆盖所有 n 种金属”。直接计算 “覆盖所有金属” 的情况数较难,可转化为 “每种金属至少被一个熔炉炼出”。
关键思路:分步计数 + 幂运算。

(减去 “所有熔炉都不炼出该金属” 的情况)。

(乘法原理,n 个(2k−1)相乘)。
以示例输入为例(n=2,k=2):

(熔炉 1 炼出、熔炉 2 炼出、两个都炼出)。
#include <iostream>
using namespace std;
typedef long long LL;
const int MOD = 998244353;
// 快速幂:计算a^b mod p
LL qpow(LL a, LL b, LL p) {
LL ret = 1;
while (b) {
if (b & 1) ret = ret * a % p;
a = a * a % p;
b >>= 1;
}
return ret;
}
int main() {
LL n, k;
cin >> n >> k;
// 计算2^k mod MOD
LL pow2 = qpow(2, k, MOD);
// 每种金属的有效情况数:pow2 - 1
LL per_metal = (pow2 - 1 + MOD) % MOD; // +MOD避免负数
// 总情况数:(per_metal)^n mod MOD
LL result = qpow(per_metal, n, MOD);
cout << result << endl;
return 0;
}
和

,时间复杂度 O (logk + logn),适合 k 和 n 较大的情况。
★ 入门级,核心考察 “独立事件的分步计数”,学会将复杂的覆盖问题转化为单个元素的独立计数,再用幂运算快速求解。
题目链接:https://www.luogu.com.cn/problem/P3197

监狱有 n 个房间,每个房间关押一个犯人,有 m 种宗教。若相邻房间的犯人信仰相同宗教,则可能发生越狱。求可能发生越狱的状态数(对 100003 取模)。
2 36这道题直接计算 “发生越狱” 的情况数较复杂(需要考虑至少一对相邻犯人宗教相同),但用 “正难则反” 的思路会很简单。
关键思路:总状态数 - 不发生越狱的状态数 = 发生越狱的状态数。

,结果对 100003 取模。
以示例输入为例(m=2,n=3):
#include <iostream>
using namespace std;
typedef long long LL;
const int MOD = 100003;
// 快速幂:计算a^b mod p
LL qpow(LL a, LL b, LL p) {
LL ret = 1;
a %= p; // 先取模,避免溢出
while (b) {
if (b & 1) ret = ret * a % p;
a = a * a % p;
b >>= 1;
}
return ret;
}
int main() {
LL m, n;
cin >> m >> n;
// 总状态数:m^n mod MOD
LL total = qpow(m, n, MOD);
// 不越狱状态数:m * (m-1)^(n-1) mod MOD
LL no_escape = m * qpow(m - 1, n - 1, MOD) % MOD;
// 越狱状态数 = (total - no_escape) mod MOD,避免负数
LL escape = (total - no_escape + MOD) % MOD;
cout << escape << endl;
return 0;
}★★ 基础进阶,核心考察 “正难则反” 的计数技巧,这是组合计数中解决复杂约束问题的常用方法,必须掌握。
题目链接:https://www.luogu.com.cn/problem/P1350
有一个不规则的网格棋盘,由 a、b、c、d 四个参数定义(具体形状见原题)。要在棋盘上放 k 个相互不攻击的车(即任意两车不在同一行、同一列),求方案数(对105+3取模)。
2 2 2 2 238车的放置问题是组合计数的经典模型,但这道题的棋盘是不规则的,需要先将其拆分为规则图形。
关键思路:拆分棋盘 + 分步计数。

(从 a 行选 x 行)。

(x!是列的排列数,因为车是不同的?不,车是相同的,但列的选择是有序的,因为每行对应不同的列)。

(从 c 行选 k-x 行)。

。

#include <iostream>
using namespace std;
typedef long long LL;
const int N = 2e3 + 10;
const int MOD = 1e5 + 3;
LL f[N]; // 阶乘数组:f[i] = i! mod MOD
LL g[N]; // 阶乘逆元数组:g[i] = (i!)^{-1} mod MOD
// 快速幂:计算a^b mod p
LL qpow(LL a, LL b, LL p) {
LL ret = 1;
while (b) {
if (b & 1) ret = ret * a % p;
a = a * a % p;
b >>= 1;
}
return ret;
}
// 初始化阶乘和逆元数组
void init() {
int max_n = 2000; // 题目中a,b,c,d≤2000,k≤2000
f[0] = 1;
for (int i = 1; i <= max_n; ++i) {
f[i] = i * f[i - 1] % MOD;
}
g[max_n] = qpow(f[max_n], MOD - 2, MOD);
for (int i = max_n - 1; i >= 0; --i) {
g[i] = (i + 1) * g[i + 1] % MOD;
}
}
// 计算组合数C(n, m) mod MOD
LL C(int n, int m) {
if (n < m) return 0;
return f[n] * g[m] % MOD * g[n - m] % MOD;
}
int main() {
init();
LL a, b, c, d, k;
cin >> a >> b >> c >> d >> k;
LL result = 0;
// 枚举x:上矩形放x个车,下矩形放k-x个车
for (int x = 0; x <= k; ++x) {
// 上矩形方案数:C(d, x) * C(c, x) * x!
LL part1 = C(d, x) * C(c, x) % MOD;
part1 = part1 * f[x] % MOD;
// 下矩形方案数:C(b + d - x, k - x) * C(a, k - x) * (k - x)!
LL part2 = C(b + d - x, k - x) * C(a, k - x) % MOD;
part2 = part2 * f[k - x] % MOD;
// 累加x对应的方案数
result = (result + part1 * part2) % MOD;
}
cout << result << endl;
return 0;
}★★ 进阶级,核心考察 “不规则图形拆分” 和 “组合数与排列数的结合”,需要具备将复杂问题分解为多个简单子问题的能力。
题目链接:https://www.luogu.com.cn/problem/P3166

给定一个 N×M 的网格,计算三点都在格点上的三角形个数(三点不能共线)。
2 276这道题是组合计数的高阶题目,直接计算 “不共线的三点” 较难,采用 “正难则反” 的思路:总三点数 - 共线的三点数 = 三角形个数。
关键思路:总三点数 - 共线三点数 = 答案。

。

,共(N+1)行,总方案数为

。

,共(M+1)列,总方案数为

。

。

以示例输入为例(N=2,M=2):
#include <iostream>
using namespace std;
typedef long long LL;
// 计算最大公约数
LL gcd(LL a, LL b) {
return b == 0 ? a : gcd(b, a % b);
}
int main() {
LL n, m;
cin >> n >> m;
// 格点数:(n+1)*(m+1)
LL total_points = (n + 1) * (m + 1);
// 总三点数:C(total_points, 3)
LL total = total_points * (total_points - 1) * (total_points - 2) / 6;
// 水平共线三点数:(n+1)*C(m+1, 3)
LL horizontal = (n + 1) * (m + 1) * m * (m - 1) / 6;
// 垂直共线三点数:(m+1)*C(n+1, 3)
LL vertical = (m + 1) * (n + 1) * n * (n - 1) / 6;
// 斜向共线三点数
LL diagonal = 0;
for (LL i = 1; i <= n; ++i) { // 水平间隔i
for (LL j = 1; j <= m; ++j) { // 垂直间隔j
// 点对数量:(n - i + 1) * (m - j + 1)
LL cnt = (n - i + 1) * (m - j + 1);
// 共线三点数:(gcd(i,j) - 1) * cnt
diagonal += cnt * (gcd(i, j) - 1);
}
}
diagonal *= 2; // 斜率正负对称
// 答案 = 总三点数 - 共线三点数
LL ans = total - horizontal - vertical - diagonal;
cout << ans << endl;
return 0;
}★★★ 高阶级,核心考察 “正难则反”“数论与组合计数的结合”,是算法竞赛中的经典题型,需要扎实的基础和灵活的思维。
通过以上 7 道题的实战,我们可以总结出组合计数的核心解题思维:
组合计数不是孤立的公式记忆,而是一系列思维方法的综合应用。本文的 7 道题覆盖了从入门到高阶的核心考点,每道题都对应一种关键思维技巧。 学习组合计数的关键的是:多做题、多总结,遇到新题时先尝试拆解问题,思考 “这道题可以转化为哪种组合模型?”“是否可以用正难则反?”“是否需要拆分问题?”。 希望本文能帮助你打通组合计数的 “任督二脉”,在后续的算法竞赛和实际应用中灵活运用!如果有任何疑问或建议,欢迎在评论区留言交流~