Что такое сочетание?
Сочетание — это способ выбрать k элементов из n элементов, где порядок не важен.
порядок не учитывается
Например, если есть числа:
1 2 3
Сочетания по 2:
1 2
1 3
2 3
Здесь 1 2 и 2 1 — это одно и то же сочетание.
Обозначение
Количество сочетаний обозначают так:
C(n, k)
Или так:
n choose k
Это читается:
Формула сочетаний
Формула:
C(n, k) = n! / (k! * (n - k)!)
Например:
C(5, 2) = 5! / (2! * 3!)
Считаем:
5! = 120
2! = 2
3! = 6
C(5, 2) = 120 / (2 * 6) = 10
Почему порядок не важен?
Допустим, выбираем 2 человека из 3:
A B C
Сочетания:
A B
A C
B C
Пары A B и B A считаются одинаковыми.
Сочетания, размещения, перестановки
| Тип | Что значит | Порядок важен? |
|---|---|---|
| Сочетания | Выбрать k из n | Нет |
| Размещения | Выбрать k из n и расставить | Да |
| Перестановки | Расставить все n элементов | Да |
Пример разницы
Есть:
1 2 3
Сочетания по 2:
1 2
1 3
2 3
Размещения по 2:
1 2
2 1
1 3
3 1
2 3
3 2
Свойства C(n,k)
Главные свойства:
C(n, 0) = 1
C(n, n) = 1
C(n, k) = C(n, n - k)
Почему C(n, 0) = 1? Потому что есть ровно один способ выбрать ничего.
Почему C(n, n) = 1? Потому что есть ровно один способ выбрать все элементы.
Формула Паскаля
Очень важная формула:
C(n, k) = C(n - 1, k - 1) + C(n - 1, k)
Смысл:
или не берем последний элемент
- C(n - 1, k - 1) — последний элемент взяли
- C(n - 1, k) — последний элемент не взяли
Треугольник Паскаля
Значения C(n,k) можно представить как треугольник:
n = 0: 1
n = 1: 1 1
n = 2: 1 2 1
n = 3: 1 3 3 1
n = 4: 1 4 6 4 1
n = 5: 1 5 10 10 5 1
Например:
C(5, 2) = 10
Подсчет C(n,k) через DP
Можно считать через формулу Паскаля:
C[n][k] = C[n - 1][k - 1] + C[n - 1][k]
База:
C[i][0] = 1;
C[i][i] = 1;
Код: C(n,k) через DP
#include <bits/stdc++.h>
using namespace std;
const int N = 1000;
long long C[N + 1][N + 1];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
for (int n = 0; n <= N; n++) {
C[n][0] = 1;
C[n][n] = 1;
for (int k = 1; k < n; k++) {
C[n][k] = C[n - 1][k - 1] + C[n - 1][k];
}
}
int n, k;
cin >> n >> k;
cout << C[n][k];
return 0;
}
Подсчет по модулю
В задачах часто нужно считать:
C(n, k) mod MOD
Тогда в DP просто берем остаток после сложения.
C[n][k] = (C[n - 1][k - 1] + C[n - 1][k]) % MOD;
Код: C(n,k) через DP по модулю
#include <bits/stdc++.h>
using namespace std;
const int N = 1000;
const long long MOD = 1000000007;
long long C[N + 1][N + 1];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
for (int n = 0; n <= N; n++) {
C[n][0] = 1;
C[n][n] = 1;
for (int k = 1; k < n; k++) {
C[n][k] = (C[n - 1][k - 1] + C[n - 1][k]) % MOD;
}
}
int n, k;
cin >> n >> k;
cout << C[n][k];
return 0;
}
C(n,k) через факториалы
Если n большой и много запросов, удобно заранее посчитать факториалы:
fact[i] = i!
Тогда:
C(n, k) = fact[n] / (fact[k] * fact[n-k])
По модулю деление заменяется на обратный элемент.
Быстрая степень
Для обратного элемента по простому модулю используем:
inv(x) = x^(MOD - 2) mod MOD
Поэтому нужна быстрая степень:
long long binpow(long long a, long long b) {
long long res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = res * a % MOD;
}
a = a * a % MOD;
b /= 2;
}
return res;
}
Код: C(n,k) через factorial
#include <bits/stdc++.h>
using namespace std;
const int N = 1000000;
const long long MOD = 1000000007;
long long fact[N + 1];
long long invFact[N + 1];
long long binpow(long long a, long long b) {
long long res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = res * a % MOD;
}
a = a * a % MOD;
b /= 2;
}
return res;
}
long long inv(long long x) {
return binpow(x, MOD - 2);
}
void build() {
fact[0] = 1;
for (int i = 1; i <= N; i++) {
fact[i] = fact[i - 1] * i % MOD;
}
invFact[N] = inv(fact[N]);
for (int i = N - 1; i >= 0; i--) {
invFact[i] = invFact[i + 1] * (i + 1) % MOD;
}
}
long long comb(int n, int k) {
if (k < 0 || k > n) {
return 0;
}
return fact[n] * invFact[k] % MOD * invFact[n - k] % MOD;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
build();
int n, k;
cin >> n >> k;
cout << comb(n, k);
return 0;
}
Генерация сочетаний
Иногда нужно не количество, а вывести все сочетания.
Например, для:
n = 4
k = 2
Нужно вывести:
1 2
1 3
1 4
2 3
2 4
3 4
Идея генерации
Будем строить ответ рекурсивно. На каждом шаге выбираем следующее число.
start — с какого числа можно брать следующий элемент
если cur.size() == k, выводим cur
Чтобы не получить повторы, всегда берем числа в возрастающем порядке.
Код: генерация сочетаний
#include <bits/stdc++.h>
using namespace std;
int n, k;
vector<int> cur;
void gen(int start) {
if ((int)cur.size() == k) {
for (int x : cur) {
cout << x << ' ';
}
cout << '\n';
return;
}
for (int x = start; x <= n; x++) {
cur.push_back(x);
gen(x + 1);
cur.pop_back();
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
gen(1);
return 0;
}
Пример генерации
Input
4 2
Output
1 2
1 3
1 4
2 3
2 4
3 4
Оптимизация генерации
Можно не перебирать лишние числа. Если осталось мало чисел, нет смысла продолжать.
Осталось выбрать:
need = k - cur.size()
Значит последнее возможное первое число:
n - need + 1
Код: оптимизированная генерация
#include <bits/stdc++.h>
using namespace std;
int n, k;
vector<int> cur;
void gen(int start) {
if ((int)cur.size() == k) {
for (int x : cur) {
cout << x << ' ';
}
cout << '\n';
return;
}
int need = k - (int)cur.size();
for (int x = start; x <= n - need + 1; x++) {
cur.push_back(x);
gen(x + 1);
cur.pop_back();
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
gen(1);
return 0;
}
Генерация через битмаски
Если n маленькое, можно перебрать все маски от 0 до 2^n - 1.
В маске бит равен 1, если элемент выбран.
выбраны элементы, где стоят единицы
Для сочетания размера k нужно, чтобы количество единиц было равно k.
Код: сочетания через битмаски
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
for (int mask = 0; mask < (1 << n); mask++) {
if (__builtin_popcount(mask) == k) {
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) {
cout << i + 1 << ' ';
}
}
cout << '\n';
}
}
return 0;
}
next_permutation для сочетаний
Можно создать массив из нулей и единиц. Единица означает, что элемент выбран.
Например, для n = 5, k = 3:
0 0 1 1 1
Потом запускаем next_permutation.
Код: сочетания через next_permutation
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<int> a(n, 0);
for (int i = n - k; i < n; i++) {
a[i] = 1;
}
do {
for (int i = 0; i < n; i++) {
if (a[i] == 1) {
cout << i + 1 << ' ';
}
}
cout << '\n';
} while (next_permutation(a.begin(), a.end()));
return 0;
}
Когда какой способ использовать?
| Задача | Способ |
|---|---|
| Нужно количество C(n,k), n маленькое | DP Паскаля |
| Нужно количество C(n,k), n большое | Факториалы + inverse |
| Нужно вывести все сочетания | Рекурсия / backtracking |
| n маленькое и удобно работать с подмножествами | Битмаски |
| Нужен простой способ через STL | next_permutation |
Асимптотика
| Метод | Время | Память |
|---|---|---|
| DP Паскаля | O(n²) | O(n²) |
| Факториалы build | O(n log MOD) | O(n) |
| Один запрос C(n,k) после build | O(1) | O(n) |
| Генерация всех сочетаний | O(C(n,k) * k) | O(k) |
| Битмаски | O(2^n * n) | O(1) |
Типичные ошибки
- Путать сочетания и перестановки
- Считать 1 2 и 2 1 разными сочетаниями
- Забыть базы C(n,0)=1 и C(n,n)=1
- Получить overflow при обычном подсчете C(n,k)
- Использовать факториалы без модуля для больших n
- Делить по модулю обычным делением
- В генерации не делать pop_back()
- В генерации начинать всегда с 1 и получить повторы
Короткий шаблон генерации
int n, k;
vector<int> cur;
void gen(int start) {
if ((int)cur.size() == k) {
for (int x : cur) {
cout << x << ' ';
}
cout << '\n';
return;
}
for (int x = start; x <= n; x++) {
cur.push_back(x);
gen(x + 1);
cur.pop_back();
}
}
Короткий шаблон C(n,k) mod
long long comb(int n, int k) {
if (k < 0 || k > n) {
return 0;
}
return fact[n] * invFact[k] % MOD * invFact[n - k] % MOD;
}
Главное запомнить
порядок не важен
C(n,k) = n! / (k! * (n-k)!)
C(n,k) = C(n-1,k-1) + C(n-1,k)
все сочетания удобно генерировать рекурсией