¿Cómo se puede resolver el coeficiente binomial usando programación dinámica y tabla hash?

Permítame recordarle la hermosa propiedad de los coeficientes binomiales que nos permite resolver este problema utilizando la programación dinámica:

[matemáticas] \ binom {n} {k} = \ frac {n} {k} \ binom {n-1} {k-1} [/ matemáticas]

Implementemos una solución recursiva sencilla (C ++ como el código pseuso):

int binom (int n, int k)
{
afirmar (n> = k);
si (k == 0) devuelve 1;
binomio de retorno (n-1, k-1) * n / k;
}

A pesar de ser muy simple, es sorprendentemente eficiente. El árbol de recursión no es ramificado, solo n llamadas recursivas consecutivas, lo que nos da complejidad O (n) tiempo y O (n) espacio (pila de llamadas).

Ahora vamos a agregar memorización usando el mapa hash. Tenga en cuenta que esto no tiene mucho sentido para este problema en particular , pero es muy útil para problemas en los que una función recursiva se llama a sí misma varias veces con diferentes valores de argumento. En tales casos, la memorización generalmente reduce la complejidad del tiempo de tiempo exponencial a tiempo polinómico. Pero hagámoslo de todos modos:

par de estructura
{
int n;
int k;
Par (int N, int K) {n = N; k = K;}
};

HashMap dp;
int binom (int n, int k)
{
afirmar (n> = k);
si (k == 0) devuelve 1;
Par p = Par (n, k);
if (dp.hasKey (p)) devuelve dp.get (p);
int resultado = binom (n-1, k-1) * n / k;
dp.set (p, resultado);
resultado de retorno;
}

Ahora deshazte de la recursividad:

int binom (int n, int k)
{
HashMap dp;
/ * llenar los casos base (k == 0) * /
para (int i = 0; i <= n; i ++)
dp.set (Par (i, 0), 1);

/ *
Ahora llena el resto
En cada iteración garantizamos que (i-1, j-1)
binom ya ha sido calculado
* /
para (int i = 1; i <= n; i ++)
para (int j = 1; j <= k; j ++)
{
int prev = dp.get (Par (i-1, j-1));
dp.set (Par (i, j), anterior * i / j);
}

return dp.get (Par (n, k));
}

¿No crees que un mapa hash es demasiado aquí? Sustitúyalo por una matriz 2d y obtenga un código más elegante:

int binom (int n, int k)
{
int dp [n + 1] [k + 1];
/ * llenar los casos base (k == 0) * /
para (int i = 0; i <= n; i ++)
dp [i] [0] = 1;

para (int i = 1; i <= n; i ++)
para (int j = 1; j <= k; j ++)
dp [i] [j] = dp [i-1] [j-1] * i / j;

devuelve dp [n] [k];
}

Puede observar fácilmente el hecho de que para calcular dp[n][k] solo necesitamos el elemento dp[n-1][k-1] , que en su caso necesita dp[n-2][k-2] precalculado y así sucesivamente hasta alcanzar el caso base dp[nk][0] = 1 . Siga adelante y use la observación:

int binom (int n, int k)
{
afirmar (n> = k);
/ * Mantener dp [i] [j] en el resultado * /
int resultado = 1; // dp [nk] [0]
para (int j = 1; j <= k; j ++)
resultado = resultado * (n-k + j) / j;
resultado de retorno;
}

Súper genial y elegante solución de competencia O (n) tiempo y O (1) espacio.

* Se recomienda encarecidamente corregir errores.

Puede usar un caché para almacenar los resultados de cálculos anteriores, ya que sabe que [math] n + k [/ math] siempre disminuye cuando realiza las llamadas recursivas (por lo que se está acercando a los casos base [math] n = 0 [ / matemática] o [matemática] k = 0 [/ matemática]).

Código:

caché = {}
def bi (n, k):
si (n, k) no está en caché:
si k == 0:
caché [(n, k)] = 1
elif n == 0:
caché [(n, k)] = 0
más:
caché [(n, k)] = bi (n-1, k) + bi (n-1, k-1)
devolver caché [(n, k)]

Aquí está la relación de recurrencia

C [i] [j] = C [i-1] [j] + C [i-1] [j-1]

¿De dónde viene?

C [i] [j] = número de formas en que puede elegir j elementos de i elementos

bueno, puedes dividirlo en dos casos principales:

1- número de formas en que puede elegir la combinación j de los elementos i y el elemento i no está involucrado en la combinación, que es igual a C [i-1] [j] (número de formas en que puede hacerlo usando solo el primeros artículos i-1)

2: número de formas en que puede elegir la combinación j de los elementos i y el elemento i está involucrado en la combinación, y esto se puede ver como cuántas formas de elegir (j-1) combinación de los primeros elementos i-1 ( simplemente puede agregar el elemento i-ésimo a cada combinación) y esto es C [i-1] [j-1]

si construye esta tabla desde C [0] [0] = 1

Y luego cada entrada se puede calcular sumando los 2 elementos que se encuentran sobre ella, se puede ver como un triángulo con vértice C [0] [0]

esto se llama triángulo de pascal