Articulo de referencia

Algoritmo de divide y vencerás

En informática , la máxima política «divide y vencerás » designa un paradigma de diseño de algoritmos . Un algoritmo de divide y vencerás descompone recursivamente un problema e...

En informática , la máxima política «divide y vencerás » designa un paradigma de diseño de algoritmos . Un algoritmo de divide y vencerás descompone recursivamente un problema en dos o más subproblemas del mismo tipo o relacionados, hasta que estos se vuelven lo suficientemente simples como para resolverse directamente. Las soluciones a los subproblemas se combinan para obtener la solución al problema original.

La técnica de divide y vencerás es la base de algoritmos eficientes para muchos problemas, como la ordenación (por ejemplo, ordenación rápida , ordenación por fusión ), la multiplicación de números grandes (por ejemplo, el algoritmo de Karatsuba ), la búsqueda del par de puntos más cercanos , el análisis sintáctico (por ejemplo, analizadores sintácticos descendentes ), la resolución de problemas SAT [ 1 ] y el cálculo de la transformada discreta de Fourier ( FFT ) [ 2 ] .

Diseñar algoritmos eficientes de divide y vencerás puede ser difícil. Al igual que en la inducción matemática , a menudo es necesario generalizar el problema para que admita una solución recursiva . La corrección de un algoritmo de divide y vencerás generalmente se demuestra mediante inducción matemática, y su costo computacional suele determinarse resolviendo relaciones de recurrencia .

Divide y vencerás

Método de divide y vencerás para ordenar la lista (38, 27, 43, 3, 9, 82, 10) en orden ascendente. Mitad superior: división en sublistas; mitad central: una lista de un solo elemento se ordena fácilmente; mitad inferior: composición de las sublistas ordenadas.

El paradigma de divide y vencerás se utiliza a menudo para encontrar la solución óptima a un problema. Su idea básica consiste en descomponer un problema dado en dos o más subproblemas similares, pero más sencillos, resolverlos sucesivamente y combinar sus soluciones para resolver el problema original. Los problemas suficientemente sencillos se resuelven directamente. Por ejemplo, para ordenar una lista de n números naturales , se divide en dos listas de aproximadamente n /2 números cada una, se ordenan sucesivamente y se intercalan ambos resultados adecuadamente para obtener la versión ordenada de la lista original (véase la imagen). Este método se conoce como algoritmo de ordenación por fusión .

El término «divide y vencerás» se aplica a veces a algoritmos que reducen cada problema a un único subproblema, como el algoritmo de búsqueda binaria para encontrar un registro en una lista ordenada (o su análogo en computación numérica , el algoritmo de bisección para encontrar raíces ). [ 3 ] Estos algoritmos pueden implementarse de forma más eficiente que los algoritmos generales de divide y vencerás; en particular, si utilizan recursión de cola , pueden convertirse en bucles simples . Sin embargo, bajo esta definición amplia, cualquier algoritmo que utilice recursión o bucles podría considerarse un «algoritmo de divide y vencerás». Por lo tanto, algunos autores consideran que el término «divide y vencerás» solo debería utilizarse cuando cada problema puede generar dos o más subproblemas. [ 4 ] En su lugar, se ha propuesto el término «reducir y vencerás» para la clase de un solo subproblema. [ 5 ]

Una aplicación importante de divide y vencerás se encuentra en la optimización, donde si el espacio de búsqueda se reduce ("podar") por un factor constante en cada paso, el algoritmo general tiene la misma complejidad asintótica que el paso de poda, con la constante dependiendo del factor de poda (sumando la serie geométrica ); esto se conoce como podar y buscar .

Ejemplos históricos tempranos

Los primeros ejemplos de estos algoritmos son principalmente de reducción y conquista: el problema original se descompone sucesivamente en subproblemas individuales y, de hecho, se puede resolver de forma iterativa.

La búsqueda binaria , un algoritmo de reducción y conquista donde los subproblemas son aproximadamente la mitad del tamaño original, tiene una larga historia. Si bien una descripción clara del algoritmo en computadoras apareció en 1946 en un artículo de John Mauchly , la idea de usar una lista ordenada de elementos para facilitar la búsqueda se remonta al menos a Babilonia en el año 200  a. C. [ 6 ] Otro antiguo algoritmo de reducción y conquista es el algoritmo euclidiano para calcular el máximo común divisor de dos números reduciendo los números a subproblemas equivalentes cada vez más pequeños, que data de varios siglos antes de Cristo.

