Métodos de Cálculo Combinatorio y Principio de Inclusión-Exclusión

Cálculo de Números Combinatorios

Los números combinatorois, denotados como C(n, k), se pueden calcular mediante diferentes algoritmos optimizados según las restricciones del problema.

1. Preprocesamiento por Recurrencia

Para valores de n hasta 2000, se puede usar una tabla de programación dinámica con complejidad O(n²). El código siguiente precalcula todos los C(i, j) para i y j hasta un límite.

#include <iostream>
#include <vector>
using namespace std;

const int LIMITE = 2001;
const int MODULO = 1e9 + 7;
vector<vector<int>> tabla_comb(LIMITE, vector<int>(LIMITE, 0));

void precalcular_combinaciones() {
    for (int fila = 0; fila < LIMITE; ++fila) {
        tabla_comb[fila][0] = 1;
        for (int col = 1; col <= fila; ++col) {
            tabla_comb[fila][col] = (tabla_comb[fila-1][col] + tabla_comb[fila-1][col-1]) % MODULO;
        }
    }
}

int main() {
    precalcular_combinaciones();
    int consultas;
    cin >> consultas;
    while (consultas--) {
        int n, k;
        cin >> n >> k;
        cout << tabla_comb[n][k] << endl;
    }
    return 0;
}

2. Preprocesamiento con Factoriales e Inversos

Para n hasta 10^5, se precalculan factoriales y sus inversos modulares usando exponenciación rápida. La fórmula es C(n, k) = fact[n] * inv_fact[k] * inv_fact[n-k] % MODULO.

#include <iostream>
#include <vector>
using namespace std;

typedef long long ll;
const int MAX_N = 100001;
const int MOD = 1e9 + 7;
vector<ll> fac(MAX_N, 1), inv_fac(MAX_N, 1);

ll exponenciar_mod(ll base, ll exp, ll mod) {
    ll resultado = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) resultado = (resultado * base) % mod;
        base = (base * base) % mod;
        exp >>= 1;
    }
    return resultado;
}

void preprocesar_factoriales() {
    for (int i = 1; i < MAX_N; ++i) {
        fac[i] = (fac[i-1] * i) % MOD;
    }
    inv_fac[MAX_N-1] = exponenciar_mod(fac[MAX_N-1], MOD-2, MOD);
    for (int i = MAX_N-2; i >= 1; --i) {
        inv_fac[i] = (inv_fac[i+1] * (i+1)) % MOD;
    }
}

int main() {
    preprocesar_factoriales();
    int consultas;
    cin >> consultas;
    while (consultas--) {
        int n, k;
        cin >> n >> k;
        ll combinacion = (((fac[n] * inv_fac[k]) % MOD) * inv_fac[n-k]) % MOD;
        cout << combinacion << endl;
    }
    return 0;
}

3. Teorema de Lucas para Grandes Modulos

Cuando n y k son grandes (hasta 10^18) pero el módulo p es primo y pequeño (hasta 10^5), el teorema de Lucas descompone el prolbema en subproblemas menores usando la representación en base p.

#include <iostream>
#include <vector>
using namespace std;

typedef long long ll;

ll exponenciar_rapida(ll base, ll exp, ll mod) {
    ll res = 1;
    base %= mod;
    while (exp > 0) {
        if (exp & 1) res = (res * base) % mod;
        base = (base * base) % mod;
        exp /= 2;
    }
    return res;
}

ll comb_pequeño(ll n, ll k, ll p) {
    if (k > n) return 0;
    ll numerador = 1, denominador = 1;
    for (int i = 0; i < k; ++i) {
        numerador = (numerador * (n - i)) % p;
        denominador = (denominador * (i + 1)) % p;
    }
    return (numerador * exponenciar_rapida(denominador, p-2, p)) % p;
}

ll teorema_lucas(ll n, ll k, ll p) {
    if (k == 0) return 1;
    return (comb_pequeño(n % p, k % p, p) * teorema_lucas(n / p, k / p, p)) % p;
}

int main() {
    int consultas;
    cin >> consultas;
    while (consultas--) {
        ll n, k, p;
        cin >> n >> k >> p;
        cout << teorema_lucas(n, k, p) << endl;
    }
    return 0;
}

4. Inversos Modulares Lineales

Para calcular inversos modulares de 1 a n en O(n), se usa la recurrencia: inv[i] = MODULO - (MODULO / i) * inv[MODULO % i] % MODULO.

