Комбинаторика по модулю
Биномиальные коэффициенты за константу после линейной подготовки: факториалы, обратные факториалы одним проходом и типовые формулы.
5 мин
Половина комбинаторных задач заканчивается словами «выведите ответ по модулю ». Причина проста: ответы вроде — числа в шестьдесят тысяч цифр, и хранить их незачем.
Считать биномиальные коэффициенты по модулю умеют двумя способами, и выбор между ними определяется размером .
Когда n мало: треугольник Паскаля
При до пары тысяч проще всего заполнить таблицу по рекуррентности :
vector<vector<long long>> c(n + 1, vector<long long>(n + 1, 0));
for (int i = 0; i <= n; i++) {
c[i][0] = 1;
for (int k = 1; k <= i; k++)
c[i][k] = (c[i - 1][k - 1] + c[i - 1][k]) % MOD;
}
Плюс — работает при любом модуле, даже составном: здесь только сложение. Минус — времени и памяти, дальше нескольких тысяч не масштабируется.
Когда n велико: факториалы
Основная схема. Предподсчитываем факториалы и обратные к ним, дальше каждый коэффициент — два умножения.
const long long MOD = 1000000007;
const int N = 200001;
long long fact[N], invFact[N];
long long power(long long base, long long exp, long long mod) {
long long result = 1;
base %= mod;
while (exp > 0) {
if (exp & 1) result = result * base % mod;
base = base * base % mod;
exp >>= 1;
}
return result;
}
void build() {
fact[0] = 1;
for (int i = 1; i < N; i++) fact[i] = fact[i - 1] * i % MOD;
invFact[N - 1] = power(fact[N - 1], MOD - 2, MOD);
for (int i = N - 1; i > 0; i--) invFact[i - 1] = invFact[i] * i % MOD;
}
long long C(int n, int k) {
if (k < 0 || k > n) return 0;
return fact[n] * invFact[k] % MOD * invFact[n - k] % MOD;
}
Здесь есть один приём, ради которого стоит читать код внимательно.
Обратные факториалы за один проход
Наивно обратный к каждому факториалу считается возведением в степень: факториалов по — это операций. Терпимо, но лишнее.
Правильный ход: посчитать возведением в степень только последний обратный, а дальше идти справа налево по тождеству
Один power на всю подготовку вместо штук. Строка invFact[i - 1] = invFact[i] * i % MOD — это ровно оно.
Проверка, что всё собралось верно: должно выполняться fact[i] * invFact[i] % MOD == 1 для каждого , и invFact[0] == 1.
Что важно помнить про модуль
Схема с обратными факториалами требует, чтобы модуль был простым и больше . Оба условия существенны.
Простота нужна, чтобы работала малая теорема Ферма. Если модуль составной, обратного к некоторым числам просто не существует.
Условие нужно, чтобы факториал не обнулился: как только в попадает множитель , весь факториал становится нулём по модулю, и никакой обратный его не воскресит. Для и до это не проблема, а вот при модуле вроде и — уже да. Такие случаи решает теорема Люка: коэффициент считают по цифрам и в системе счисления с основанием и перемножают.
Типовые формулы
Что чаще всего приходится выражать через :
| задача | формула |
|---|---|
| выбрать из | |
| упорядоченный выбор из | |
| сочетания с повторениями | |
| пути в сетке вправо-вверх | |
| разложить на неотрицательных слагаемых | |
| перестановки с повторами |
Отдельно стоит запомнить два тождества, потому что они превращают квадратичный перебор в один коэффициент:
Формула включений-исключений
Спутник комбинаторики по модулю. Когда «посчитать напрямую» не выходит, считают все варианты и вычитают плохие:
Практический вид — сумма по подмножествам со знаком . Если множеств до 20, перебираются все масок; если групп немного и они однотипны, сумма сворачивается в один цикл.
Типичный пример — количество путей в сетке с запрещёнными клетками: считаем все пути, вычитаем проходящие через первое препятствие, добавляем прошедшие через два, и так далее.
Важная деталь при работе по модулю: вычитание может уйти в минус. Каждый раз после - нужно нормализовать: answer = ((answer - bad) % MOD + MOD) % MOD. Забытая нормализация даёт отрицательный ответ, который судья не примет, — и найти её тяжело, потому что на маленьких тестах она обычно не проявляется.