Un ejemplo temprano de un algoritmo de divide y vencerás con múltiples subproblemas es la descripción de Gauss de 1805 de lo que ahora se llama el algoritmo de transformada rápida de Fourier (FFT) de Cooley-Tukey , [ 7 ] aunque no analizó cuantitativamente su recuento de operaciones , y las FFT no se generalizaron hasta que fueron redescubiertas más de un siglo después.

Un algoritmo D&C de dos subproblemas temprano que fue desarrollado específicamente para computadoras y analizado adecuadamente es el algoritmo de ordenación por fusión , inventado por John von Neumann en 1945. [ 8 ]

Otro ejemplo notable es el algoritmo inventado por Anatolii A. Karatsuba en 1960 [ 9 ] que podía multiplicar dos números de n dígitos enO(norteregistro23){\displaystyle O(n^{\log _{2}3})}operaciones (en notación Big O ). Este algoritmo refutó la conjetura de Andrey Kolmogorov de 1956 queΩ(norte2){\displaystyle \Omega (n^{2})}Para esa tarea se requerirían operaciones.

Como otro ejemplo de un algoritmo de divide y vencerás que originalmente no involucraba computadoras, Donald Knuth describe el método que una oficina de correos suele usar para enrutar el correo: las cartas se clasifican en bolsas separadas para diferentes áreas geográficas, cada una de estas bolsas se clasifica a su vez en lotes para subregiones más pequeñas, y así sucesivamente hasta que se entregan. [ 6 ] Esto está relacionado con una clasificación por radix , descrita para máquinas clasificadoras de tarjetas perforadas ya en 1929. [ 6 ]

Ventajas

Resolver problemas difíciles

Divide y vencerás es una herramienta poderosa para resolver problemas conceptualmente difíciles: solo requiere una forma de dividir el problema en subproblemas, resolver los casos triviales y combinar los subproblemas con el problema original. De manera similar, disminuir y vencerás solo requiere reducir el problema a un único problema más pequeño, como el clásico rompecabezas de la Torre de Hanoi , que reduce el movimiento de una torre de alturanorte{\displaystyle n}mover una torre de alturanorte1{\displaystyle n-1}.

Eficiencia del algoritmo

El paradigma de divide y vencerás suele ser útil para descubrir algoritmos eficientes. Fue clave, por ejemplo, para el método de multiplicación rápida de Karatsuba , los algoritmos quicksort y mergesort, el algoritmo de Strassen para la multiplicación de matrices y las transformadas rápidas de Fourier.

En todos estos ejemplos, el enfoque D&C condujo a una mejora en el costo asintótico de la solución. Por ejemplo, si (a) los casos base tienen un tamaño constante y acotado, el trabajo de dividir el problema y combinar las soluciones parciales es proporcional al tamaño del problema.norte{\displaystyle n}y (b) hay un número acotadopag{\displaystyle p}de subproblemas de tamaño ~nortepag{\displaystyle {\frac {n}{p}}}En cada etapa, el costo del algoritmo de divide y vencerás seráO(norteregistropagnorte){\displaystyle O(n\log _{p}n)}.

Para otros tipos de enfoques de divide y vencerás, los tiempos de ejecución también se pueden generalizar. Por ejemplo, cuando a) el trabajo de dividir el problema y combinar las soluciones parciales llevadonorte{\displaystyle cn}tiempo, dondenorte{\displaystyle n}es el tamaño de entrada ydo{\displaystyle c}es alguna constante; b) cuandonorte<2{\displaystyle n<2}, el algoritmo tarda un tiempo limitado superiormente pordo{\displaystyle c}y c) hayq{\displaystyle q}subproblemas donde cada subproblema tiene un tamaño ~norte2{\displaystyle {\frac {n}{2}}}Los tiempos de ejecución son los siguientes:

  • si el número de subproblemasq>2{\displaystyle q>2}, entonces el tiempo de ejecución del algoritmo divide y vencerás está limitado porO(norteregistro2q){\displaystyle O(n^{\log _{2}q})}.
  • Si el número de subproblemas es exactamente uno, entonces el tiempo de ejecución del algoritmo divide y vencerás está acotado porO(norte){\displaystyle O(n)}. [ 10 ]