vector<int> inversos(MAX_N, 0);
inversos[1] = 1;
for (int i = 2; i < MAX_N; ++i) {
    inversos[i] = MODULO - (long long)(MODULO / i) * inversos[MODULO % i] % MODULO;
}

Principio de Inclusión-Exclusión

Este principio se utiliza para contar elementos que cumplen al menos una de varias condiciones. Sea U el conjunto universal y S_i el conjunto de elementos que cumplen la condición i. Entonces, el número de elementos en la unión de los S_i se calcula como:

|∪ S_i| = Σ |S_i| - Σ |S_i ∩ S_j| + Σ |S_i ∩ S_j ∩ S_k| - ... + (-1)^{m-1} |∩ S_i|

La implementación típica recorre todos los subconjuntos de condiciones, lo que lleva tiempo O(2^m).

Ejemplo: Conteo con Restricciones

Para calcular cuántos números hasta N son divisibles por al menos uno de un conjunto de primos, se aplica inclusión-exclusión sobre los subconjuntos de primos.

#include <iostream>
#include <vector>
using namespace std;

typedef long long ll;

int main() {
    ll limite;
    int num_primos;
    cin >> limite >> num_primos;
    vector<ll> primos(num_primos);
    for (int i = 0; i < num_primos; ++i) {
        cin >> primos[i];
    }
    ll total = 0;
    for (int mascara = 1; mascara < (1 << num_primos); ++mascara) {
        ll producto = 1;
        int bits = 0;
        bool desbordamiento = false;
        for (int idx = 0; idx < num_primos; ++idx) {
            if (mascara & (1 << idx)) {
                ++bits;
                if (producto > limite / primos[idx]) {
                    desbordamiento = true;
                    break;
                }
                producto *= primos[idx];
            }
        }
        if (desbordamiento) continue;
        ll contribucion = limite / producto;
        if (bits % 2 == 1) {
            total += contribucion;
        } else {
            total -= contribucion;
        }
    }
    cout << total << endl;
    return 0;
}

Aplicación Combinada

Problemas como asignar objetos a personas con restricciones o colorear tableros con condiciones de filas, columnas y colores se resuelven combinando combinatoria e inclusión-exclusión.

Ejemplo: Asignación con Restricciones

Dado m tipos de productos y n personas, donde cada persona debe recibir al menos un producto de cada tipo, se usa la fórmula de combinación con barras y se ajusta con inclusión-exclusión para garantizarr que todas las personas reciban algo.

#include <iostream>
#include <vector>
using namespace std;

typedef long long ll;
const int MOD = 1e9 + 7;
const int MAX_VAL = 5001;

vector<ll> fac(MAX_VAL, 1), inv_fac(MAX_VAL, 1);

ll potencia_mod(ll base, ll exp) {
    ll resultado = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp & 1) resultado = (resultado * base) % MOD;
        base = (base * base) % MOD;
        exp >>= 1;
    }
    return resultado;
}

void inicializar() {
    for (int i = 1; i < MAX_VAL; ++i) {
        fac[i] = (fac[i-1] * i) % MOD;
    }
    inv_fac[MAX_VAL-1] = potencia_mod(fac[MAX_VAL-1], MOD-2);
    for (int i = MAX_VAL-2; i >= 0; --i) {
        inv_fac[i] = (inv_fac[i+1] * (i+1)) % MOD;
    }
}

ll combinatoria(int n, int k) {
    if (k < 0 || k > n) return 0;
    return (((fac[n] * inv_fac[k]) % MOD) * inv_fac[n-k]) % MOD;
}

int main() {
    inicializar();
    int personas, tipos;
    cin >> personas >> tipos;
    vector<int> cantidades(tipos);
    for (int i = 0; i < tipos; ++i) {
        cin >> cantidades[i];
    }
    ll respuesta = 0;
    for (int mascara = 0; mascara <= personas; ++mascara) {
        ll signo = (mascara % 2 == 0) ? 1 : MOD-1;
        ll formas = 1;
        for (int j = 0; j < tipos; ++j) {
            int disponible = cantidades[j] + personas - mascara - 1;
            formas = (formas * combinatoria(disponible, personas - mascara - 1)) % MOD;
        }
        formas = (formas * combinatoria(personas, mascara)) % MOD;
        respuesta = (respuesta + signo * formas) % MOD;
    }
    cout << (respuesta + MOD) % MOD << endl;
    return 0;
}

Etiquetas: binomial coefficients inclusion-exclusion principle C++ modular arithmetic combinatorial mathematics

Publicado el 6-21 23:04