Algoritmo Divide y Vencerás
Algoritmo Divide y Vencerás
Algoritmo Divide y Vencerás
Ir a la navegaciónIr a la búsqueda
Esta técnica es la base de los algoritmos eficientes para casi cualquier tipo de
problema como, por ejemplo, algoritmos de ordenamiento (quicksort, mergesort, entre
muchos otros), multiplicar números grandes (Karatsuba), análisis sintácticos
(análisis sintáctico top-down) y la transformada discreta de Fourier.
Por otra parte, analizar y diseñar algoritmos de DyV son tareas que lleva tiempo
dominar. Al igual que en la inducción, a veces es necesario sustituir el problema
original por uno más complejo para conseguir realizar la recursión, y no hay un
método sistemático de generalización.
El nombre divide y vencerás también se aplica a veces a algoritmos que reducen cada
problema a un único subproblema, como la búsqueda binaria para encontrar un
elemento en una lista ordenada (o su equivalente en computación numérica, el
algoritmo de bisección para búsqueda de raíces). Estos algoritmos pueden ser
implementados más eficientemente que los algoritmos generales de “divide y
vencerás”; en particular, si es usando una serie de recursiones que lo convierten
en simples bucles. Bajo esta amplia definición, sin embargo, cada algoritmo que usa
recursión o bucles puede ser tomado como un algoritmo de “divide y vencerás”. El
nombre decrementa y vencerás ha sido propuesta para la subclase simple de
problemas.
Índice
1 Precedentes históricos
2 Diseño e implementación
2.1 Recursión
2.2 Pila explícita
2.3 Tamaño de la pila
2.4 Elegir los casos base
2.5 Compartir subproblemas repetidos
3 Ventajas
3.1 Resolución de problemas complejos
3.2 Eficiencia del algoritmo
3.3 Paralelismo
3.4 Acceso a memoria
3.5 Control del redondeo
4 Desventajas
5 Ejemplos
6 Referencias
Precedentes históricos
La búsqueda binaria, un algoritmo de divide y vencerás en el que el problema
original es partido sucesivamente en subproblemas simples de más o menos la mitad
del tamaño, tiene una larga historia. La idea de usar una lista ordenada de objetos
para facilitar su búsqueda data de la antigua Babilonia en el 200 a. C., mientras
que una descripción del algoritmo en ordenadores apareció en 1946 en un artículo de
John Mauchly. Otro algoritmo de “divide y vencerás” con un único subproblema es el
algoritmo de Euclides para computar el máximo común divisor de dos números
(mediante reducción de números a problemas equivalentes cada vez más pequeños), que
data de muchos siglos antes de Cristo.
Otro ejemplo notable es el algoritmo inventado por A. Karatsuba en 1960 que puede
multiplicar dos números de n dígitos en {\displaystyle O(n^{\log _{2}3})}
{\displaystyle O(n^{\log _{2}3})}. El algoritmo refutó la conjetura de Andréi
Kolmogórov en 1956 que esa tarea requería Ω(n2) operaciones. Otro ejemplo de
algoritmo de divide y vencerás que originalmente no se usaba en ordenador, Knuth
dio el método que habitualmente una oficina de correos usa para dirigir las cartas:
las cartas se ordenan en diferentes bolsas en función de su área geográfica, cada
una de estas bolsas a su vez se ordena en diferentes lotes para subregiones más
pequeñas, y así sucesivamente hasta que son repartidas. Esto está relacionado con
el ordenamiento Radix, descrito para las máquinas de ordenación de tarjetas
perforadas a los comienzos .
Diseño e implementación
La resolución de un problema mediante esta técnica consta fundamentalmente de los
siguientes pasos:
if esCasoBase(p)
return resuelve(p)
else
subproblemas: array of TipoProblema
subproblemas = divideEnSubproblemas(p)
soluciones_parciales: array of TipoSolucion
return mezcla(soluciones_parciales)
endIf
finAlgoritmoDyV
Por el hecho de usar un diseño recursivo, los algoritmos diseñados mediante la
técnica de Divide y Vencerás van a heredar las ventajas e inconvenientes que la
recursión plantea:
Por un lado el diseño que se obtiene suele ser simple, claro, robusto y elegante,
lo que da lugar a una mayor legibilidad y facilidad de depuración y mantenimiento
del código obtenido.
Por contra, los diseños recursivos conllevan normalmente un mayor tiempo de
ejecución que los iterativos, además de la complejidad espacial que puede
representar el uso de la pila de recursión.
Sin embargo, este tipo de algoritmos también se pueden implementar como un
algoritmo no recursivo que almacene las soluciones parciales en una estructura de
datos explícita, como puede ser una pila, cola, o cola de prioridad. Esta
aproximación da mayor libertad al diseñador, de forma que se pueda escoger qué
subproblema es el que se va a resolver a continuación, lo que puede ser importante
en el caso de usar técnicas como Ramificación y acotación o de optimización.
Recursión
Los algoritmos de “divide y vencerás” están naturalmente implementados, como
procesos recursivos. En ese caso, los subproblemas parciales encabezados por aquel
que ya ha sido resuelto se almacenan en la pila de llamadas de procedimientos.
Pila explícita
Los algoritmos de divide y vencerás también pueden ser implementados por un
programa no recursivo que almacena los subproblemas parciales en alguna estructura
de datos explícita, tales como una pila, una cola, o una cola de prioridad. Este
enfoque permite más libertad a la hora de elegir los subproblemas a resolver
después, una característica que es importante en algunas aplicaciones, por ejemplo
en la búsqueda en anchura y en el método de ramificación y poda para optimización
de subproblemas. Este enfoque es también la solución estándar en lenguajes de
programación que no permiten procedimientos recursivos.
Tamaño de la pila
En implementaciones recursivas de algoritmos de DyV, debe asegurarse que hay
suficiente memoria libre para la pila de recursión, sino la ejecución puede fallar
por desbordamiento de la pila. Afortunadamente, los algoritmos DyV que son
eficientes temporalmente tienen una profundidad recursiva relativamente pequeña.
Por ejemplo, el algoritmo quicksort puede ser implementado de forma que nunca
requiere más de {\displaystyle log_{2}n}{\displaystyle log_{2}n} llamadas
recursivas para ordenar n elementos.
Elegir los casos base más pequeños y simples posibles es más elegante y normalmente
nos da lugar a programas más simples, porque hay menos casos a considerar y son más
fáciles de resolver. Por ejemplo, un algoritmo FFT podría parar la recursión cuando
la entrada es una muestra simple, y el algoritmo quicksort de ordenación podría
parar cuando la entrada es una lista vacía. En ambos casos, sólo hay que considerar
un caso base, y no requiere procesamiento.
Ventajas
Resolución de problemas complejos
Este modelo algorítmico es una herramienta potente para solucionar problemas
complejos, tales como el clásico juego de las torres de Hanói. Todo lo que necesita
este algoritmo es dividir el problema en subproblemas más sencillos, y éstos en
otros más sencillos hasta llegar a unos subproblemas sencillos (también llamados
casos base). Una vez ahí, se resuelven y se combinan los subproblemas en orden
inverso a su inicio. Cómo dividir los problemas es, a menudo, la parte más compleja
del algoritmo. Por eso, en muchos problemas, el modelo sólo ofrece la solución más
sencilla, no la mejor.
Caso p < c: Es decir, si el problema se divide en pocas partes de gran tamaño cada
una, entonces la sumatoria (4) es O(1) dado que se puede aplicar directamente la
fórmula {\displaystyle \sum _{i=0}^{n-1}{a^{i}}={\frac {1-a^{n}}{1-a}}}
{\displaystyle \sum _{i=0}^{n-1}{a^{i}}={\frac {1-a^{n}}{1-a}}} si a>1.
Caso en que p == c:
Paralelismo
Este tipo de algoritmos se adapta de forma natural a la ejecución en entornos
multiprocesador, especialmente en sistemas de memoria compartida donde la
comunicación de datos entre los procesadores no necesita ser planeada por
adelantado, por lo que subproblemas distintos se pueden ejecutar en procesadores
distintos.
Acceso a memoria
Los algoritmos que siguen el paradigma Divide y vencerás, tienden naturalmente a
hacer un uso eficiente de las memorias cachés. La razón es que una vez que un
subproblema es lo suficientemente pequeño, él y todos sus subproblemas se pueden,
en principio, solucionar dentro de esa caché, sin tener acceso a la memoria
principal, que es del orden de decenas de veces más lenta. Un algoritmo diseñado
para aprovechar la memoria caché de esta manera se llama modelo caché-olvidadiza,
olvidadiza porque no contiene el tamaño de la memoria como parámetro explícito. Por
otra parte, estos algoritmos se pueden diseñar para muchos problemas importantes,
tales como ordenación, la multiplicación de matrices, de manera que se haga un uso
óptimo de la caché. En contraste, el acercamiento tradicional para explotar la
caché es hacer bloques, de esta forma, el problema se divide explícitamente en las
partes de tamaños apropiados para que se pueda utilizar al caché de forma óptima,
pero solamente cuando el algoritmo es mejorado para el tamaño específico de la
caché de una máquina particular. La misma ventaja existe en lo que respecta a otros
sistemas jerárquicos de memoria, por ejemplo NUMA o memoria virtual, así como para
niveles múltiples de caché: una vez que un subproblema es suficientemente pequeño,
puede ser solucionado dentro de un nivel dado de la jerarquía, sin tener que
acceder al más alto (más lento).
Desventajas
La principal desventaja de este método es su lentitud en la repetición del proceso
recursivo: los gastos indirectos de las llamadas recursivas a la resolución de los
subproblemas, junto con el hecho de tener que almacenar la pila de llamadas (el
estado en cada punto en la repetición), pueden empeorar cualquier mejora hasta
entonces lograda. Esta tarea, sin embargo, depende del estilo de la implementación:
con casos base lo suficientemente grandes, se reducen los gastos indirectos de la
repetición de las llamadas.
Ejemplos
Multiplicación de enteros grandes: algoritmo eficiente para multiplicar números de
tamaño considerable, que se salen de los límites de representación, y no abordable
con los algoritmos clásicos debido al excesivo coste.
Subvector de suma máxima: Algoritmo eficiente para encontrar subcadenas dentro de
un vector evitando tener que recorrer todo el vector desde cada posición.