Si, en cambio, el trabajo de dividir el problema y combinar las soluciones parciales llevadonorte2{\displaystyle cn^{2}}tiempo, y hay 2 subproblemas donde cada uno tiene tamañonorte2{\displaystyle {\frac {n}{2}}}, entonces el tiempo de ejecución del algoritmo divide y vencerás está limitado porO(norte2){\displaystyle O(n^{2})}. [ 10 ]

Paralelismo

Los algoritmos de divide y vencerás se adaptan naturalmente para su ejecución en máquinas multiprocesador , especialmente en sistemas de memoria compartida donde la comunicación de datos entre procesadores no necesita planificarse con antelación, ya que los distintos subproblemas pueden ejecutarse en diferentes procesadores.

Acceso a la memoria

Los algoritmos de divide y vencerás tienden naturalmente a hacer un uso eficiente de las cachés de memoria . La razón es que, una vez que un subproblema es lo suficientemente pequeño, este y todos sus subproblemas pueden, en principio, resolverse dentro de la caché , sin acceder a la memoria principal , que es más lenta . Un algoritmo diseñado para explotar la caché de esta manera se denomina ajeno a la caché , porque no contiene el tamaño de la caché como un parámetro explícito . [ 11 ] Además, los algoritmos de D&C pueden diseñarse para algoritmos importantes (por ejemplo, ordenación, FFT y multiplicación de matrices) para que sean algoritmos óptimos ajenos a la caché; utilizan la caché de una manera probablemente óptima, en un sentido asintótico, independientemente del tamaño de la caché. En contraste, el enfoque tradicional para explotar la caché es bloqueante , como en la optimización de anidamiento de bucles , donde el problema se divide explícitamente en fragmentos del tamaño apropiado; esto también puede usar la caché de manera óptima, pero solo cuando el algoritmo está ajustado para los tamaños de caché específicos de una máquina en particular.

La misma ventaja se aplica a otros sistemas de almacenamiento jerárquico, como NUMA o la memoria virtual , así como a múltiples niveles de caché: una vez que un subproblema es lo suficientemente pequeño, se puede resolver dentro de un nivel determinado de la jerarquía, sin acceder a los niveles superiores (más lentos).

Control de redondeo

En cálculos con aritmética redondeada, por ejemplo, con números de coma flotante , un algoritmo de divide y vencerás puede producir resultados más precisos que un método iterativo superficialmente equivalente. Por ejemplo, se pueden sumar N números mediante un bucle simple que suma cada dato a una sola variable, o mediante un algoritmo de divide y vencerás llamado suma por pares , que divide el conjunto de datos en dos mitades, calcula recursivamente la suma de cada mitad y luego suma las dos sumas. Si bien el segundo método realiza la misma cantidad de sumas que el primero y conlleva la sobrecarga de las llamadas recursivas, suele ser más preciso. [ 12 ]

Problemas de implementación

Recursión

Los algoritmos de divide y vencerás se implementan naturalmente como procedimientos recursivos . En ese caso, los subproblemas parciales que conducen al que se está resolviendo se almacenan automáticamente en la pila de llamadas del procedimiento . Una función recursiva es una función que se llama a sí misma dentro de su definición.

Pila explícita

Los algoritmos de divide y vencerás también pueden implementarse mediante un programa no recursivo que almacena los subproblemas parciales en una estructura de datos explícita, como una pila , una cola o una cola de prioridad . Este enfoque permite mayor libertad en la elección del subproblema a resolver, una característica importante en algunas aplicaciones, como la recursión en amplitud y el método de ramificación y acotación para la optimización de funciones. Este enfoque es también la solución estándar en lenguajes de programación que no admiten procedimientos recursivos.

Tamaño de la pila

En las implementaciones recursivas de algoritmos D&C, es necesario asegurarse de que se asigne suficiente memoria para la pila de recursión; de lo contrario, la ejecución podría fallar debido a un desbordamiento de pila . Los algoritmos D&C que son eficientes en tiempo suelen tener una profundidad de recursión relativamente pequeña. Por ejemplo, el algoritmo quicksort se puede implementar de manera que nunca requiera más deregistro2norte{\displaystyle \log _{2}n}llamadas recursivas anidadas para ordenarnorte{\displaystyle n}elementos.

