Dividir y Conquistar
Publicado el 1 de diciembre de 2025
Dominando la Complejidad: El Método Dividir y Conquistar
El paradigma Dividir y Conquistar (Divide and Conquer) es una de las técnicas algorítmicas más poderosas, capaz de transformar problemas aparentemente intratables en soluciones elegantes y eficientes.
1. Definición: Principios Fundamentales
Dividir y Conquistar (D&C)
Dividir y Conquistar es un método de diseño algorítmico que consiste en la estrategia de descomponer sistemáticamente un problema de gran escala en subproblemas más pequeños y manejables.
Idealmente, estos subproblemas son instancias del mismo tipo que el problema original.
Propósito Central
Éxito
El propósito de D&C es facilitar la solución al enfrentar unidades de trabajo menos abrumadoras.
Esta simplificación permite concentrarse en detalles específicos que podrían pasarse por alto si se estudiara la totalidad del sistema.
Ventajas Adicionales
- Procesamiento paralelo efectivo: Facilita la descomposición de tareas en múltiples procesadores
- Claridad conceptual: Cada subproblema es más fácil de entender y resolver
- Reutilización: Los subproblemas suelen ser del mismo tipo, permitiendo recursión elegante
def divide_y_conquista(problema):
""" Patrón general de Dividir y Conquistar """
# Caso base: problema suficientemente pequeño
if es_simple(problema): return
resolver_directamente(problema)
# DIVIDIR: Descomponer en subproblemas
subproblemas = dividir(problema)
# CONQUISTAR: Resolver recursivamente
soluciones_parciales = []
for subproblema in subproblemas:
solucion = divide_y_conquista(subproblema)
soluciones_parciales.append(solucion)
# COMBINAR: Fusionar soluciones
solucion_final = combinar(soluciones_parciales)
return solucion_final ¿Qué característica hace que un subproblema sea ideal para Dividir y Conquistar?
2. El Flujo del Proceso D&C
El procedimiento de Dividir y Conquistar se implementa a través de tres fases operacionales:
Fase 1: División
Dividir
Se segmenta el problema de entrada en múltiples subproblemas.
Típicamente, esto implica dividir el conjunto de entrada en mitades, tercios, o segmentos más manejables.
Fase 2: Conquista
Conquistar
Se resuelven cada uno de estos subproblemas de manera recursiva.
El caso base de la recursión maneja subproblemas suficientemente pequeños que se pueden resolver directamente.
Fase 3: Combinación
Combinar
Se reestructuran las soluciones parciales obtenidas para generar una solución final y completa.
La eficiencia de esta fase es crucial para el rendimiento total del algoritmo.
Diagrama del Proceso
Problema Original (tamaño n)
|
[DIVIDIR]
|
+-------------+-------------+
| |
Subproblema 1 Subproblema 2
(tamaño n/2) (tamaño n/2)
| |
[CONQUISTAR] [CONQUISTAR]
(recursivo) (recursivo)
| |
Solución 1 Solución 2
| |
+-------------+-------------+
|
[COMBINAR]
|
Solución Final Ejemplo: Merge Sort
Problema: Ordenar el array [38, 27, 43, 3, 9, 82, 10]
def merge_sort(arr):
"""Merge Sort usando Dividir y Conquistar"""
# Caso base
if len(arr) <= 1:
return arr
# DIVIDIR
mid = len(arr) // 2
izquierda = arr[:mid]
derecha = arr[mid:]
# CONQUISTAR (recursión)
izquierda_ordenada = merge_sort(izquierda)
derecha_ordenada = merge_sort(derecha)
# COMBINAR
return merge(izquierda_ordenada, derecha_ordenada)
def merge(izq, der):
"""Combinar dos arrays ordenados"""
resultado = []
i = j = 0
while i < len(izq) and j < len(der):
if izq[i] <= der[j]:
resultado.append(izq[i])
i += 1
else:
resultado.append(der[j])
j += 1
resultado.extend(izq[i:])
resultado.extend(der[j:])
return resultado
Árbol de recursión:
[38, 27, 43, 3, 9, 82, 10]
|
[DIVIDIR]
|
+---------------+---------------+
| |
[38, 27, 43] [3, 9, 82, 10]
| |
[DIVIDIR] [DIVIDIR]
| |
+-----+-----+ +-----+-----+
| | | |
[38] [27, 43] [3, 9] [82, 10]
| | |
[DIVIDIR] [DIVIDIR] [DIVIDIR]
| | |
+---+---+ +---+---+ +---+---+
| | | | | |
[27] [43] [3] [9] [82] [10]
════════════ COMBINAR (de abajo hacia arriba) ════════════
[27] [43] [3] [9] [82] [10]
| | | | | |
+---+---+ +---+---+ +---+---+
| | |
[27, 43] [3, 9] [10, 82]
| | |
| [COMBINAR] [COMBINAR]
| | |
| +-----+-----+
| |
| [3, 9, 10, 82]
| |
[COMBINAR] [COMBINAR]
| |
+------------+------------+
|
[COMBINAR]
|
[3, 9, 10, 27, 38, 43, 82]Análisis:
- División: niveles (dividiendo por 2 cada vez)
- Combinación: en cada nivel (merge lineal)
- Total:
Clave de Eficiencia
La eficiencia de un algoritmo D&C se maximiza cuando la fusión de las soluciones requiere un tiempo menor que la resolución de los subproblemas individuales.
Si combinar es muy costoso, D&C puede no ofrecer ventajas.
3. Análisis de Complejidad: Relaciones de Recurrencia
Relaciones de Recurrencia
El rendimiento de los algoritmos D&C se modela mediante relaciones de recurrencia: ecuaciones donde la función de tiempo se auto-define.
Forma General
La relación de recurrencia estándar para D&C, donde un problema de tamaño se divide en piezas de tamaño :
Donde:
- = número de subproblemas
- = tamaño de cada subproblema
- = tiempo dedicado a dividir y combinar
Ejemplos Comunes
| Algoritmo | Recurrencia | Explicación | Complejidad |
|---|---|---|---|
| Búsqueda Binaria | 1 subproblema de tamaño , división constante | ||
| Merge Sort | 2 subproblemas de tamaño , merge lineal | ||
| Multiplicación ingenua | 4 subproblemas de tamaño | ||
| Karatsuba | 3 subproblemas de tamaño |
El Teorema Maestro
Teorema Maestro
Para recurrencias de la forma , donde :
Caso 1: Si , entonces
Caso 2: Si , entonces
Caso 3: Si , entonces
Ejemplo 1: Merge Sort
Recurrencia:
- (dos subproblemas)
- (cada uno de tamaño )
- , entonces
Verificar: y , entonces
Caso 2: ✓
Ejemplo 2: Búsqueda Binaria
Recurrencia:
- (un subproblema)
- (tamaño )
- , entonces
Verificar: y , entonces
Caso 2: ✓
Ejemplo 3: Karatsuba
Recurrencia:
- (tres subproblemas)
- (tamaño )
- , entonces
Verificar: y , entonces
Caso 1: ✓
En la recurrencia T(n) = 4T(n/2) + O(n), ¿qué significa el coeficiente 4?
4. Búsqueda Binaria: El Ejemplo Arquetípico
Binary Search
La búsqueda binaria es el ejemplo fundamental y arquetípico de Dividir y Conquistar. Se utiliza para buscar rápidamente una clave en una matriz ordenada.
Procedimiento
Dado: Un array ordenado S y una clave de búsqueda q
Proceso:
- Comparar
qcon el elemento centralS[n/2] - Si
q < S[n/2], buscar recursivamente en la mitad izquierda - Si
q > S[n/2], buscar recursivamente en la mitad derecha - Si
q == S[n/2], ¡encontrado!
Implementación
def busqueda_binaria(arr, objetivo, izq=0, der=None):
"""
Búsqueda binaria recursiva
Tiempo: O(log n)
Espacio: O(log n) por la pila de recursión
"""
if der is None:
der = len(arr) - 1
# Caso base: no encontrado
if izq > der:
return -1
# DIVIDIR: Encontrar el punto medio
mid = (izq + der) // 2
# CONQUISTAR: ¿Es el elemento buscado?
if arr[mid] == objetivo:
return mid
# CONQUISTAR (recursivo): Buscar en el subproblema apropiado
if objetivo < arr[mid]:
return busqueda_binaria(arr, objetivo, izq, mid - 1)
else:
return busqueda_binaria(arr, objetivo, mid + 1, der)
# Versión iterativa (más eficiente en espacio)
def busqueda_binaria_iterativa(arr, objetivo):
"""
Búsqueda binaria iterativa
Tiempo: O(log n)
Espacio: O(1)
"""
izq, der = 0, len(arr) - 1
while izq <= der:
mid = (izq + der) // 2
if arr[mid] == objetivo:
return mid
elif objetivo < arr[mid]:
der = mid - 1
else:
izq = mid + 1
return -1
# Ejemplo
numeros = [2, 5, 8, 12, 16, 23, 38, 45, 56, 67, 78]
print(busqueda_binaria(numeros, 23)) # Resultado: 5
print(busqueda_binaria(numeros, 100)) # Resultado: -1 (no encontrado) Análisis de Eficiencia
Comparación: Búsqueda Lineal vs Binaria
| Tamaño | Búsqueda Lineal | Búsqueda Binaria | Mejora |
|---|---|---|---|
| 10 | 10 comparaciones | 4 comparaciones | 2.5x |
| 100 | 100 comparaciones | 7 comparaciones | 14x |
| 1,000 | 1,000 comparaciones | 10 comparaciones | 100x |
| 1,000,000 | 1,000,000 comparaciones | 20 comparaciones | 50,000x |
| 1,000,000,000 | 1 mil millones | 30 comparaciones | 33 millones x |
Traza de ejecución:
Buscar 23 en [2, 5, 8, 12, 16, 23, 38, 45, 56, 67, 78]
Paso 1: [2, 5, 8, 12, 16, |23|, 38, 45, 56, 67, 78]
mid = 5, arr[5] = 23
23 == 23 → ¡Encontrado en índice 5!
Total: 1 comparaciónBuscar 45 en el mismo array:
Paso 1: [2, 5, 8, 12, 16, |23|, 38, 45, 56, 67, 78]
mid = 5, arr[5] = 23
45 > 23 → Buscar en mitad derecha
Paso 2: [38, 45, |56|, 67, 78]
mid = 2 (índice global 7), arr[7] = 56
45 < 56 → Buscar en mitad izquierda
Paso 3: [38, |45|]
mid = 1 (índice global 6), arr[6] = 45
45 == 45 → ¡Encontrado en índice 6!
Total: 3 comparacionesRecurrencia:
Aplicando el Teorema Maestro: , ,
, entonces Caso 2:
Variantes Avanzadas
Pro Tip
La búsqueda binaria es la base para numerosas variantes, como:
- Buscar el primer/último elemento que cumple una condición
- Contar ocurrencias en un bloque contiguo:
- Búsqueda en arrays rotados
- Encontrar el punto de rotación
def buscar_primero(arr, objetivo):
"""Encuentra la primera ocurrencia de objetivo"""
izq, der = 0, len(arr) - 1
resultado = -1
while izq <= der:
mid = (izq + der) // 2
if arr[mid] == objetivo:
resultado = mid
der = mid - 1 # Continuar buscando a la izquierda
elif objetivo < arr[mid]:
der = mid - 1
else:
izq = mid + 1
return resultado
def buscar_ultimo(arr, objetivo):
"""Encuentra la última ocurrencia de objetivo"""
izq, der = 0, len(arr) - 1
resultado = -1
while izq <= der:
mid = (izq + der) // 2
if arr[mid] == objetivo:
resultado = mid
izq = mid + 1 # Continuar buscando a la derecha
elif objetivo < arr[mid]:
der = mid - 1
else:
izq = mid + 1
return resultado
def contar_ocurrencias(arr, objetivo):
"""
Cuenta las ocurrencias de objetivo en O(log n)
"""
primero = buscar_primero(arr, objetivo)
if primero == -1:
return 0
ultimo = buscar_ultimo(arr, objetivo)
return ultimo - primero + 1
# Ejemplo
numeros = [1, 2, 2, 2, 2, 3, 4, 7, 8, 8]
print(contar_ocurrencias(numeros, 2)) # 4
print(contar_ocurrencias(numeros, 8)) # 2
print(contar_ocurrencias(numeros, 5)) # 0
5. Multiplicación Rápida: Algoritmo de Karatsuba
Problema
La multiplicación estándar de dos números de dígitos toma tiempo usando el método que aprendemos en la escuela.
¿Podemos hacerlo más rápido usando Dividir y Conquistar?
Enfoque Ingenuo D&C
Idea: Dividir cada número en dos mitades
Para números e de dígitos:
Entonces:
Expandiendo:
Esto requiere 4 multiplicaciones de números de dígitos:
Recurrencia:
Aplicando Teorema Maestro: , ,
, entonces Caso 1:
❌ No hay mejora sobre el método estándar
El Algoritmo de Karatsuba
Observación Clave de Karatsuba
Podemos calcular usando solo 3 multiplicaciones en lugar de 4:
Ya calculamos y , ¡así que solo necesitamos una multiplicación adicional!
def karatsuba(x, y):
"""
Multiplicación de Karatsuba
Tiempo: O(n^1.585) donde n = número de dígitos
"""
# Caso base
if x < 10 or y < 10:
return x * y
# Calcular número de dígitos
n = max(len(str(x)), len(str(y)))
mitad = n // 2
# DIVIDIR: Separar en mitades
potencia = 10 ** mitad
x_l = x // potencia # Mitad izquierda de x
x_r = x % potencia # Mitad derecha de x
y_l = y // potencia # Mitad izquierda de y
y_r = y % potencia # Mitad derecha de y
# CONQUISTAR: 3 multiplicaciones recursivas (¡no 4!)
z0 = karatsuba(x_r, y_r) # x_r * y_r
z1 = karatsuba(x_l + x_r, y_l + y_r) # (x_l + x_r)(y_l + y_r)
z2 = karatsuba(x_l, y_l) # x_l * y_l
# COMBINAR
# z1 - z2 - z0 = x_l*y_r + x_r*y_l (el término medio)
return z2 * (10 ** (2 * mitad)) + (z1 - z2 - z0) * (10 ** mitad) + z0
# Ejemplo
x = 1234
y = 5678
print(f"Método estándar: {x * y}")
print(f"Karatsuba: {karatsuba(x, y)}")
# Para números grandes, la diferencia es notable
import time
# Número de 100 dígitos
grande_x = 12345678901234567890123456789012345678901234567890
grande_y = 98765432109876543210987654321098765432109876543210
inicio = time.time()
resultado_normal = grande_x * grande_y
tiempo_normal = time.time() - inicio
inicio = time.time()
resultado_karatsuba = karatsuba(grande_x, grande_y)
tiempo_karatsuba = time.time() - inicio
print(f"\nNúmeros de 50 dígitos:")
print(f"Método normal: {tiempo_normal:.6f}s")
print(f"Karatsuba: {tiempo_karatsuba:.6f}s") Análisis de Karatsuba
Recurrencia:
Aplicando Teorema Maestro: , ,
, entonces Caso 1:
✅ ¡Mejora sobre !
Comparación de tiempos para multiplicar números de dígitos:
| Dígitos | Estándar | Karatsuba | Mejora |
|---|---|---|---|
| 10 | 100 ops | 32 ops | 3.1x |
| 100 | 10,000 ops | 1,000 ops | 10x |
| 1,000 | 1,000,000 ops | 31,623 ops | 31x |
| 10,000 | 100,000,000 ops | 1,000,000 ops | 100x |
Visualización del ahorro:
Método Estándar:
X × Y requiere 4 multiplicaciones recursivas
┌───────┬───────┐
│ XL×YL │ XL×YR │
├───────┼───────┤
│ XR×YL │ XR×YR │
└───────┴───────┘
4 multiplicaciones de n/2 dígitos
Karatsuba:
Solo 3 multiplicaciones usando truco algebraico
┌───────┐
│ XL×YL │ z2
├───────┤
│ (XL+XR)×(YL+YR) │ z1
├───────┤
│ XR×YR │ z0
└───────┘
XL×YR + XR×YL = z1 - z2 - z0 (sin multiplicación extra!) ¿Por qué el algoritmo de Karatsuba es más rápido que la multiplicación ingenua de D&C?
6. Consideraciones sobre Paralelismo
Advertencia: No todo D&C es Paralelizable
Si bien D&C sugiere paralelismo natural (resolver subproblemas independientemente), no todos los algoritmos D&C se paralelizan fácilmente.
Tipos de Paralelismo
| Tipo | Descripción | Ejemplo | Paralelizable |
|---|---|---|---|
| Paralelismo de Datos | Aplicar el mismo algoritmo a conjuntos de datos independientes | Merge Sort | ✅ Sí |
| Paralelismo Secuencial | Cada paso depende del resultado anterior | Búsqueda Binaria | ❌ No |
Búsqueda Binaria: Inherentemente Secuencial
Peligro
Algoritmos como la búsqueda binaria son inherentemente secuenciales. Si cada paso (o “pregunta”) depende del resultado de los pasos anteriores, la ganancia de velocidad se ve limitada.
# Búsqueda binaria - cada decisión depende de la anterior
def busqueda_binaria_no_paralelizable(arr, objetivo):
izq, der = 0, len(arr) - 1
# Paso 1: Verificar elemento central
mid1 = (izq + der) // 2
if arr[mid1] == objetivo:
return mid1
# Paso 2: DEPENDE del resultado del Paso 1
# No puedo decidir dónde buscar hasta conocer arr[mid1]
if objetivo < arr[mid1]:
# Ahora busco en la izquierda
der = mid1 - 1
else:
# Ahora busco en la derecha
izq = mid1 + 1
# Paso 3: DEPENDE del resultado del Paso 2
# Y así sucesivamente...
# ❌ No puedo ejecutar los pasos 1, 2 y 3 simultáneamente
# porque cada uno necesita el resultado del anterior Merge Sort: Paralelizable Eficientemente
from concurrent.futures import ThreadPoolExecutor
def merge_sort_paralelo(arr, profundidad=0, max_profundidad=3):
"""
Merge Sort con paralelismo limitado
"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
izquierda = arr[:mid]
derecha = arr[mid:]
# Solo paralelizar en los primeros niveles
if profundidad < max_profundidad:
with ThreadPoolExecutor(max_workers=2) as executor:
# ✅ Estos dos pueden ejecutarse en paralelo
futuro_izq = executor.submit(
merge_sort_paralelo, izquierda, profundidad + 1, max_profundidad
)
futuro_der = executor.submit(
merge_sort_paralelo, derecha, profundidad + 1, max_profundidad
)
izq_ordenada = futuro_izq.result()
der_ordenada = futuro_der.result()
else:
# En niveles profundos, usar versión secuencial
izq_ordenada = merge_sort_paralelo(izquierda, profundidad + 1, max_profundidad)
der_ordenada = merge_sort_paralelo(derecha, profundidad + 1, max_profundidad)
return merge(izq_ordenada, der_ordenada) Balanceo de Carga
Problema: Desbalanceo
Una mala división del trabajo puede resultar en que algunos procesos terminen rápidamente, mientras que uno final y pesado mantiene el resto del sistema inactivo.
Caso de estudio: Búsqueda de la Conjetura de Waring
Problema: Verificar una propiedad para todos los números en un rango enorme.
Intento ingenuo:
Procesador 1: Números 1 - 1,000,000 → Termina rápido
Procesador 2: Números 1,000,001 - 2,000,000 → Termina rápido
Procesador 3: Números 2,000,001 - 3,000,000 → Termina rápido
Procesador 4: Números 3,000,001 - 4,000,000 → TARDA MUCHO
Tiempo total = Tiempo del procesador más lentoProblema: Si algunos números requieren mucho más cálculo que otros, la división uniforme del rango no garantiza balance de carga.
Solución: Work Stealing (Robo de Trabajo)
Cola de tareas: [Tarea1, Tarea2, Tarea3, ..., Tarea1000]
Procesador 1: Toma Tarea1 → Termina → Toma siguiente tarea disponible
Procesador 2: Toma Tarea2 → Termina → Toma siguiente tarea disponible
Procesador 3: Toma Tarea3 → Termina → Toma siguiente tarea disponible
Procesador 4: Toma Tarea4 → Termina → Toma siguiente tarea disponible
Los procesadores que terminan primero "roban" trabajo de la colaVentaja: Balance dinámico - ningún procesador queda ocioso si hay trabajo pendiente.
Recomendación para Paralelismo
Mejores Prácticas
Se recomienda restringir la atención a algoritmos de paralelismo de datos donde:
- Los subproblemas son verdaderamente independientes
- La comunicación entre procesos sea mínima
- La comunicación se limite a la recolección de la salida final
Razón: El procedimiento de depuración de algoritmos con comunicación no determinista es notoriamente difícil.
7. Aplicaciones Adicionales de D&C
Quick Sort
def quick_sort(arr):
"""
Quick Sort usando Dividir y Conquistar
Promedio: O(n log n)
Peor caso: O(n²)
"""
if len(arr) <= 1:
return arr
# DIVIDIR: Elegir pivote y particionar
pivote = arr[len(arr) // 2]
menores = [x for x in arr if x < pivote]
iguales = [x for x in arr if x == pivote]
mayores = [x for x in arr if x > pivote]
# CONQUISTAR: Ordenar recursivamente
menores_ordenados = quick_sort(menores)
mayores_ordenados = quick_sort(mayores)
# COMBINAR: Concatenar (trivial)
return menores_ordenados + iguales + mayores_ordenados
# Ejemplo
numeros = [64, 34, 25, 12, 22, 11, 90]
print(quick_sort(numeros))
Máximo y Mínimo Simultáneos
Problema: Encontrar tanto el máximo como el mínimo de un array.
Solución ingenua: comparaciones (recorrer para max, luego para min)
Solución D&C: Solo comparaciones
def max_min_dc(arr, izq, der):
"""
Encuentra max y min usando D&C
Comparaciones: 3n/2 - 2 (mejor que 2n - 2)
"""
# Caso base: un elemento
if izq == der:
return arr[izq], arr[izq]
# Caso base: dos elementos
if der == izq + 1:
if arr[izq] < arr[der]:
return arr[der], arr[izq] # max, min
else:
return arr[izq], arr[der]
# DIVIDIR
mid = (izq + der) // 2
# CONQUISTAR
max_izq, min_izq = max_min_dc(arr, izq, mid)
max_der, min_der = max_min_dc(arr, mid + 1, der)
# COMBINAR
max_total = max(max_izq, max_der) # 1 comparación
min_total = min(min_izq, min_der) # 1 comparación
return max_total, min_total
# Uso
numeros = [3, 5, 1, 9, 2, 8, 4, 7, 6]
maximo, minimo = max_min_dc(numeros, 0, len(numeros) - 1)
print(f"Máximo: {maximo}, Mínimo: {minimo}") Análisis:
- Cada nivel compara pares de resultados: 2 comparaciones por combinación
- Niveles:
- Elementos comparados por nivel: decrece geométricamente
- Total: comparaciones (mejor que )
8. Cierre y Conclusiones
Poder de Dividir y Conquistar
Dividir y Conquistar se mantiene como una de las técnicas algorítmicas más poderosas, transformando la complejidad cuadrática en soluciones logarítmicas o casi lineales .
Lecciones Clave
1. Reconocimiento de Convolución
Pro Tip
Muchos problemas que implican buscar todas las formas posibles de combinar elementos (multiplicación de polinomios, string matching avanzado) pueden reformularse como una convolución, lo que permite algoritmos “mágicos” de .
2. Análisis Riguroso
Advertencia
La comprensión del comportamiento de las relaciones de recurrencia es la clave para predecir si un diseño D&C será eficiente o si simplemente mantendrá la complejidad del problema original.
Usar el Teorema Maestro para análisis rápido.
3. Diseño para Paralelismo
Información
Aunque D&C sugiere paralelismo, se recomienda:
- ✅ Enfocarse en paralelismo de datos
- ✅ Minimizar comunicación entre procesos
- ✅ Limitar comunicación a recolección de salida final
- ❌ Evitar dependencias secuenciales cuando sea posible
Checklist para Aplicar D&C
¿Cuándo usar Dividir y Conquistar?
✅ Sí, si:
- El problema se puede dividir en subproblemas similares
- Los subproblemas son independientes
- La solución se puede reconstruir desde las partes
- Existe un caso base claro
- La combinación es eficiente
❌ No, si:
- Los subproblemas no son independientes (usar Programación Dinámica)
- La división/combinación es muy costosa
- No hay estructura recursiva natural
- El problema es inherentemente secuencial
Pasos para diseñar un algoritmo D&C:
-
Identificar la estructura recursiva
- ¿Cómo se puede dividir el problema?
- ¿Los subproblemas son del mismo tipo?
-
Definir el caso base
- ¿Cuándo es el problema lo suficientemente pequeño?
- ¿Se puede resolver directamente?
-
Diseñar la división
- ¿En cuántas partes dividir? (2, 3, k partes)
- ¿De qué tamaño? (n/2, n/3, √n)
-
Resolver subproblemas
- Aplicar recursión
- Confiar en la corrección de las llamadas recursivas
-
Combinar soluciones
- ¿Cómo se fusionan los resultados?
- Minimizar el costo de combinación
-
Analizar complejidad
- Escribir la recurrencia:
- Aplicar Teorema Maestro
- Verificar que haya mejora
Comparación de Paradigmas
| Paradigma | Cuándo Usar | Ejemplos | Complejidad Típica |
|---|---|---|---|
| Dividir y Conquistar | Subproblemas independientes | Merge Sort, Binary Search, Karatsuba | a |
| Programación Dinámica | Subproblemas superpuestos | Fibonacci, Knapsack, Camino más corto | a |
| Greedy | Elección local óptima | Dijkstra, Huffman, Kruskal | a |
| Backtracking | Exploración exhaustiva | N-Queens, Sudoku, Laberintos | a |
Resumen de Conceptos Clave
Estructura de D&C
- Dividir: Descomponer en subproblemas
- Conquistar: Resolver recursivamente
- Combinar: Fusionar soluciones
Análisis
- Recurrencia:
- Teorema Maestro: Determina complejidad según , y
Ejemplos Clave
- Búsqueda Binaria: - 1 subproblema
- Merge Sort: - 2 subproblemas
- Karatsuba: - 3 subproblemas (truco algebraico)
Paralelismo
- ✅ Paralelizable: Subproblemas independientes (Merge Sort)
- ❌ Secuencial: Dependencias entre pasos (Binary Search)
Cuándo Usar D&C
- Problema divisible en partes similares
- Subproblemas independientes
- Combinación eficiente
- Estructura recursiva natural
Regla de Oro
“Divide el problema hasta que sea trivial, luego combina las soluciones de forma eficiente”
¿Cuál es la ventaja principal de D&C sobre métodos iterativos simples?
¡Felicitaciones!
Has completado el paradigma de Dividir y Conquistar. Ahora comprendes:
- Los tres pasos fundamentales: Dividir, Conquistar, Combinar
- Cómo analizar algoritmos D&C con relaciones de recurrencia
- El Teorema Maestro para determinar complejidad
- Ejemplos clásicos: Binary Search, Merge Sort, Karatsuba
- Consideraciones sobre paralelismo y balanceo de carga
- Cuándo aplicar (y cuándo no) este paradigma
Próximo paso: Explorar otros paradigmas como Programación Dinámica, Algoritmos Greedy, y técnicas avanzadas de optimización.