← Все вопросы

Как считать C(n, k) по модулю, избегая переполнения, если модуль не нужен?

Задан 24 месяца назад714 просмотров2 ответа
8

В задаче ответ — это C(n,k), и он гарантированно влезает в 64 бита (по условию мал), но промежуточные n! огромны и переполняют long long ещё до деления. Как посчитать само значение C(n,k) точно, без модуля и без длинной арифметики, не словив переполнение?

2 ответа

13
✓ Принятый ответ — помог автору

Не вычисляй факториалы целиком — строй C(n,k) множительно, чередуя умножение и точное деление, чтобы значение никогда не превышало финальный (маленький) ответ:

C(n,k) = ∏_{i=1}^{k} (n−k+i)/i.

Ключ в том, что после i-го шага накопленное значение равно C(n−k+i, i) — это всегда целое, поэтому деление точное и без остатка.

unsigned long long binom(int n, int k) {
    if (k < 0 || k > n) return 0;
    k = min(k, n - k);                 // симметрия: меньше итераций и меньше рост
    unsigned long long res = 1;
    for (int i = 1; i <= k; i++) {
        res = res / i * (n - k + i)    // НЕ переполнит, т.к. res делится на i на этом шаге
            + (res % i) * (n - k + i) / i; // безопасный вариант, если порядок умножения важен
    }
    return res;
}

Простой и обычно достаточный вариант — res = res * (n-k+i) / i; (деление точное, т.к. промежуток — биномиальный коэффициент). Сложность O(k). Симметрия k = min(k, n−k) и удержание значения «маленьким» — главная защита от переполнения. Если даже ответ велик — нужен __int128 или длинная арифметика.

5

Тонкость: в res * (n-k+i) / i промежуточное произведение res * (n-k+i) может переполнить, даже если итог мал — потому что res на этом шаге уже близок к ответу, а домножение временно его раздувает до деления. Если ответ близок к границе 64 бит, считай через __int128 для промежутка: res = (unsigned long long)((__int128)res * (n-k+i) / i); — это убирает риск переполнения промежуточного произведения.

Ваш ответ

Войдите, чтобы ответить на вопрос.