Divide and conquer y recurrence relations | Nicolás Garzón
Divide and conquer separa un problema en subproblemas de la misma forma, los resuelve y combina sus resultados.
Texto
Copiar divide → conquer → combineLa técnica es útil cuando los subproblemas son suficientemente independientes y la combinación es más barata que resolver el problema original directamente.
TypeScript
Copiar function solve ( problem: Problem) : Result {
if ( isSmall ( problem) ) {
return solveDirectly ( problem) ;
}
const parts = divide ( problem) ;
const partialResults = parts. map ( solve) ;
return combine ( partialResults) ;
} No toda recursión es divide and conquer. Una función que reduce n a n - 1 tiene una estructura recursiva, pero no necesariamente divide en subproblemas independientes.
Binary search: conserva una mitad y descarta la otra.
Merge sort: resuelve ambas mitades y las combina.
Quick sort: particiona y ordena regiones.
Exponentiation by squaring: reutiliza una potencia de tamaño medio.
Closest pair of points: divide el plano y combina candidatos cercanos a la frontera.
Cada algoritmo tiene una recurrencia diferente.
Una recurrencia describe el costo de una llamada en función de llamadas menores.
Texto
Copiar T(n) = T(n/2) + O(1)Solo continúa una mitad y hace trabajo constante para comparar y actualizar límites.
Texto
Copiar T(n) = 2T(n/2) + O(n)Resuelve dos mitades y combina n elementos.
Texto
Copiar T(n) = T(n - 1) + O(1)Texto
Copiar T(n) = T(n - 1) + T(n - 2) + O(1)Los subproblemas se repiten. El tiempo crece exponencialmente; no es un divide and conquer eficiente. Memoization cambia el grafo de ejecución al calcular cada estado una vez.
Expande la recurrencia por niveles.
Texto
Copiar nivel 0: n costo n
/ \
nivel 1: n/2 n/2 costo n
/ \ / \
nivel 2: n/4 ... n/4 costo n
...
altura: log nCada nivel suma O(n) y hay O(log n) niveles:
Texto
Copiar O(n) × O(log n) = O(n log n)Este método ayuda a ver cuánto trabajo existe por nivel y cuántos niveles se crean.
Propón una cota y demuéstrala por inducción.
Texto
Copiar T(n) = 2T(n/2) + nSupón T(n/2) ≤ c(n/2) log(n/2) y sustituye:
Texto
Copiar T(n) ≤ cn log(n/2) + n
= cn(log n - 1) + n
= cn log n - cn + nPara un c adecuado, esto es O(n log n).
La sustitución aporta rigor cuando el árbol es menos evidente.
Para recurrencias de la forma:
Texto
Copiar T(n) = aT(n/b) + f(n)
a: cantidad de subproblemas;
n/b: tamaño de cada uno;
f(n): trabajo fuera de las llamadas.
Si f(n) crece polinómicamente más lento:
Texto
Copiar T(n) = Θ(n^(log_b a))Texto
Copiar T(n) = 4T(n/2) + n
n^(log₂4) = n²
resultado Θ(n²)Texto
Copiar f(n) = Θ(n^(log_b a) log^k n)entonces se añade un factor logarítmico:
Texto
Copiar T(n) = Θ(n^(log_b a) log^(k+1) n)Merge sort es el caso k = 0: Θ(n log n).
Si f(n) crece polinómicamente más rápido y cumple una condición de regularidad:
Texto
Copiar T(n) = Θ(f(n))El teorema no aplica a recurrencias como T(n - 1), subproblemas de tamaños desiguales generales o relaciones no ajustadas a esa forma.
Divide and conquer resuelve varios subproblemas: merge sort.
Decrease and conquer reduce a uno menor: binary search o insertion sort.
Transform and conquer cambia la representación: heapify antes de heap sort.
Las categorías ayudan a ver la estructura, pero no sustituyen el análisis concreto.
Calcular base^exponent multiplicando exponent veces cuesta O(exponent).
Texto
Copiar x^n = (x^(n/2))² si n es par
x^n = x · (x^floor(n/2))² si n es imparTypeScript
Copiar function power ( base: number , exponent: number ) : number {
if ( ! Number. isInteger ( exponent) || exponent < 0 ) {
throw new RangeError ( "Exponent must be a non-negative integer" ) ;
}
if ( exponent === 0 ) return 1 ;
const half = power ( base, Math. floor ( exponent / 2 ) ) ;
const squared = half * half;
return exponent % 2 === 0 ? squared : base * squared;
} La llamada a half se hace una sola vez. Escribirla dos veces:
TypeScript
Copiar power ( base, n / 2 ) * power ( base, n / 2 ) crearía dos subárboles y perdería la mejora.
Tiempo: O(log exponent). Call stack: O(log exponent).
Merge sort crea mitades independientes. Fibonacci crea subproblemas superpuestos. Esa diferencia decide si divide and conquer puro evita trabajo o si necesitas DP.
Texto
Copiar fib(5)
├─ fib(4)
│ └─ fib(3)
└─ fib(3) ← repetidoSi diferentes ramas vuelven al mismo estado, considera memoization.
Dividir puede ser barato y combinar costoso, o al revés.
Merge sort divide por índices en O(1) conceptual y combina en O(n).
Quick sort particiona en O(n) y combina en O(1) después de recursión.
Binary search no combina dos respuestas porque solo continúa una rama.
Identificar dónde se hace el trabajo evita recurrencias incorrectas.
Una implementación puede cambiar la complejidad espacial:
TypeScript
Copiar const left = values. slice ( 0 , middle) ; slice copia elementos. Si el algoritmo se describe por índices, pero el código crea arrays en cada nivel, debes contar esas asignaciones.
En merge sort, el pico de memoria suele seguir siendo O(n), pero las constantes y presión de garbage collection aumentan.
Subproblemas independientes pueden ejecutarse en paralelo. Sin embargo:
crear workers tiene overhead;
compartir o copiar datos cuesta;
la combinación puede ser secuencial;
demasiados subproblemas pequeños empeoran el rendimiento.
La estructura permite paralelismo; no lo hace gratis.
tamaño cero;
división que no reduce el problema;
redondeo de mitades;
subproblemas muy desbalanceados;
copias ocultas;
combinación no asociativa;
profundidad excesiva;
overflow numérico en resultados.
Omitir el trabajo de combine.
Multiplicar costos de ramas que en realidad se ejecutan secuencialmente; para tiempo se suman.
Escribir dos veces la misma llamada recursiva en vez de reutilizarla.
Aplicar Master theorem a una recurrencia incompatible.
Declarar O(log n) solo porque el tamaño se divide, aunque existan dos ramas.
Ignorar slice, buffers y call stack.
No garantizar que cada subproblema sea más pequeño.
Confundir subproblemas independientes con repetidos.
La recurrencia debe reflejar cantidad, tamaño y trabajo externo de las llamadas.
Un recursion tree muestra costo por nivel.
T(n/2) y 2T(n/2) producen comportamientos muy distintos.
Subproblemas repetidos sugieren DP; independientes, divide and conquer.
Las copias y la combinación forman parte del costo real.
¿Por qué T(n) = 2T(n/2) + O(1) no es O(log n)?
Respuesta Porque se ejecutan dos ramas en cada nivel. El árbol tiene 2^log n = n hojas y una cantidad total lineal de nodos, por lo que la recurrencia es Θ(n).