El desbordamiento de pila puede ser difícil de prevenir al usar procedimientos recursivos, ya que muchos compiladores tratan la pila de recursión como un bloque de memoria contiguo y algunos le reservan una cantidad fija de espacio. Además, los compiladores pueden almacenar más información en la pila de recursión de la estrictamente necesaria, incluyendo la dirección de retorno, los parámetros sin modificar y las variables locales del procedimiento. Por lo tanto, la probabilidad de desbordamiento de pila puede reducirse limitando el número de parámetros y variables locales en el procedimiento recursivo, o reemplazando la recursión con una estructura de datos de pila explícita.

Selección de los casos base

En cualquier algoritmo recursivo, existe una considerable libertad en la elección de los casos base , los pequeños subproblemas que se resuelven directamente para finalizar la recursión.

Elegir los casos base más pequeños o sencillos posibles es más elegante y suele dar lugar a programas más simples, ya que hay menos casos que considerar y son más fáciles de resolver. Por ejemplo, un algoritmo de Transformada Rápida de Fourier podría detener la recursión cuando la entrada es una sola muestra, y el algoritmo de ordenación rápida de listas podría detenerse cuando la entrada es una lista vacía; en ambos ejemplos, solo hay un caso base que considerar y no requiere ningún procesamiento.

Por otro lado, la eficiencia suele mejorar si la recursión se detiene en casos base relativamente grandes y estos se resuelven de forma no recursiva, lo que da como resultado un algoritmo híbrido . Esta estrategia evita la sobrecarga de las llamadas recursivas que realizan poco o ningún trabajo y también puede permitir el uso de algoritmos no recursivos especializados que, para esos casos base, son más eficientes que la recursión explícita. Un procedimiento general para un algoritmo recursivo híbrido simple es el cortocircuito del caso base , también conocido como recursión de longitud de brazo . En este caso, se comprueba si el siguiente paso dará como resultado el caso base antes de la llamada a la función, evitando una llamada innecesaria. Por ejemplo, en un árbol, en lugar de recurrir a un nodo hijo y luego comprobar si es nulo, comprobar la nulidad antes de recurrir evita la mitad de las llamadas a funciones en algunos algoritmos sobre árboles binarios. Dado que un algoritmo D&C finalmente reduce cada instancia de problema o subproblema a un gran número de instancias base, estas suelen dominar el coste total del algoritmo, especialmente cuando la sobrecarga de división/unión es baja. Cabe señalar que estas consideraciones no dependen de si la recursión se implementa mediante el compilador o mediante una pila explícita.

Así, por ejemplo, muchas implementaciones de biblioteca de quicksort cambiarán a un algoritmo de ordenación por inserción simple basado en bucles (o similar) una vez que el número de elementos a ordenar sea suficientemente pequeño. Tenga en cuenta que, si la lista vacía fuera el único caso base, ordenar una lista connorte{\displaystyle n}Las entradas implicarían como máximonorte{\displaystyle n}Llamadas a quicksort que no harían nada más que devolver un valor inmediatamente. Aumentar los casos base a listas de tamaño 2 o menos eliminará la mayoría de esas llamadas que no hacen nada, y, en general, se suele utilizar un caso base mayor que 2 para reducir la fracción de tiempo dedicado a la sobrecarga de llamadas a funciones o a la manipulación de la pila.

Alternativamente, se pueden emplear casos base grandes que aún utilizan un algoritmo de divide y vencerás, pero implementando el algoritmo para un conjunto predeterminado de tamaños fijos donde el algoritmo se puede desplegar completamente en código sin recursión, bucles ni condicionales (relacionado con la técnica de evaluación parcial ). Por ejemplo, este enfoque se utiliza en algunas implementaciones eficientes de FFT, donde los casos base son implementaciones desplegadas de algoritmos FFT de divide y vencerás para un conjunto de tamaños fijos. [ 13 ] Se pueden utilizar métodos de generación de código fuente para producir la gran cantidad de casos base separados que se desean para implementar esta estrategia de manera eficiente. [ 13 ]

