DP de divide y vencerás
Divide and Conquer es una optimización de programación dinámica.
Precondiciones
Algunos problemas de programación dinámica tienen una recurrencia de esta forma:
donde es una función de costo y cuando .
Digamos y , y evaluar toma tiempo. Entonces la evaluación directa de la recurrencia de arriba es . Hay estados, y transiciones por cada estado.
Sea el valor de que minimiza la expresión de arriba. Asumiendo que la función de costo satisface la desigualdad del cuadrángulo, podemos mostrar que para todos los . Esto se conoce como la condición de monotonía. Entonces, podemos aplicar DP de divide y vencerás. El “punto de corte” óptimo para un fijo crece a medida que crece.
Esto nos permite resolver todos los estados de forma más eficiente. Digamos que computamos para algún y fijos. Entonces para cualquier sabemos que . Esto significa que al computar , no tenemos que considerar tantos puntos de corte.
Para minimizar el tiempo de ejecución, aplicamos la idea detrás de divide y vencerás. Primero, computamos . Después, computamos , sabiendo que es menor o igual que y sabiendo que es mayor o igual que . Llevando recuenta de forma recursiva las cotas inferiores y superiores de , llegamos a un tiempo de ejecución . Ver el código de abajo para los detalles de implementación.
Para demostrar la complejidad de divide y vencerás, primero nótese que hay niveles en la recursión. Afirmamos que se hacen pasos en cada nivel. Sea la longitud total de los intervalos (denotados por y en el código) en el -ésimo nivel , y observemos que cada vez que un intervalo del nivel de longitud se parte, el/los intervalo(s) resultante(s) tienen longitud total a lo sumo . Además, en el nivel , se realizan a lo sumo cortes, así que . Aplicando la cota por inducción con da que para cada nivel ,
Así, la complejidad de cada divide y vencerás es , y la complejidad de todo el cómputo de DP es .
Implementación genérica
Aunque la implementación varía según el problema, acá hay un template bastante genérico.
La función compute computa una fila de estados dp_cur, dada la fila anterior de estados dp_before.
Hay que llamarla con compute(0, n-1, 0, n-1). La función solve computa m filas y devuelve el resultado.
int m, n;
vector<long long> dp_before, dp_cur;
long long C(int i, int j);
// compute dp_cur[l], ... dp_cur[r] (inclusive)
void compute(int l, int r, int optl, int optr) {
if (l > r)
return;
int mid = (l + r) >> 1;
pair<long long, int> best = {LLONG_MAX, -1};
for (int k = optl; k <= min(mid, optr); k++) {
best = min(best, {(k ? dp_before[k - 1] : 0) + C(k, mid), k});
}
dp_cur[mid] = best.first;
int opt = best.second;
compute(l, mid - 1, optl, opt);
compute(mid + 1, r, opt, optr);
}
long long solve() {
dp_before.assign(n,0);
dp_cur.assign(n,0);
for (int i = 0; i < n; i++)
dp_before[i] = C(0, i);
for (int i = 1; i < m; i++) {
compute(0, n - 1, 0, n - 1);
dp_before = dp_cur;
}
return dp_before[n - 1];
}Cosas a tener en cuenta
La mayor dificultad de los problemas de DP de divide y vencerás es demostrar la monotonía de . Un caso especial en el que esto vale es cuando la función de costo satisface la desigualdad del cuadrángulo, es decir, para todos los . Muchos problemas de DP de divide y vencerás también se pueden resolver con el Convex Hull trick o viceversa. ¡Es útil conocer y entender ambos!
Problemas de práctica
- AtCoder - Yakiniku Restaurants
- CodeForces - Ciel and Gondolas (Be careful with I/O!)
- CodeForces - Levels And Regions
- CodeForces - Partition Game
- CodeForces - The Bakery
- CodeForces - Yet Another Minimization Problem
- Codechef - CHEFAOR
- CodeForces - GUARDS (This is the exact problem in this article.)
- Hackerrank - Guardians of the Lunatics
- Hackerrank - Mining
- Kattis - Money (ACM ICPC World Finals 2017)
- SPOJ - ADAMOLD
- SPOJ - LARMY
- SPOJ - NKLEAVES
- Timus - Bicolored Horses
- USACO - Circular Barn
- UVA - Arranging Heaps
- UVA - Naming Babies