A ver... Como no estoy seguro de si todo el mundo se está haciendo a la idea de qué hablamos cuando nos referimos a "tiempo polinómico", voy a poner un ejemplo bastante típico: ordenación de una serie de números (ordenación y búsqueda son arquetípicos y nos machacan con eso parte de la carrera).
[LADRILLO VA]
Imaginemos que tenemos una serie de números (10, 20, 100, 1000, los que sean), que nos los dan desordenados (supuestamente) y que los queremos poner por orden. Uno de los algoritmos más sencillos para un caso como éste es la "ordenación de burbuja" (
bubble sort), que se llama así porque los números más pequeños van "subiendo" y los más grandes "bajando" en la lista de números que tenemos, según vamos haciendo pasadas por ella.
Primero va una descripción en "pseudocódigo" del algoritmo (luego la explico):
Código:
ordenar_burbuja ( lista )
hacer
ordenado = verdadero
para x = 1 hasta ( tamaño(lista) - 1 ) hacer
si lista[x] > lista[x+1] entonces
intercambiar lista[x] y lista[x-1]
ordenado = falso
fin si
fin para
hasta que esté ordenado
fin ordenar_burbuja
El ejemplo es bastante sencillo de entender. Tenemos un procedimiento al que llamaremos
ordenar_burbuja al que le pasamos una
lista. Esta lista tiene
n elementos numerados del 1 al
n[/b], y para acceder a cada uno de ellos individualmente usaremos la notación [i]lista[k], siendo
k el índice de ese elemento dentro de la lista. La estructura
para ... hasta ... hacer es un tipo de lo que llamamos "bucle", es decir, una estructura repetitiva. En particular, lo que hace es repetir el trozo de código que contiene, asignando valores desde 1 hasta
tamaño(lista)-1 (es decir, hasta
n - 1) a una variable que vamos a llamar
x. Estos valores los asignará en orden creciente (1, 2, 3, etc). Además, usaremos una señal,
ordenado, para saber si la lista está ordenada y podemos dejar de hacer repeticiones.
La idea entonces es simple. Asumimos de entrada que la lista está ordenada (
ordenado = verdadero). Pasamos entonces a ir comparando cada elemento con el que le sigue, usando el bucle para eso. Si por casualidad encontramos en el recorrido que al menos dos de ellos están al revés (el mayor está antes que el menor), los intercambiamos y marcamos
ordenado = falso, de manera que cuando acabemos de hacer esta pasada de comprobaciones, empecemos de nuevo, para asegurarnos que esta vez *sí* está todo bien ordenado.
Ejemplo del aspecto que tendría una de estas listas según avanza el algoritmo al principio de cada uno de los ciclos:
Código:
(3, 2, 1, 5, 4)
(2, 1, 3, 4, 5)
(1, 2, 3, 4, 5)
Como se ve, los números más pequeños van "gravitando" hacia el principio de la lista, y los más grandes hacia el final.
Ahora, lo interesante: ¿cómo predecimos el tiempo que va a costarnos hacer todo esto? Vamos a asumir que cada operación (una asignación, una comparación, el intercambio de dos valores) nos va a costar una "unidad de tiempo" (que puede ser 1 segundo, 1 millonésima de segundo, semana y media, da igual...), y ahora pasamos a contar el número de operaciones que hacemos

. Esto supone un problema, porque a priori no sabemos cuántas veces se va a repetir el bucle (ya que depende de lo desordenada que esté la lista inicialmente), o cuántas veces se va a repetir el intercambio de valores (ya que sólo sucede si dos valores comparados están desordenados, claro). Es por esto que normalmente los valores que nos interesan son el "mejor caso" y (especialmente) el "peor caso".
Empecemos analizando el interior del bucle
para. Vemos que hay cuatro instrucciones potenciales: asignar un valor a x, la comparación, el intercambio de valores, y poner
ordenado a
falso. Es decir, por cada pasada del bucle ejecutaremos de 2 a 4 instrucciones (dependiendo de si la condición de la comparación se cumple o no). Vamos a tomar el peor valor (4).
Segundo, hemos determinado que el bucle
para se va a ejecutar siempre "n-1" veces, así que el bucle va a tardar siempre entre 2*(n-1) y 4*(n-1) unidades de tiempo en ejecutarse.
Por último, nos queda saber cuántas veces va a ejecutarse ese bucle. Eso, como decíamos, depende de lo "bien" o "mal" ordenado que esté la lista inicial. El mejor caso sería claramente que nos pasasen la lista ya totalmente ordenada, ya que pasaríamos una vez por ella, comprobándolo todo, y al llegar al final veríamos que nada ha cambiado y que podríamos salir sin más, todo ello en 2(n-1) unidades de tiempo. El peor caso sería que nos hayan dado una lista en que no se repita ningún número y el más pequeño de todos ellos estuviera al final del todo, independientemente de lo bien o mal ordenado que esté el resto de la lista. ¿Por qué? Dado que cada elemento sólo se mueve un máximo de un puesto por cada ciclo del bucle, si el más pequeño está al final, habrá que ejecutar el bucle n-1 veces para moverlo hasta donde le corresponde (el primero de la lista), con lo cual habrá que multiplicar lo que tenemos hasta ahora por (n-1), de nuevo.
Resumiendo: nuestro algoritmo se ejecutará entre 2*(n-1) (mejor caso) y 4*(n-1)^2 (peor caso) veces. Dado que el tiempo para el mejor caso depende de un número relacionado con el número de elementos de la lista, decimos que la
cota inferior tiene
orden lineal. El peor caso depende de un número relacionado con el cuadrado de elementos que componen la lista, así decimos que la
cota superior es de
orden cuadrático. En notación algorítmica escribiríamos esto como O(n^2), despreciando las constantes multiplicativas asociadas.
Este análisis temporal es importante porque nos permite comparar soluciones a un problema sin necesidad de hacer pruebas exhaustivas, que aparte de costosas (en tiempo) podrían dar lugar a resultados engañosos. Por ejemplo, un algoritmo podría ser mejor que otro hasta un "n" determinado, pero ser peor de ahí en adelante. Si nos aburrimos de comparar en "n-1" y luego aplicamos el primer algoritmo para "n^2" elementos, la hemos jodido
En cualquier caso, lo importante aquí es que hemos obtenido un polinomio que nos dice cuánto tiempo va a tardar nuestro programa en resolver el problema.
[/LADRILLO]
Es más, dado que nuestro programa puede implementarse en una máquina determinista (nuestro ordenador), entonces cae dentro del conjunto P. Si hubiéramos tenido que usar una solución
no determinista para nuestro problema, pero una con tiempo polinómico de resolución, entonces hablaríamos de un problema que cae fuera de P (pero dentro de NP).
Edit: joder con el cortar+pegar