Complejidad computacional Hoy vamos a entender el campo de la Complejidad Computacional. Normalmente, en el campo de la Computación nos dedicamos básicamente a hablar de problemas y algoritmos que se resuelven en tiempo polinomial. Hoy vamos a hablar de cuando no se puede hacer esto. Algunas veces podemos demostrar que no podemos resolver un problema en tiempo polinomial. Algunas veces, estamos bastante seguros de que no podemos hacerlo (pero no podemos demostrarlo). Para empezar, entendamos los que hay detrás de 3 clases de “complejidad” (hablamos de Complejidad Computacional): P, EXP y R. ¿Qué es P? Pues P es el conjunto (una clase de complejidad) que incluye a todos aquellos problemas que podemos resolver en tiempo polinomial. A estos nos dedicamos en el campo de la Informática: resolver problemas utilizando algoritmos; pero solo podemos resolver aquellos que consumen un tiempo polinomial, un tiempo aceptable. P. Spoiler: ¿Alguien conoce la excepción? Seria NO P. Lo que no es P. NP. ¿Qué sigue a P? EXP. Exponencial. Es el conjunto (una clase) que incluye a todos aquellos problemas que podemos resolver en tiempo exponencial. Podemos decir que es 2n.Si queremos generalizar, podemos decir mn. Elementalmente, un exponencial es una generalización de un polinomial, por lo que la clase EXP incluye a la clase P. Tiempo exponencial es mucho tiempo… Podemos pintar estas clases en un eje, que llamaremos dificultad computacional. Este dibujo no es un verdadero diagrama, pero es una buena guía para visualizar lo que nos pasa con la complejidad de los problemas. Sigamos por este eje; tenemos a continuación a R. ¿Qué es R?. R es la clase de problemas que podemos resolver en tiempo finito. R es Recursivo. El tiempo finito es un requisito razonable para todos los algoritmos. Pero la línea de dificultad computacional continua… es algo desalentador, pero tenemos que decirlo: hay problemas que no tienen solución (en un tiempo finito). Vamos a demostrarlo, pero antes vamos a ver algunos ejemplos: - Un ajedrez nxn. Te doy un tablero de tamaño n y un montón de piezas de ajedrez para ese tablero y te pregunto desde una posición concreta, ¿ganan las blancas? ¿o ganan las negras? Este problema se puede resolver en tiempo exponencial; es suficiente con jugar todas las posibles partidas y ver quien gana. No está en P (no se puede resolver en tiempo polinomial). La complejidad computacional tiene que ver con el orden de crecimiento del problema. - Go también está en EXP pero no está en P. Y así muchos juegos, muchos juegos complejos. - Tetris, pero un Tetris especial. Te digo la ficha que va a venir a continuación (si no te la digo, estamos poniendo un nivel de aleatoriedad que no nos ayuda a resolver el problema; no nos ayuda a definir una estrategia). Quiero saber si puedo sobrevivir en una posición de tablero inicial dado y para una secuencia determinada de piezas. Esto también se puede resolver en tiempo exponencial. Basta con probar todas las posibilidades. En este caso no sabemos si está en P; estamos bastante seguros de que no está en P. Y, espero, que al final de esta charla se entenderá por qué creemos que no está en P. A Tetris le ponemos por aquí en medio. Pero no hemos definido lo que es aquí en medio (todavía…). - Por último, un problema un poco “especial”: El problema de detener un programa informático. Nos da lo mismo el lenguaje de programación (son todos iguales en un sentido teórico). Escribes un código y lo has ejecutado durante horas y no sabes si, porque hay un error o porque tienes un bucle infinito o porque es realmente ¿lento?, se va a detener en algún momento; no estaría mal dárselo a un programa de verificación y que nos diga si se ejecutará para siempre o terminará en algún momento. Este problema no está en R; no existe un algoritmo correcto para resolver este problema en tiempo finito; no hay forma de saber, dado un programa arbitrario cualesquiera si este se detendrá o se ejecutará indefinidamente. Claro que si, por ejemplo, tengo un programa vacío, puedo decir que se detiene. O tengo un subconjunto de programas llamémosles “simples” puedo decir que se detienen o no. Pero no existe un algoritmo que resuelva esta duda para todos los programas en un tiempo finito. Claro que lo puedo resolver en un tiempo infinito para cualquier programa: simplemente le dejo ejecutarse… Bien, vistos estos ejemplos, vamos a demostrar que casi todos los problemas (interesantes) no están en P. Para ello necesitamos entender un concepto: problemas de decisión. Un problema de decisión podemos entenderlo como aquel cuya respuesta es binaria: sí o no. ¿Ganan las blancas desde esta posición? Sí o no. ¿podrás sobrevivir en Tetris? Sí o no. Y, claro, ¿se detiene el programa? Por varias razones, básicamente por conveniencia, todo el campo de la complejidad computacional se centra en los problemas de decisión. Y, de hecho, los problemas de decisión son aquellos en los que la respuesta es sí o no. Eso es todo. ¿Por qué? Esencialmente, si tomamos cualquier problema que nos importe, podemos convertirlo en un problema de decisión. Los problemas de decisión son básicamente tan difíciles como cualquier problema de optimización. Centrémonos en los problemas de decisión. La respuesta es sí o no. Vamos a afirmar que la mayoría de ellos son indiscutibles (no sabemos decir sí o no). Y podemos probar esto con bastante facilidad si conocemos un poco de teoría de conjuntos. Por un lado, tengo problemas que quiero resolver. Estos son los problemas de decisión (ya hemos dicho que todo problema lo podemos convertir, de alguna forma, en un problema de decisión). Por otro lado, tengo algoritmos o programas de ordenador para resolver estos problemas. Vamos a hablar, en vez de algoritmo, de programa de ordenador porque con algoritmo igual nos cuesta un poco más pensar en pseudocódigo: lo que es válido / lo que no es válido. Los programas de ordenador son muy claros. Te doy un código. Lo codificas en, por ejemplo, Phyton. O funciona o no. Y hace algo. Se ejecuta durante un tiempo. ¿Cómo puedo pensar en el espacio de todos los programas posibles? Bueno, los programas son, al fin y al cabo, cosas que escribo en un ordenador en ASCII. Al final, puedes pensar en ellos como una cadena binaria. De alguna manera podemos asumir que se codifica en binario. Todo se reduce a binario en un ordenador. Entonces tenemos una cadena binaria. Ahora, también puede pensar en una cadena binaria que representa un número (en binario). Entonces, también puede pensar en un programa como un número natural; solo 0, 1, 2, 3. Puede pensar que cada programa es, en última instancia, un número entero. Es un gran número entero, pero bueno. Es un entero. Entonces ese es el espacio de todos los programas. ℕ Ahora, quiero pensar en el espacio de todos los problemas de decisión. Entonces, ¿cómo puedo definir un problema de decisión? Bueno, la forma natural de pensar en un problema de decisión es como una función que asigna entradas a sí o no. Función de entradas a sí o no (a 1 y 0). Entonces, ¿qué es una entrada? Bueno, una entrada es una cadena binaria. Entonces, una entrada es un número: un número natural. La entrada es una cadena binaria, que podemos pensar que está en ℕ. Es decir, tenemos una función de ℕ a 0,1. Entonces, otra forma de representar una de estas funciones es como una tabla. Podría simplemente escribir todas las respuestas (como hemos hecho en programación dinámica). Así que tengo que la entrada podría ser 0 y la salida el número 0. La entrada podría ser 1 y, tal vez, la salida sea 0. Entonces, la entrada podría ser 2, 3, 4, 5, 1, 0, 1, 1, etc. Tengo que poder escribir la tabla de todas las respuestas. Esta es otra forma de ver esta función. Lo que tenemos aquí es una cadena infinita de bits. Cada uno de ellos puede dar como resultado un 0 o 1. Cualquier cadena infinita de bits representa un problema de decisión. Son lo mismo. Entonces, un problema de decisión es una cadena infinita de bits. Pero un programa es una cadena finita de bits. Son cosas diferentes. Vamos a poner un punto decimal en esta idea. Una forma de ver que son diferentes es poner un punto decimal en esta cadena infinita de bits. Ahora, esta cadena infinita es un número real entre 0 y 1. El número real está escrito en binario, pero es un número real con un punto binario. Entonces… cualquier número real puede expresarse mediante una cadena infinita de bits, cualquier numero real entre 0 y 1. Entonces un problema de decisión es algo que esta en R, el conjunto de todos los números reales, mientras que un programa de ordenador es algo que esta en N, el conjunto de los números naturales. Y la cosa es que el número de números reales es mucho, mucho mayor que el número de enteros. En un sentido formal, llamamos a R incontablemente infinito, y a N contablemente infinito. Esto no lo vamos a probar, pero es una demostración matemática muy estética. https://www.youtube.com/watch?v=TUFQKN5mIWM Y esto es una mala noticia: significa que hay muchos más problemas de los que hay programas de ordenador para resolverlos. Significa que casi todos los problemas que podemos concebir son irresolubles por un programa de ordenador. Y esto es bastante deprimente. Y algo raro. Porque la mayoría de los problemas que pensamos son computables. Eso es todo lo que voy a decir sobre R. Puedes pensar en esta línea como una línea infinita y solo hay esta pequeña porción que son cosas que puedes resolver Ahora vamos a hablar sobre esta nueva muesca, que es NP. Al igual que P, EXP y R, NP es un conjunto de problemas de decisión; pero no, NP no significa No Polinomial… Significa Polinomio No Determinista. Veremos que es No Determinista. La primera idea es que incluye a todos los problemas de decisión que se pueden resolver en tiempo polinomial (esto nos suena a P); pero ahora viene la mala noticia: se pueden resolver a través de un algoritmo de “suerte” o afortunado (que hace conjeturas). Un algoritmo afortunado solo necesita probar una posibilidad porque es “afortunado”; siempre adivina la elección correcta. Esto no es un modelo de cálculo realista, pero es un modelo computacional llamado no determinista. En un modelo de computación no determinista, un algoritmo puede calcular cosas, pero realmente hace suposiciones. Hablemos de Tetris; esta en NP. Sabemos como solucionarlo en tiempo exponencial; pruebo todas las opciones y ya está. Pero, de hecho, no necesito probar todas las opciones; basta con usar la magia “no determinista”. Podría decir ¿Debo dejar la pieza aquí, aquí, aquí, aquí, aquí o aquí? Y debería ser rotado así, o así, o así, ¿o así? No lo sé. Así que supongo. Y solo coloca esa pieza. Adivino de nuevo dónde colocar la siguiente pieza. Cualquier elección que tengas que hacer para jugar al Tetris, puedes adivinarla. Solo hay polinomialmente muchas conjeturas que necesitas hacer. Entonces todavía es tiempo polinomial. Eso es importante. No es que no podamos hacer nada. Podemos hacer en un número polinomial estas conjeturas mágicas. Y luego, al final, determinar si morimos, o mejor dicho, si sobrevivimos. Otra forma de pensar en NP. Y puede que encontremos esto más intuitivo porque hemos estado haciendo muchas conjeturas. Es un poco loco. Hay otra forma que es más intuitiva para muchas personas. Otra forma de pensar en NP, que resulta ser equivalente, es que no pienses tanto sobre algoritmos para resolver un problema, sino que pienses en algoritmos para comprobar la solución a un problema. Por lo general, es mucho más fácil verificar un trabajo que resolver un problema. Y NP tiene que ver con ese tema. Así que piensa en problemas de decisión y piensa si tienen una solución. Si lo aplicamos a Tetris, diremos que Tetris tiene solución. Y puedes probarlo en tiempo polinomial. Es decir, NP es el conjunto de problemas que solo hemos encontrado la forma de resolverlos en tiempo exponencial, pero que podemos verificar que una solución es una solución (conseguida de forma “mágica”) en tiempo polinomial. Y nos aparece la pregunta del millón (de dólares): ¿Es P igual a NP? La mayoría de la gente piensa que P no es igual a NP. Cada año hay gente que intenta demostrar que no es igual (o que es igual). Piensa en lo que esto realmente significa: P son los problemas que podemos resolver en un ordenador y NP son los problemas que podemos resolver “de forma mágica” en un ordenador… Otra forma de verlo es que resolver problemas es más difícil que verificar las soluciones dadas para un problema. Es más difícil probar un teorema que comprobar la prueba de un teorema. Todos sabemos verificar la prueba de un teorema: lo escribes con precisión y te aseguras de que cada paso sigue a los anteriores. Hecho. Pero demostrar un teorema es difícil; necesita inspiración; necesitas una idea inteligente. Necesitas adivinar. Afortunadamente, este no es final de la historia; podemos decir mas cosas sobre Tetris. Sabemos que Tetris esta en NP menos P. Y sabemos que hay muchos problemas en ese mismo sitio. Realmente, a este punto le llamamos NP-completo; y ¿Qué significa NPcompleto? Significa que es tan difícil como cada problema en NP. Por eso lo ponemos en el extremo de NP. Y podemos seguir poniendo nombres: NP-hard. Decimos que un problema en NP-hard cuando es al menos tan difícil como cualquier problema en NP. Y podemos seguir poniendo nombres: EXP-completo. Y tenemos en EXPcompleto: el ajedrez. Es el problema EXP más difícil. Y tambien tenemos EXP-hard…. Ahora nos aparecen nuevas preguntas, aunque no tan famosas: ¿Es NP igual a EXP? Vamos a introducir un nuevo concepto: las reducciones. Si tienes un problema A que deseas resolver (pero no sabes) y lo puedes convertir (y lo conviertes) en un problema B que ya sabes resolver. Voilá… ¿Cómo resuelvo el problema de encontrar el camino mas corto no ponderado? Te doy un grafo sin pesos en los bordes y quiero el camino más corto. Ahora te digo Djikstra. Pero no puedo aplicarlo porque no tengo pesos… Bueno, hago una reducción: aplico un peso de 1 a cada nodo. Y ya está. Las reducciones nos ayudan mucho porque no queremos implementar un algoritmo para cada problema que tengamos; es bueno poder reutilizar algunos de los algoritmos que ya tenemos. Pues las reducciones nos permiten hacer esto. Bien, pues utilicemos las reducciones. Voy a asegurar que el problema Partición de 3 se puede reducir a Tetris. Contemos que es el problema Partición de 3: te doy n números y quiero saber si se pueden dividirlos en tres grupos, cada uno de la misma suma. Suena fácil, pero es un problema NP-completo. Necesito concentrarme para demostrar que Tetris es más difícil que Partición de 3. Más difícil significa, cuando puedo reducir un problema A a un problema B, que B es al menos tan difícil como A. Esto es porque puedo resolver A resolviendo B. Si tengo una buena manera de resolver B entonces se convertiría en una buena manera de resolver A. Realmente, todos los problemas NP-completos se pueden reducir entre sí. Es decir, las reducciones son muy útiles para obtener resultados positivos en la creación de nuevos algoritmos, pero también para demostrar resultados negativos mostrando que un problema es más difícil que otro. Veamos, como punto final más problemas conocidos. El problema de viajante: dado un grafo quiero encontrar el camino más corto que recorre todos los vértices. Este problema es NP-completo. Si te doy n-cadenas en las que se necesita encontrar la subsecuencia común más larga, es NP-completo. El buscaminas, el sudoku, la mayoría de los rompecabezas interesantes son NP-completos.