La versión generalizada de esta idea se conoce como "desenrollamiento" o "engrosamiento" de la recursión, y se han propuesto varias técnicas para automatizar el procedimiento de ampliación del caso base. [ 14 ]

Programación dinámica para subproblemas superpuestos

En algunos problemas, la recursión ramificada puede terminar evaluando el mismo subproblema varias veces. En tales casos, puede ser útil identificar y guardar las soluciones de estos subproblemas superpuestos, una técnica conocida como memorización . Llevada al extremo, esta técnica da lugar a algoritmos de divide y vencerás de abajo hacia arriba , como la programación dinámica .

Véase también

Referencias

  1. Heule, Marijn JH ; Kullmann, Oliver; Wieringa, Siert; Biere, Armin (2012), "Cube and Conquer: Guiding CDCL SAT Solvers by Lookaheads", Hardware and Software: Verification and Testing , Lecture Notes in Computer Science, vol.  7261, Springer Berlin Heidelberg, pp. 50–65 , doi : 10.1007/978-3-642-34188-5_8 , ISBN  978-3-642-34187-8
  2. Blahut, Richard (14 de mayo de 2014). Algoritmos rápidos para el procesamiento de señales . Cambridge University Press. págs. 139–143 . ISBN  978-0-511-77637-3.
  3. ^ Thomas H. Cormen; Charles E. Leiserson; Ronald L. Rivest; Clifford Stein (31 de julio de 2009). Introducción a los algoritmos . Prensa del MIT. ISBN 978-0-262-53305-8.
  4. Brassard, G., y Bratley, P. Fundamentos de algoritmia, Prentice-Hall, 1996.
  5. Anany V. Levitin, Introducción al diseño y análisis de algoritmos (Addison Wesley, 2002).
  6. 1 2 3 Donald E. Knuth, El arte de la programación informática: Volumen 3, Ordenación y búsqueda , segunda edición (Addison-Wesley, 1998).
  7. Heideman, MT, DH Johnson y CS Burrus, " Gauss y la historia de la transformada rápida de Fourier ", IEEE ASSP Magazine, 1, (4), 14–21 (1984).
  8. Knuth, Donald (1998). El arte de la programación informática: Volumen 3: Ordenación y búsqueda . Addison-Wesley. pág . 159. ISBN  0-201-89685-0.
  9. Karatsuba, Anatolii A .; Yuri P. Ofman (1962). "Умножение многозначных чисел на автоматах". Doklady Akademii Nauk SSSR . 146 : 293-294 .Traducido en Karatsuba, A.; Ofman, Yu. (1963). "Multiplicación de números de varios dígitos en autómatas" . Soviet Physics Doklady . 7 : 595–596 . Bibcode : 1963SPhD....7..595K .
  10. ^ Kleinberg , Jon; Tardos, Eva (16 de marzo de 2005). Diseño de algoritmos (1 ed.). Educación Pearson . págs. 214-220 . ISBN   9780321295354Consultado el 26 de enero de 2025 .
  11. M. Frigo; CE Leiserson; H. Prokop (1999). «Algoritmos ajenos a la caché» . 40.º Simposio Anual sobre Fundamentos de la Informática (Cat. n.º 99CB37039) . págs. 285–297 . doi : 10.1109/SFFCS.1999.814600 . ISBN  0-7695-0409-4. S2CID 62758836 . 
  12. Nicholas J. Higham, " La precisión de la suma de punto flotante ", SIAM J. Scientific Computing 14 (4), 783–799 (1993).
  13. 1 2 Frigo, M.; Johnson, SG (febrero de 2005). "El diseño e implementación de FFTW3" (PDF) . Actas del IEEE . 93 (2): 216– 231. Bibcode : 2005IEEEP..93..216F . CiteSeerX 10.1.1.66.3097 . doi : 10.1109/JPROC.2004.840301 . S2CID 6644892 .  
  14. Radu Rugina y Martin Rinard, " Desenrollado recursivo para programas de divide y vencerás " en Lenguajes y compiladores para computación paralela , capítulo 3, págs. 34–48. Lecture Notes in Computer Science vol. 2017 (Berlín: Springer, 2001).