El papel de los algoritmos en la informática ¿Qué son los algoritmos? ¿Por qué merece la pena estudiar los algoritmos? ¿Cuál es el papel de los algoritmos en relación con otras tecnologías utilizadas en informática? En este capítulo responderemos a estas preguntas. Antes de que existieran los ordenadores, había algoritmos. Pero ahora que hay ordenadores, hay aún más algoritmos, y los algoritmos constituyen el núcleo de la informática. Este libro ofrece una introducción completa al estudio moderno de los algoritmos informáticos. Presenta muchos algoritmos y los trata con considerable profundidad, pero hace que su diseño y análisis sean accesibles a todos los niveles de lectores. Hemos intentado que las explicaciones sean elementales sin sacrificar la profundidad de la cobertura o el rigor matemático. 1.1 Algoritmos Informalmente, un algoritmo es cualquier procedimiento computacional bien definido que toma algún valor, o conjunto de valores, como entrada y produce algún valor, o conjunto de valores, como salida. Un algoritmo es, por tanto, una secuencia de pasos computacionales que transforman la entrada en la salida. También podemos ver un algoritmo como una herramienta para resolver un problema computacional bien especificado. El enunciado del problema especifica en términos generales la relación entrada/salida deseada. El algoritmo describe un procedimiento computacional específico para conseguir esa relación entrada/salida. Por ejemplo, podríamos necesitar ordenar una secuencia de números en orden no decreciente. Este problema se plantea con frecuencia en la práctica y proporciona un terreno fértil para introducir muchas técnicas de diseño y herramientas de análisis estándar. A continuación, definimos formalmente el problema de ordenación: Por ejemplo, dada la secuencia de entrada h31; 41; 59; 26; 41; 58i, un algoritmo de ordenación devuelve como salida la secuencia h26; 31; 41; 41; 58; 59i. Una secuencia de entrada de este tipo se denomina instancia del problema de ordenación. En general, una instancia de un problema consiste en la entrada (que satisface cualquier restricción impuesta en el enunciado del problema) necesaria para calcular una solución al problema. Dado que muchos programas la utilizan como paso intermedio, la ordenación es una operación fundamental en informática. Como resultado, disponemos de un gran número de buenos algoritmos de ordenación. Cuál es el mejor algoritmo para una aplicación determinada depende, entre otros factores, del número de elementos que haya que ordenar, de hasta qué punto los elementos están ya algo ordenados, de las posibles restricciones en los valores de los elementos, de la arquitectura del ordenador y del tipo de dispositivos de almacenamiento que se vayan a utilizar: memoria principal, discos o incluso cintas. Se dice que un algoritmo es correcto si, para cada instancia de entrada, se detiene con la salida correcta. Decimos que un algoritmo correcto resuelve el problema de cálculo dado. Un algoritmo incorrecto puede no detenerse en algunos casos de entrada, o puede detenerse con una respuesta incorrecta. Al contrario de lo que cabría esperar, los algoritmos incorrectos a veces pueden ser útiles, si podemos controlar su tasa de error. Veremos un ejemplo de algoritmo con una tasa de error controlable en el capítulo 31, cuando estudiemos algoritmos para encontrar números primos grandes. Sin embargo, normalmente sólo nos ocuparemos de los algoritmos correctos. Un algoritmo se puede especificar en inglés, como un programa de ordenador o incluso como un diseño de hardware. El único requisito es que la especificación proporcione una descripción precisa del procedimiento computacional a seguir. ¿Qué tipo de problemas resuelven los algoritmos? La ordenación no es, ni mucho menos, el único problema computacional para el que se han desarrollado algoritmos. (Las aplicaciones prácticas de los algoritmos son omnipresentes e incluyen los siguientes ejemplos: El Proyecto Genoma Humano ha hecho grandes progresos hacia los objetivos de identificar los 100.000 genes del ADN humano, determinar las secuencias de los 3.000 millones de pares de bases químicas que componen el ADN humano, almacenar esta información en bases de datos y desarrollar herramientas para el análisis de datos. Cada uno de estos pasos requiere algoritmos sofisticados. Aunque las soluciones a los distintos problemas implicados quedan fuera del alcance de este libro, muchos métodos para resolver estos problemas biológicos utilizan ideas de varios de los capítulos de este libro, lo que permite a los científicos realizar las tareas utilizando los recursos de forma eficiente. Se ahorra tiempo, tanto humano como mecánico, y dinero, ya que se puede extraer más información de las técnicas de laboratorio. Internet permite a personas de todo el mundo acceder y recuperar rápidamente grandes cantidades de información. Con la ayuda de algoritmos inteligentes, los sitios de Internet son capaces de gestionar y manipular este gran volumen de datos. Ejemplos de problemas que hacen un uso esencial de los algoritmos son encontrar buenas rutas para que viajen los datos (las técnicas para resolver este tipo de problemas se dan en 1.1 Algoritmos 7 Capítulo 24), y utilizar un motor de búsqueda para encontrar rápidamente páginas en las que reside determinada información (las técnicas relacionadas se dan en los Capítulos 11 y 32). El comercio electrónico permite negociar e intercambiar electrónicamente bienes y servicios, y depende de la privacidad de la información personal, como números de tarjetas de crédito, contraseñas y extractos bancarios. Las tecnologías básicas utilizadas en el comercio electrónico incluyen la criptografía de clave pública y las firmas digitales (tratadas en el Capítulo 31), que se basan en algoritmos numéricos y en la teoría de números. Las empresas manufactureras y otras empresas comerciales a menudo necesitan asignar recursos escasos de la manera más beneficiosa. Una compañía petrolera puede querer saber dónde colocar sus pozos para maximizar su beneficio esperado. Un candidato político puede querer determinar dónde gastar dinero comprando publicidad de campaña para maximizar las posibilidades de ganar unas elecciones. Una aerolínea puede querer asignar tripulaciones a los vuelos de la forma menos costosa posible, asegurándose de que cada vuelo está cubierto y de que se cumple la normativa gubernamental relativa a la programación de tripulaciones. Un proveedor de servicios de Internet puede querer determinar dónde colocar recursos adicionales para servir a sus clientes con mayor eficacia. Todos estos son ejemplos de problemas que pueden resolverse mediante programación lineal, que estudiaremos en el Capítulo 29. Aunque algunos de los detalles de estos ejemplos quedan fuera del alcance de este libro, sí que ofrecemos técnicas subyacentes que se aplican a estos problemas y áreas problemáticas. También mostramos cómo resolver muchos problemas específicos, entre los que se incluyen los siguientes: Se nos da un mapa de carreteras en el que está marcada la distancia entre cada par de intersecciones adyacentes, y deseamos determinar la ruta más corta de una intersección a otra. El número de rutas posibles puede ser enorme, incluso si rutas que se cruzan. ¿Cómo elegimos cuál de todas las rutas posibles es la más corta? En este caso, modelizamos el mapa de carreteras (que es a su vez un modelo de las carreteras reales) como un grafo (que conoceremos en la Parte VI y en el Apéndice B), y deseamos encontrar el camino más corto de un vértice a otro del grafo. Veremos cómo resolver eficientemente este problema en el Capítulo 24. Se nos dan dos secuencias ordenadas de símbolos, X D hx1; x2; : : : ; xmi e Y D hy1; y2; : : ; yni, y queremos encontrar la subsecuencia común más larga de X e Y . Una subsecuencia de X es simplemente X con algunos (o posiblemente todos o ninguno) de sus elementos eliminados. Por ejemplo, una subsecuencia de hA; B; C; D; E; F; Gi sería hB; C; E; Gi. La longitud de la subsecuencia común más larga de X e Y da una medida de la similitud entre estas dos secuencias. Por ejemplo, si las dos secuencias son pares de bases en cadenas de ADN, podríamos considerarlas similares si tienen una subsecuencia común larga. Si X tiene m símbolos e Y tiene n símbolos, entonces X e Y tienen 2m y 2n subsecuencias posibles, respectivamente. Seleccionar todas las subsecuencias posibles de X e Y y emparejarlas podría llevar un tiempo prohibitivo, a menos que m y n sean muy pequeños. En el capítulo 15 veremos cómo utilizar una técnica general conocida como programación dinámica para resolver este problema de forma mucho más eficiente. Se nos da un diseño mecánico en términos de una biblioteca de piezas, donde cada pieza puede incluir instancias de otras piezas, y necesitamos listar las piezas en orden de forma que cada pieza aparezca antes que cualquier pieza que la utilice. Si el diseño consta de n piezas, entonces hay nŠ órdenes posibles, donde nŠ denota la función factorial. Debido a que la función factorial crece más rápido que incluso una función exponencial, no podemos generar de forma factible cada orden posible y luego verificar que, dentro de ese orden, cada parte aparece antes de las partes que la utilizan (a menos que tengamos sólo unas pocas partes). Este problema es un caso de ordenación topológica, y veremos en el capítulo 22 cómo resolver este problema eficientemente. Damos n puntos en el plano y queremos hallar el casco convexo de estos puntos. El casco convexo es el menor polígono convexo que contiene los puntos. Intuitivamente, podemos pensar que cada punto está representado por un clavo que sobresale de una tabla. El casco convexo estaría representado por una banda elástica tensa que rodea todos los clavos. Cada clavo alrededor del cual gira la goma elástica es un vértice del casco convexo. (Véase la figura 33.6 en la página 1029 para ver un ejemplo). Cualquiera de los 2n subconjuntos de los puntos pueden ser los vértices del casco convexo. Saber qué puntos son vértices del casco convexo tampoco es suficiente, ya que también necesitamos saber el orden en el que aparecen. aparecen. Hay muchas opciones, por tanto, para los vértices del casco convexo. En el capítulo 33 se presentan dos buenos métodos para encontrar el casco convexo. Estas listas no son ni mucho menos exhaustivas (como probablemente habrás deducido por el volumen del libro), pero presentan dos características comunes a muchos problemas algorítmicos interesantes: 1. Tienen muchas soluciones candidatas, la inmensa mayoría de las cuales no resuelven el problema en cuestión. Encontrar una que lo haga, o una que sea “la mejor”, puede suponer todo un reto. 2. Tienen aplicaciones prácticas. De los problemas de la lista anterior, encontrar el camino más corto ofrece los ejemplos más sencillos. Una empresa de transportes, como una compañía de camiones o de ferrocarriles, tiene un interés económico en encontrar el camino más corto a través de una red de carreteras o de ferrocarriles, ya que al tomar caminos más cortos se reducen los costes de mano de obra y de combustible. O un nodo de encaminamiento en Internet puede necesitar encontrar el camino más corto a través de la red para encaminar un mensaje rápidamente. O puede que una persona que desee ir de Nueva York a Boston en coche quiera encontrar indicaciones de cómo llegar en un sitio web adecuado, o puede que utilice su GPS mientras conduce. No todos los problemas resueltos mediante algoritmos tienen un conjunto fácilmente identificable de soluciones candidatas. Por ejemplo, supongamos que tenemos un conjunto de valores numéricos que representan muestras de una señal y queremos calcular la transformada discreta de Fourier de esas muestras. La transformada discreta de Fourier convierte el dominio del tiempo en el dominio de la frecuencia, produciendo un conjunto de coeficientes numéricos que nos permiten determinar la intensidad de las distintas frecuencias de la señal muestreada. Además de estar en el corazón del procesamiento de señales, las transformadas discretas de Fourier tienen aplicaciones en la compresión de datos y en la multiplicación de grandes polinomios y números enteros. El capítulo 30 presenta un algoritmo eficiente, la transformada rápida de Fourier (comúnmente llamada FFT), para este problema, y el capítulo también esboza el diseño de un circuito de hardware para calcular la FFT. Estructuras de datos Este libro también contiene varias estructuras de datos. Una estructura de datos es una forma de almacenar y organizar datos para facilitar el acceso y la modificación. Ninguna estructura de datos única es adecuada para todos los propósitos y, por lo tanto, es importante conocer las fortalezas y limitaciones de algunas de ellas. Técnica Aunque puede usar este libro como un “libro de cocina” sobre algoritmos, algún día puede encontrar un problema para el cual no podrá encontrar fácilmente un algoritmo publicado (por ejemplo, muchos ejercicios y tareas en este libro). Este libro le enseñará cómo diseñar y analizar algoritmos para que pueda desarrollarlos usted mismo, demostrar que dan la respuesta correcta y comprender su efectividad. Diferentes capítulos cubren diferentes aspectos de la resolución algorítmica de problemas. Algunos capítulos tratan tareas específicas, como encontrar medianas y estadísticas de orden en el capítulo 9, calcular árboles de expansión mínimos en el capítulo 23 y determinar el flujo máximo en la red en el capítulo 26. Otros capítulos cubren técnicas como divide y vencerás en el capítulo 4, programación dinámica en el capítulo 15 y análisis amortizado en el capítulo 17. Problemas difíciles La mayor parte de este libro trata sobre algoritmos eficientes. Nuestra medida habitual de eficiencia es la velocidad, es decir, cuánto tiempo tarda un algoritmo en producir su resultado. Sin embargo, hay algunos problemas para los que no se conoce una solución eficiente. El capítulo 34 estudia un subconjunto interesante de estos problemas, que se conocen como NP-completos. ¿Por qué son interesantes los problemas NP-completos? Primero, aunque nunca se ha encontrado un algoritmo eficiente para un problema NP-completo, nadie ha demostrado que no pueda existir un algoritmo eficiente para uno. En otras palabras, nadie sabe si existen algoritmos eficientes para problemas NP-completos. En segundo lugar, el conjunto de problemas NP-completos tiene la notable propiedad de que si existe un algoritmo eficiente para cualquiera de ellos, entonces existen algoritmos eficientes para todos ellos. Esta relación entre los problemas NP-completos hace que la falta de soluciones eficientes sea aún más tentadora. En tercer lugar, varios problemas NP-completos son similares, pero no idénticos, a problemas para los que sí conocemos algoritmos eficientes. Los informáticos están intrigados por cómo un pequeño cambio en el enunciado del problema puede causar un gran cambio en la eficiencia del algoritmo más conocido. Debería conocer los problemas NP-completos porque algunos de ellos surgen sorprendentemente a menudo en aplicaciones reales. Si se le pide que produzca un algoritmo eficiente para un problema NP-completo, es probable que dedique mucho tiempo a una búsqueda infructuosa. Si puede demostrar que el problema es NP-completo, puede dedicar su tiempo a desarrollar un algoritmo eficiente que ofrezca una buena solución, pero no la mejor posible. Como ejemplo concreto, considere una empresa de reparto con un depósito central. Cada día, carga cada camión de reparto en el depósito y lo envía para entregar mercancías a varias direcciones. Al final del día, cada camión debe terminar de regreso en el depósito para que esté listo para ser cargado para el día siguiente. Para reducir costos, la compañía desea seleccionar un orden de paradas de entrega que arroje la distancia total más baja recorrida por cada camión. Este problema es el conocido "problema del viajante de comercio", y es NP-completo. No tiene un algoritmo eficiente conocido. Sin embargo, bajo ciertas suposiciones, conocemos algoritmos eficientes que dan una distancia general que no está muy por encima de la más pequeña posible. El capítulo 35 analiza tales " algoritmos de aproximación.” Paralelismo Durante muchos años, pudimos contar con que las velocidades de reloj del procesador aumentaran a un ritmo constante. Sin embargo, las limitaciones físicas presentan un obstáculo fundamental para velocidades de reloj cada vez mayores: debido a que la densidad de potencia aumenta superlinealmente con la velocidad del reloj, los chips corren el riesgo de derretirse una vez que sus velocidades de reloj se vuelven lo suficientemente altas. Por lo tanto, para realizar más cálculos por segundo, los chips se están diseñando para contener no solo uno, sino varios “núcleos de procesamiento”. Podemos comparar estas computadoras multinúcleo con varias computadoras secuenciales en un solo chip; en otras palabras, son un tipo de “computadora paralela”. Para obtener el mejor rendimiento de las computadoras multinúcleo, necesitamos diseñar algoritmos teniendo en cuenta el paralelismo. El capítulo 27 presenta un modelo para algoritmos “multiproceso”, que aprovechan múltiples núcleos. Este modelo tiene ventajas desde un punto de vista teórico y forma la base de varios programas informáticos exitosos, incluido un programa de ajedrez de campeonato. 1.2 Los algoritmos como tecnología Supongamos que las computadoras fueran infinitamente rápidas y la memoria de la computadora estuviera libre. ¿Tendría alguna razón para estudiar algoritmos? La respuesta es sí, si no es por otra razón que por la que aún desea demostrar que su método de solución termina y lo hace con la respuesta correcta. Si las computadoras fueran infinitamente rápidas, cualquier método correcto para resolver un problema sería suficiente. Probablemente desee que su implementación se encuentre dentro de los límites de las buenas prácticas de ingeniería de software (por ejemplo, su implementación debe estar bien diseñada y documentada), pero la mayoría de las veces usaría el método que fuera más fácil de implementar. Por supuesto, las computadoras pueden ser rápidas, pero no son infinitamente rápidas. Y la memoria puede ser barata, pero no es gratuita. Por lo tanto, el tiempo de computación es un recurso limitado, al igual que el espacio en la memoria. Debe utilizar estos recursos de manera inteligente, y los algoritmos que sean eficientes en términos de tiempo o espacio lo ayudarán a hacerlo. Eficiencia Los diferentes algoritmos ideados para resolver el mismo problema a menudo difieren drásticamente en su eficiencia. Estas diferencias pueden ser mucho más significativas que las diferencias debidas al hardware y al software. Como ejemplo, en el capítulo 2, veremos dos algoritmos para ordenar. El primero, conocido como ordenación por inserción, lleva un tiempo aproximadamente igual a c1n2 para ordenar n elementos, donde c1 es una constante que no depende de n. Es decir, lleva un tiempo aproximadamente proporcional a n2. El segundo, el ordenamiento por fusión, lleva un tiempo aproximadamente igual a c2n lg n, donde lg n representa log2 n y c2 es otra constante que tampoco depende de n. El ordenamiento por inserción generalmente tiene un factor constante menor que el ordenamiento por fusión, de modo que c1 < c2. Veremos que los factores constantes pueden tener un impacto mucho menor en el tiempo de ejecución que la dependencia del tamaño de entrada n. Escribamos el tiempo de ejecución de la ordenación por inserción como c1n n y el tiempo de ejecución de la ordenación por fusión como c2n lg n. Luego vemos que donde la ordenación por inserción tiene un factor de n en su tiempo de ejecución, la ordenación por fusión tiene un factor de lg n, que es mucho menor. (Por ejemplo, cuando n D 1000, lg n es aproximadamente 10, y cuando n es igual a un millón, lg n es aproximadamente solo 20.) Aunque la ordenación por inserción generalmente se ejecuta más rápido que la ordenación por fusión para tamaños de entrada pequeños, una vez que el tamaño de entrada n sea lo suficientemente grande, la ventaja de la ordenación por fusión de lg n frente a n compensará con creces la diferencia en factores constantes. No importa cuánto más pequeño sea c1 que c2, siempre habrá un punto de cruce más allá del cual la ordenación por fusión sea más rápida. Para un ejemplo concreto, enfrentemos una computadora más rápida (computadora A) que ejecuta ordenación por inserción contra una computadora más lenta (computadora B) que ejecuta ordenación por fusión. Cada uno de ellos debe ordenar una matriz de 10 millones de números. (Aunque 10 millones de números pueden parecer mucho, si los números son enteros de ocho bytes, entonces la entrada ocupa aproximadamente 80 megabytes, lo que cabe en la memoria incluso de una computadora portátil económica muchas veces.) Supongamos que la computadora A ejecuta 10 mil millones de instrucciones por segundo (más rápido que cualquier computadora secuencial individual en el momento de escribir este artículo) y la computadora B ejecuta solo 10 millones de instrucciones por segundo, de modo que la computadora A es 1000 veces más rápida que la computadora B en potencia de cálculo bruta. Para hacer la diferencia aún más dramática, supongamos que el programador más hábil del mundo ordena la inserción de códigos en lenguaje de máquina para la computadora A, y el código resultante requiere instrucciones 2n2 para ordenar n números. Supongamos además que solo un programador promedio implementa la ordenación por fusión, utilizando un lenguaje de alto nivel con un compilador ineficiente, y el código resultante toma 50n instrucciones lg n. Para ordenar 10 millones de números, la computadora A toma 2 .107/2 instrucciones 1010 instrucciones / segundo D 20.000 segundos (más de 5,5 horas); mientras que la computadora B tarda 50 107 lg 107 instrucciones 107 instrucciones / segundo 1163 segundos (menos de 20 minutos): ¡Al usar un algoritmo cuyo tiempo de ejecución crece más lentamente, incluso con un compilador deficiente, la computadora B se ejecuta más de 17 veces más rápido que la computadora A! La ventaja de la ordenación combinada es aún más pronunciada cuando clasificamos 100 millones de números: donde la ordenación por inserción tarda más de 23 días, la ordenación combinada tarda menos de cuatro horas. En general, a medida que aumenta el tamaño del problema, también lo hace la ventaja relativa del ordenamiento combinado. 1.3 Algoritmos y otras tecnologías El ejemplo anterior muestra que deberíamos considerar los algoritmos, como el hardware de la computadora, como una tecnología. El rendimiento total del sistema depende tanto de la elección de algoritmos eficientes como de la elección de hardware rápido. Así como se están logrando rápidos avances en otras tecnologías informáticas, también se están logrando en algoritmos. Quizás se pregunte si los algoritmos son realmente tan importantes en las computadoras contemporáneas a la luz de otras tecnologías avanzadas, como arquitecturas informáticas avanzadas y tecnologías de fabricación, interfaces gráficas de usuario (GUI) intuitivas y fáciles de usar, sistemas orientados a objetos, tecnologías web integradas y redes rápidas, tanto cableadas como inalámbricas. La respuesta es sí. Aunque algunas aplicaciones no requieren explícitamente contenido algorítmico a nivel de aplicación (como algunas aplicaciones simples basadas en la Web), muchas sí lo requieren. Por ejemplo, considere un servicio basado en la web que determina cómo viajar de una ubicación a otra. Su implementación se basaría en hardware rápido, una interfaz gráfica de usuario, redes de área amplia y posiblemente también en la orientación a objetos. Sin embargo, también requeriría algoritmos para ciertas operaciones, como encontrar rutas (probablemente usando un algoritmo de ruta más corta), renderizar mapas e interpolar direcciones. Además, incluso una aplicación que no requiere contenido algorítmico a nivel de aplicación depende en gran medida de los algoritmos. ¿La aplicación se basa en hardware rápido? El diseño del hardware utilizó algoritmos. ¿La aplicación se basa en interfaces gráficas de usuario? El diseño de cualquier GUI se basa en algoritmos. ¿La aplicación se basa en la creación de redes? El enrutamiento en las redes se basa en gran medida en algoritmos. ¿La aplicación estaba escrita en un lenguaje distinto al código máquina? Luego fue procesado por un compilador, intérprete o ensamblador, todos los cuales hacen un uso extensivo 14 Capítulo 1 El papel de los Algoritmos en la Computación de algoritmos. Los algoritmos son el núcleo de la mayoría de las tecnologías utilizadas en las computadoras contemporáneas. Además, con las capacidades cada vez mayores de las computadoras, las usamos para resolver problemas más grandes que nunca. Como vimos en la comparación anterior entre ordenación por inserción y ordenación por fusión, es en problemas de mayor tamaño donde las diferencias en eficiencia entre algoritmos se vuelven particularmente prominentes. Tener una base sólida de conocimientos y técnicas algorítmicas es una característica que separa a los programadores verdaderamente hábiles de los novatos. Con la tecnología informática moderna, puede realizar algunas tareas sin saber mucho sobre algoritmos, pero con una buena formación en algoritmos, puede hacer mucho, mucho más.