Transcript ALGORITMO

Algoritmos y Estructuras de Datos III
2do cuatrimestre 2010
Paula Zabala
Irene Loiseau
Observaciones y advertencias:
• Estas transparencias pueden cambiar un poco antes o
después de la clase correspondiente a cada tema.
• Las presentaciones NO INCLUYEN las demostraciones
que TAMBIEN forman parte del curso y que son dadas en
clase. En la mayoría de los casos se incluyen sólo los
enunciados de los teoremas. (o sea esto NO es un apunte de
la materia!!).
• También puede haber algún ejemplo o resultado que se de
en clase y no esté las transparencias.
• Parte de los temas del práctico 7 (PERT- CPM) no están
incluidos en ninguna de las presentaciones.
PROGRAMA
1. ALGORITMOS:
Definición de algoritmo. Modelos de computación: modelo RAM, Máquina de Turing.
Complejidad, definición, complejidad en el peor caso, en el caso promedio. Algoritmos
de tiempo polinomial y no polinomial. Límite inferior. Ejemplo: análisis de algoritmos de
ordenamiento. Algoritmos recursivos. Análisis de la complejidad de algoritmos
recursivos.
Técnicas de diseño de algoritmos: dividir y conquistar, backtracking, algoritmos golosos,
programación dinámica.
Algoritmos probabilísticos. Algoritmos aproximados Algoritmos heurísticos: ejemplos.
Metaheurísticas. Nociones de evaluación de heurísticas.
2. GRAFOS:
Definiciones básicas. Adyacencia, grado de un nodo, isomorfismos, caminos, conexión, etc.
Grafos eulerianos y hamiltonianos. Grafos bipartitos. Arboles: caracterización, árboles
orientados, árbol generador. Enumeración. Planaridad. Coloreo. Número cromático.
Matching, conjunto independiente, recubrimiento. Recubrimiento de aristas y vértices.
3. ALGORITMOS EN GRAFOS Y APLICACIONES:
Representación de un grafo en la computadora: matrices de incidencia y adyacencia, listas.
Algoritmos de búsqueda en grafos: BFS, DFS, A*. Mínimo árbol generador, algoritmos de
Prim y Kruskal. Arboles ordenados: códigos unívocamente descifrables. Algoritmos para
detección de circuitos. Algoritmos para encontrar el camino mínimo en un grafo:
Dijkstra, Ford, Floyd, Dantzig. Planificación de procesos: PERT/CPM.
. Heurísticas para el problema del viajante de comercio. Algoritmos para determinar si un
grafo es planar. Algoritmos para coloreo de grafos.
Algoritmos para encontrar el flujo máximo en una red: Ford y Fulkerson. Matching:
algoritmos para correspondencias máximas en grafos bipartitos. Otras aplicaciones.
4. PROBLEMAS NP-COMPLETOS:
Problemas tratables e intratables. Problemas de decisión. P y NP. Maquinas de Turing no
determinísticas. Problemas NP-completos. Relación entre P y NP. Problemas de grafos
NP-completos: coloreo de grafos, grafos hamiltonianos, recubrimiento mínimo de las
aristas, corte máximo, etc.
BIBLIOGRAFIA
BIBLIOGRAFÍA BÁSICA :
•
•
•
•
•
Brassard,G., Bratley,P.”Fundamental of Algorithmics”, Prentice Hall,1996.
Cormen, T.,Leiserson, C.,Rivest,R.,Stein, C.,”Introduction to Algorithms”, The
MIT Press, McGraw-Hill,2001.
Garey M.R. and Johnson D.S.: “Computers and intractability: a guide to the
theory of NP- Completeness”, W. Freeman and Co., 1979.
Gross,J., and Yellen, J. ,”Graph theory and its applications”, CRC, 1999
Harary F., “Graph theory”, Addison-Wesley, 1969.
BIBLIOGRAFÍA DE CONSULTA : ver en la página WEB de la materia y en el
el archivo de las prácticas.
ALGORITMOS
• Qué es un algoritmo ?
• Qué es un buen algoritmo?
• Dados dos algoritmos para resolver un mismo problema,
cuál es mejor?
• Cuándo un problema está bien resuelto?
• Cómo se hace para inventar un nuevo algoritmo?
Qué es un algoritmo?
Noción “informal”:
Un algoritmo es una sucesión finita de instrucciones “bien definidas” tal
que:
i) No hay ambigüedad en las instrucciones.
ii) Después de ejecutar una instrucción no hay ambigüedad respecto de
cual es la instrucción que debe ejecutarse a continuación.
iii) Después de un número finito de instrucciones ejecutadas se llega
siempre a la instrucción STOP (“Un algoritmo siempre para”).
PROBLEMA
• instancia de un problema
• datos de entrada de una instancia (E)
• solución (S)
ALGORITMO:
• técnica para la resolución de un problema
• función f tal que f (E) = S
Problema de Collatz
Paso 1: z número entero positivo
Paso 2: mientras z  1 hacer
Paso 3: Si z es par poner z = : z / 2
En caso contrario poner z =: 3 z +1
Paso 4: parar
=====================================
es esto un algoritmo?
• Cuánto tarda un algoritmo en resolver un
problema?
• Cuándo un algoritmo que resuelve un problema es
mejor que otro que resuelve el mismo problema?
Complejidad Computacional
Complejidad
La complejidad de un algoritmo es una función que
calcula el tiempo de ejecución en función del
tamaño de la entrada de un problema.
• Historia
• Complejidad en el peor caso (es siempre significativo el
peor caso?)
• Complejidad en el caso promedio (porqué no usamos este
enfoque siempre?)
Cómo medir el tiempo de ejecución en función del
tamaño de la entrada de los datos del problema?
MODELOS DE COMPUTACION
• RAM
• MAQUINA DE TURING
RANDOM ACCESS MACHINE
Es el modelo que usamos implícitamente en la práctica para
evaluar la complejidad de un algoritmo.
==================================================
• unidad de memoria
• unidad de entrada
• procesador (programa)
• unidad de salida
• conjunto de instrucciones
====================================================
Un programa RAM es una secuencia finita de estas instrucciones
Esquema de una máquina RAM
memoria
entrada
procesador
Salida
En este modelo suponemos que:
• La unidad de entrada es una cinta de cuadrados en cada uno de los
cuales se puede almacenar un entero.
• La memoria es una suceción de registros, cada uno de los cuales puede
almacenar un entero de tamaño arbitrario. Los cálculos se hacen en el
primero de ellos, r0.
• El programa no se almacena en en la memoria y no se modifica a si
mismo.
• El conjunto de instrucciones es similar al de las computadoras reales.
Cada instrucción tiene un nombre y una dirección.
• Este modelo sirve para modelar situaciónes donde tenemos memoria
suficiente para almacenar el problema y donde los enteros que se usan
en los cálculos entran en una palabra.
Ejemplo
( sacado del libro de Aho, Hopcroft y Ullmann)
Ejemplo de tabla de instrucciones de una máquina RAM (código y dirección)
LOAD
STORE
ADD
SUB
MULT
DIV
READ
WRITE
JUMP
JGTZ
JZERO
HALT
operando
operando
operando
operando
operando
operando
operando
operando
label
label
label
Los operandos pueden ser:
• =i
• Un entero nonegativo indicando el contenido del registro i
• Un puntero *i
Ejemplo de un programa para calcula nn en esta máquina RAM
=====================
READ
1
LOAD
1
JGTZ
pos
WRITE = 0
JUMP endif
pos
LOAD
1
STORE
2
LOAD
1
SUB
=1
STORE
3
while
LOAD
3
JGTZ
continue
JUMP
endwhile
continue LOAD
2
MULT 1
STORE 2
LOAD
3
SUB
=1
STORE
3
JUMP
while
endwhile WRITE
2
endif
HALT
leer r1
si r1 ≤ 0 entonces write 0
“
“
sino r2 -- r1
“
r3 -- r1 -1 .
“
“
mientras r3 > 0 hacer .
“
“
r2 -- r2 * r1 .
“
“
r2 -- r2 * r1
“
“
write r2
Cómo calculamos la complejidad de un algoritmo con este
modelo?
tj : tiempo de ejecución de la instrucción j.
T: tiempo de ejecución de un programa que ejecuta nj instrucciones de
tipo j.
T = j nj tj
Si suponemos que todos los tj son iguales tenemos
T = j nj
---------------------------------------------------------------------------Esta es el modelo que usamos implícitamente cuando
“calculamos” la complejidad de un algoritmo en la práctica
Tamaño de la entrada de un problema
• Número de símbolos de un alfabeto finito necesarios para
codificar todos los datos de un problema.
• El tamaño de la entrada de un problema depende de la base
o alfabeto elegidos.
•
para almacenar un entero positivo N en base 2 se necesitan
L =log2(N+1)  dígitos binarios.
• en una base b cualquiera se necesitan L =logb(N+1) 
• si a y b  1 entonces logb N = loga N / loga b
qué implica esto desde el punto de vista de la complejidad de
un algoritmo?
Podemos considerar distintos modelos para determinar la
complejidad en función del tamaño de la entrada del
problema.
• Modelo uniforme: suponemos que los valores a almacenar
están acotados.
• Modelo logarítmico: consideramos la representación
binaria de los números a almacenar, o sea medimos el
tamaño de la entrada en bits.
Qué consecuencias puede tener usar uno u otro modelo?
Máquina de Turing
(determinística)
Componentes de una máquina de Turing
===========================================================
• Cinta infinita dividida en celdas iguales que pueden contener un único símbolo
de un alfabeto finito T.
• Cabeza de lectura escritura que apunta a una de las celdas.
• lista finita de estados posibles {qi}. Hay al menos un estado final.
• operaciones:
i) lectura del símbolo ti inscripto en la celda señalada por la cabeza.
ii) guardar en esta celda el símbolo tf determinado por qi.
iii) transición al estado qf determinado por ti y qi .
iv) movimiento de la cabeza de lectura/escritura a izquierda o derecha.
V) inicio de un nuevo ciclo.
Para cuando llega a un estado final.
==========================================================
Un programa correspondiente a esta máquina de Turing es un
conjunto finito de quintuplas (qi,ti,qs, ts, m).
La instrucción (qi,ti,qs, ts, m) se interpreta como:
“ si la máquina está en el estado qi y lee en la posición
actual de la cabeza de lectora el símbolo del alfabeto ti ,
entonces escribe en dicha posición el símbolo ts , pasa al
estado qs y se mueve a la izquierda o a la derecha según el
valor de m sea -1 ó +1 “.
Ejemplo
• Máquina de Turing para resolver el problema de determinar si
dados N y M con M > 1, N es divisible por M.
• Alfabeto T = { A, B, 0, 1, 2 }
• Se almacenan en la cinta M y N codificados en base 1, separados
por el símbolo B y con el símbolo A marcando el fin del número.
• Estado inicial: q0
• Estados posibles Q = {q0, q1 ,q2,qs, qn}
• Estados finales: qs, qn
Programa
Instrucciones
(q0, A,, q2 , A , + 1)
(q0, B,, q0 , B , - 1)
(q0, 0 , q0 , 0 , - 1)
(q0, 1 , q1 , 2 , + 1)
(q0, 2, q0 , 2 , -1)
(q1, A,, qn , * , *)
(q1, B,, q1 , B , + 1)
(q1, 0,, q1 , 0 , + 1)
(q1, 1 , q0 , 0 , - 1)
(q1, 2 , q1 , 2 , + 1)
(q2, A,, qs , * , *)
(q2, B,, q2 , B , + 1)
(q2, 0, q2 , 0 , + 1)
(q2, 1 , q0 , 1 , -1)
(q2, 2, q2 , 1 , + 1)
Cómo funciona esto?.
Usar esta máquina para decidir si 5 es divisible por 3.
• Cómo se calcula la complejidad de un algoritmo
representado por una máquina de Turing?.
• Formalmente un algoritmo es un objeto asociado a una máquina de
Turing que resuelve un determinado problema. La complejidad del
mismo debería ser calculada en función del mismo.
• En la práctica sin embargo usaremos una noción un poco menos formal
de algoritmo y también para determinar la complejidad de un algoritmo,
que se parece más al modelo RAM.
• Desde el punto de vista práctico ambos enfoques han mostrado ser
equivalentes.
Determinaremos la complejidad contando la cantidad de operaciones
básicas de alto nivel que realiza el algoritmo, en función del tamaño
del problema.
Es correcto esto?. Puede ser peligroso?
Cuándo decimos que un algoritmo es bueno?
Armemos esta tabla..... supongamos que tenemos algoritmos con las
siguientes complejidades y los corremos en la misma máquina
(los tiempos están dados en segundos )
10
n 0.00001
n2
n3
n5
2n
3n
20
0.00002
30
0.00003
40
0.00004
50
0.00005
60
0.00006
La tabla queda así…..
(los tiempos están dados en segundos salvo cuando dice otra cosa)
10
n 0.00001
n2 0.0001
n3 0.001
n5 0.1
2n 0.001
3n 0.59
20
0.00002
0.0004
0.008
3.2
1.0
58 min
30
0.00003
0.0009
0.027
24.3
17.9min
6.5años
40
0.00004
0.0016
0.064
1.7 min
12.7días
3855siglos
50
0.00005
90.0025
0.125
5.2 min
35.6 años
2 E+8 siglos
60
0.00006
0.0036
0.216
13.0
366 siglos
1.3 E+13 siglos
Los datos de la tabla anterior corresponden a una máquina MUY vieja
(son datos del libro de Garey y Johnson de 1979).
Qué pasa si tenemos una máquina 1000 veces más rápida?. Un millón de
veces más rápida?.
Cuál sería el tamaño del problema que podemos resolver en una hora
comparado con el problema que podemos resolver ahora?.
n
n2
n3
n5
2n
3n
actual
N1
N2
N3
N4
N5
N6
computadora 1000 veces mas rápida
1000 N1
31.6 N2
10 N3
3.98 N4
N5 + 9.97
N6 + 6.29
Qué pasaría si tuviéramos una computadora 1 millón de veces más
rápida?
Cuándo diremos entonces que un algoritmo es bueno?.
Cuándo un algoritmo es suficientemente eficiente para ser
usado en la práctica?
=============================
POLINOMIAL = “bueno”
EXPONENCIAL = “malo”
=============================
Qué pasa si tengo complejidades como las siguientes?:
• n 80
• 1.001n
esto no ocurre en la práctica
Puede ocurrir que un algoritmo sea “malo” en el peor caso y
bueno en la práctica?.
Hay muy pocos casos.
Ejemplo clásico:
método simplex para problemas de programación lineal.
En el peor caso es exponencial pero en la práctica el tiempo de ejecución
es generalmente O(m3) donde m es el número de ecuaciones del
problema de programación lineal (es un método MUY usado en la
industria y los servicios para resolver problemas de
optimización…desde hace 50 años)
Cuándo decimos que un problema está
computacionalmente bien resuelto?
Cuándo hay un algoritmo polinomial para resolverlo.
Como veremos más adelante hay problemas para los cuales no
sabemos si existe o no un algoritmo polinomial para
resolverlos. Saber si una gran familia de estos problemas
tiene solución polinomial o no es el problema abierto más
importante de teoría de la computación.......
continuará en el último capítulo del curso.......
Repasando notaciones
Dadas dos funciones f y g : N+  R decimos que:
• f(n) = O (g(n)) si  c 0 y n0 N tal que f(n)  c g(n)
 n  n0 .
• f(n) =  (g(n)) si  c 0 y n0 N tal que f(n)  c g(n)
 n  n0 .
• f(n) =  (g(n)) si  c,c’ 0 y n0 N tal que
c g(n)  f(n)  c’ g(n)
 n  n0
Si f(n) = O (g(n)) se dice que f es de orden g(n).
Complejidad de algunos algoritmos conocidos
Calculo de determinantes
Sea una matriz M = (aij ) y sea Mij la submatriz de M que se
obtiene sacando de M la fila i y la columna j.
Fórmula recursiva para calcular el determinante:
(desarrollo por la primera fila)
det (M) =  j (- 1) j+1 a ij det (M 1j)
Complejidad de este método?.
Se puede calcular usando la fórmula recursiva que se deduce
de la versión recursiva que dimos del algoritmo:
t(n) = n t(n - 1) + O (n)
Complejidad O(n!)
Hay métodos más eficientes de calcular el determinante de
una matriz?.
SI
(triangular la matriz por el método de Gauss y calcular el producto de la diagonal)
Está este problema bien resuelto computacionalmente?.
Búsqueda secuencial
Datos: n, b, (a1,a2,.............an)
Paso 1: para i = 1,n hacer
if b = ai parar
Paso 2 : parar
=========================
Complejidad?
Búsqueda Binaria
Function Bin (T,x)
si n = 0 o x < T entonces return
i1
jn
mientras i < j hacer
k   (i+j+1)/2
si x < T (k) entonces j  k-1
sino i  k
return i
=========================
Complejidad?
Algoritmos de ordenamiento
Algoritmo de burbujeo
Datos: (a1,a2, ...................an)
Paso 0: poner k = n
Paso 1: mientras k  1 hacer
Paso 2: poner i = 1
Paso 3: mientras i  k hacer
Paso 4 : si ai  ai+1 cambiar ai con ai+1
Paso 5: poner i = i + 1
Paso 6 : poner k = k -1
Paso 7 : parar
=========================
Complejidad?
Heap sort
Heap: árbol binario casi completo, donde cada nodo tiene
asignado un valor de forma que el valor de un nodo es
mejor (mayor) que el de sus hijos.
Puede ser representado por un arreglo T en el cual los nodos
del nivel k se guardan en las posiciones
T [2k], T [2k+1], ................T [2k+1-1]
ejemplo
T [1]
T [2]
T [4]
T [8]
T [9]
T [3]
T [5]
T [10]
T [6]
T [7]
Ordenar T = 1, 6, 9, 2, 7, 5, 2, 7, 4, 10 usando Heapsort.
1
9
6
2
7
4
7
10
5
2
Algoritmo Make-Heap
Dado un arreglo T los siguientes procedimientos nos permiten ordenarlo:
procedure sift-down(T [1..n], i)
(“baja” el nodo i hasta que T [1..n] vuelve a ser un heap, suponiendo que
T sea suficientemente grande para ser un heap (caso base?)
ki
repetir mientras j  k
jk
if 2j  n y T [2j ] > T [k ] entonces k  2j
if 2j < n y T [2j +1 ] > T [k ] entonces k  2j + 1
intercambiar T [j ] y T [k ]
fin
procedure make-heap (T [1..n])
(arma el heap)
para i = n /2 ....1 hacer sift-down (T,i)
fin
procedure heapsort ((T [1..n])
(ordena T)
make-heap (T)
para i = n , 2 hacer
intercambiar T [1] y T [i]
sift-down(T [1..i-1], 1)
fin
Complejidad de make-heap
Cómo calcular t(k), complejidad de armar un heap de altura
k?
Podemos plantear la siguiente formula recursiva para la
complejidad:
t(k) = 2 t (k-1) + O (k)
entonces
t(k) = O (2k)
Cómo un heap de n elementos tiene altura log n  entonces
puede ser construido en en O(2 log n  ) pasos o sea en
tiempo lineal.
Cuál es la complejidad de Heapsort?
Límite inferior para la complejidad de un
algoritmo
• Cuándo se puede obtener?
• Importancia
EJEMPLO: algoritmos de ordenamiento basados en
comparaciones. Se pueden modelar por un árbol binario
de decisión. Los estados finales del algoritmos están
representados en las hojas.
Entonces nro de hojas  n! Como el árbol es binario, nro de
hojas  2h
(h altural del árbol) y 2h  n! . Entonces
h  log2 (n!) = log2 (n (n-1)....2. 1) > log2 (n (n-1)....n/2) >
log2 n /2 (n/2) =
= n/2 (log2 n - 1) ) =  (n log2 n )
Conclusión: HEAPSORT es “óptimo” dentro de este tipo de
algoritmos.
Técnicas de diseño de algoritmos
•
•
•
•
•
•
Algoritmos golosos
Dividir y conquistar
Recursividad
Programación dinámica
Backtracking
Algoritmos Probabilísticos
Algoritmos Golosos
Se usan principalmente para problemas de optimización.
Componentes:
• Lista o conjunto de candidatos
• Conjunto de candidatos ya usados
• Función que verifica si un conjunto de candidatos da una solución al
problema.
• Función que verifica si un conjunto de candidatos es hasta el momento
factible.
• Función de selección
• Función objetivo
--------------------------------------------------------------------------------Función GREEDY (C)
{C es el conjunto de candidatos}
S ------ 
Mientras NOT solución(S) y C   hacer
x -------------------- elemento que maximice select (x)
C <-------------------- C \ {x}
Si S U {x} es factible entonces s --------- S U {x}
Si solución(S) entonces RETURN S
sino RETURN “no hay solución”
---------------------------------------------------------------------------------
Qué hacen las funciones solución(S) y select (x) ?
Minimizar el tiempo de espera en un sistema
Un servidor tiene n clientes para atender. Se sabe que el
tiempo requerido para atender cada cliente i es ti. Se
quiere minimizar
T = i (tiempo que i está en el sistema)
El algoritmo goloso que atiende al cliente que requiere el
menor tiempo de atención entre los que aún están en la
cola resuelve el problema. (hay que demostrarlo)
Supongamos que I = ( i1,i2,......in) es una permutación de los
enteros {1,2....n}. Si los clientes son atendidos en el orden
I entonces
T = ti1 + (ti1+ ti2) + (ti1 + ti2 + ti3) + ......
= k (n-k+1) tik
Usando esta expresión podemos probar que el algoritmo
goloso es correcto………..
Supongamos que en I hay dos enteros a y b con a < b y tal que tia > tib.
O sea el cliente a es atendido antes que el b, a pesar de que el primero
requiere mas tiempo de servicio. Si intercambiamos cambiamos las
posiciones de a y b tenemos un nuevo orden I´ para el cual
T (I´) = (n-a + 1) tib +(n-b+1) tia + ka,b (n-k+1) tik
Entonces
T(I) – T ( I´) = (n-a + 1) (tia - tib ) +(n-b+1) (tib - tia )=
= (b-a) (tia - tib ) > 0
Vemos que podemos seguir mejorando el valor de T intercambiando el
orden de los clientes hasta obtener el orden óptimo dado por el
algoritmo goloso.
==================================
Cuál es la complejidad de este algoritmo?
Planificación de tareas
Hay n tareas que deben ser ejecutadas cada una de las cuales
requiere una unidad de tiempo. En cada momento se puede
ejecutar una sola tarea.
Cada tarea i produce un beneficios gi y debe ser ejecutada antes
del tiempo di.
Ejemplo con n = 4
i
gi
di
1
50
2
2
10
1
3
15
2
4
30
1
Algoritmo goloso para este problema
En cada paso elegir para ejecutar la tarea aún no realizada que
produce el mayor beneficio, siempre y cuando el conjunto de
tareas resultante sea factible.
Cómo se ve que un conjunto de tareas es factible?.
Cuando hay un ordenamiento de esas tareas que permite
ejecutarlas a todas a tiempo.
Lema: sea J un conjunto de tareas y s = (s1, s2,………, sk) una
permutación de dichas tareas tal que
ds1  ds2  ………  dsk
J es factible si y sólo si s también lo es.
Dem:  obvio.
 Si J es factible existe al menos una sucesión de los trabajos
ρ = (r1, r2,…….rk) tal que dri  i, para 1 i  k.
Si fuera s ρ , sea a el menor índice tal que sa  ra y sea b el índice tal que
rb = sa.
Claramente b > a y dra  dsa = drb.
Entonces el trabajo ra podría ser ejecutado más tarde en el lugar de rb y
este último en el lugar de ra. Si los cambiamos en ρ tenemos una
sucesión factible que tiene un trabajo más en coincidencia con s.
Repitiendo este procedimiento podemos llegar a obtener s y ver
entonces que es factible.
Lema: Este algoritmo goloso obtiene la solución óptima para
este problema de planificación de tareas.
Dem: Supongamos que el conjunto I elegido por el algoritmo
goloso es distinto de un conjunto J óptimo. Sean SI y Sj
dos sucesiones factibles para estos conjuntos.
Hacemos intercambios de modo de obtener dos sucesiones
factibles S´I y S´j tal que los trabajos comunes en ambas
secuencias sean ejecutados en el mismo momento (pueden
quedar gaps en alguna de las planificaciones).
O sea si tenemos las secuencias…
SI
SJ
p y q x r - - r s t p u v q w
podemos reordenarlas de la siguiente forma
S´I
S´J
x y - p r - q u s t p r v q w
(En S´I y S´J se siguen respetando los tiempos de entrega di)
Como hemos supuesto que los conjuntos I y J son distintos,
consideremos ahora un momento en el cual la tarea que está
programada en S´I es distinta de la programada en S´j :
• Si una tarea a está programada en S´I y en S´j hay un gap
(la tarea no pertenece a J) entonces J  {a} sería mejor que
J, absurdo porque J es óptimo.
• Si alguna tarea b está programada en S´j y en S´I hay un
gap entonces I  {b} es factible y por lo tanto el
algoritmo goloso tendría que haber incluído a b y no lo
hizo.
• La última posibilidad es que haya una tarea a programada
en S´I y otra b en S´j. Esto implica que a no aparece en J y b
no aparece en I :
- si fuera ga > gb se podría substituir a por b en J y
mejorar a J (absurdo porque J era óptimo).
- si fuera fuera gb > ga el algoritmo goloso tendría que
haber elegido a b antes de considerar a.
- entonces gb = ga
Por lo tanto S´I y S´j tienen programadas en cada momento, o
ninguna tarea, la misma tarea o distintas tareas que dan el
mismo beneficio. Y entonces como J era óptimo I también
lo es.
Algoritmo goloso
(se supone que las tareas están numeradas de modo que g1 g2  …….. gn)
------------------------------------------------------------------Función sequence (d[0……n])
d[0], j[0] --------- 0
k, j[1] ------1
Para i= 1,n hacer
r <-k
mientras d[j [r]] > max ( d(i),r) hacer r = r-1
si d[j [r]]  d[i] y d[i] > r entonces
para l=k-1, r+1 hacer j[l+1] = j[l]
j[r+1] ---------i
k ---- k+1
Return k, j[1,….k]
------------------------------------------------------------------
El algoritmo se puede mejorar un poco, cambiando la forma de
verificar cuando un conjunto de tareas es factible.
Podemos determinar un conjunto factible de tareas de una forma más
eficiente usando el siguiente lema:
Lema: Un conjunto de tareas es factible si y sólo si podemos construir una
secuencia factible que incluya todos los trabajos de la siguiente forma: para
cada trabajo i  J ejecutar i en el momento t, donde t es el mayor entero tal que
0 < t  min (n,di) y en el momento t no se ha decidido que trabajo hacer.
Ejercicio: Demostrar este lema. Escribir un algoritmo más eficiente que el anterior
basado en este lema.
Problema de la mochila
Quiero decidir cuáles son los elementos que tengo que elegir para llevar en la mochila
para maximizar mi beneficio respetando la restricción de capacidad de la misma.
Max z =j cj xj
sujeto a que
j aj xj  b
xj  N+
b : capacidad de la mochila
cj : beneficio de llevar una unidad del elemento j
aj : peso del elemento j
b, cj , aj positivos
En esta primera versión del problema de la mochila vamos a
suponer que podemos llevar a lo sumo un objeto de cada
tipo o “partes” de los mismos, es decir que consideraremos
que las variables xj son reales positivas, o sea queremos
resolver el siguiente problema:
Max j cj xj
sujeto a que
j aj xj  b
0  xj  1
Ejemplo:
n = 5, b = 100 y los beneficios y pesos están dados en la tabla
aj
10
30
20
50
40
cj
20
66
30
60
40
Posibles ideas para un algoritmo goloso para este problema:
• Elegir los objetos en orden decreciente de su beneficio….
• Elegir los objetos en orden creciente de su peso………..
Sirven estas ideas?. Dan el resultado correcto?
Que pasa si ordenamos los objetos por su relación
peso/beneficio? O sea en orden decreciente de los
cj /aj ?.
Lema: Si los objetos se eligen en el orden decreciente de los valores cj /aj
entonces el algoritmo goloso encuentra la solución óptima al problema
de la mochila con variables continuas.
Dem: supongamos que tenemos los elementos ordenados en orden
decreciente de su relación cj /aj . Sea X = (x1,…xn) la solución
encontrada por el algoritmo goloso. Si todos los xi son 1 la solución es
óptima. Sino sea j el menor índice tal que sea xj < 1.
Es claro que xi = 1 para i < j y que xi = 0 para i > j y que
 ai xi = b
Sea V(X) =  ci xi el valor de la solución dada por el algoritmo goloso.
Sea Y = (y1……yn) otra solución cualquiera factible. Como
b =  ai xi   ai yi
se puede ver que
 ( xi – yi ) ai  0
Además para todo índice i se verifica que
( xi – yi ) ci /ai  ( xi – yi ) cj /aj
entonces
V (X) – V ( Y ) =  ( xi – yi ) ci =  ( xi – yi ) aici /ai 
 (cj / aj )  ( xi – yi ) ai  0
O sea el valor de la solución determinada por el algoritmo goloso es mejor
o igual al valor de cualquier otra solución.
Veamos que ocurre con el problema de la mochila, en el caso de que las
variables xj tengan que tomar necesariamente valores enteros (como ocurre en
la práctica, es decir cuando no podemos llevar un “pedazo” de un objeto).
Algoritmo goloso para el problema de la mochila
con variables enteras nonegativas:
----------------------------------------------• Ordenar los elementos de forma que
cj / aj  cj+1 / aj+1
• Para j=1,n y mientras b  0 hacer
xj = b / aj
b = b – aj xj
z = z + cj xj
• Parar
------------------------------------------------
Cuando las variables son enteras este algoritmo
goloso no da siempre la solución óptima para el
problema de la mochila.
Ejemplo: tenemos una mochila de tamaño 10 y 3
objetos, uno de peso 6 y valor 8, y dos de peso 5 y
valor 5.
En el caso de variables enteras el algoritmo goloso
que presentamos puede considerarse como una
heurística para resolver el problema.
Dividir y conquistar
ESQUEMA GENERAL:
-------------------------------------------------------------------------------Función DQ (x)
Si x es suficientemente chico o simple entonces return
ADHOC(x)
Sino, descomponer x en subinstancias x1…….xk .
Para i=1,k hacer yi = DQ (xi)
Combinar las soluciones yi para construir una solución y
para x
Return y
--------------------------------------------
Recursividad, Caso Base
Ejemplos:
•
•
•
•
Búsqueda binaria
Merge Sort
Quick Sort
Algoritmo de multiplicación de Strassen
Cómo calcular la complejidad de estos algoritmos?.
Ecuaciones de recurrencia.
Algoritmo de multiplicación de Strassen
Cómo calcular el producto de dos matrices de dos por dos
usando menos multiplicaciones que con el método
tradicional.
Dadas dos matrices
A = a11 a12
a21 a22
podemos poner……
y
B=
b11 b12
b21 b22
m1 = (a21 + a 22 – a 11 ) (b22 - b 12 + b 11 )
m2 = a11 b11
m3 = a12 b21
m4 = (a 11 – a 21 ) (b 22 – b 12 )
m5 = (a21 + a 22 ) (b 12 – b 11 )
m6 = (a12 - a 21 + a11– a 22 ) b22
m7 = a22 ( b11 + b22 - b 12 – b 21 )
Entonces el producto AB queda:
m2 + m3
m1 + m2 + m5 + m6
m1 + m2 + m4 – m7 m1 + m2 + m4 + m5
Este procedimiento requiere 7 multiplicaciones para cacular el producto
de A y B (pero más sumas que el método tradicional!!).
• Si reemplazamos cada elemento de A y B por una matriz de
n x n, las fórmulas anteriores nos dan una forma de
multiplicar dos 2n X 2n matrices.
A partir de esto tenemos un método recursivo para calcular el
producto de matrices con n potencia de 2.
• Cómo se calcula para n par, la complejidad t(n) de este
algoritmo?.
t(n) = 7 t(n/2) + g(n)
con g(n)  O(n2)
Se puede demostrar que t(n)  Ө (n log 7) y por lo tanto t(n) 
O (n 2.81). (ejercicio)
• Este método se puede generalizar también a matrices cuyas
dimensiones no sean de la forma 2 n.
Backtracking
Técnica para recorrer sistemáticamente todas las posibles
configuraciones de un espacio. Puede pensarse también que es
una técnica para explorar implícitamente árboles dirigidos (o
grafos dirigidos en general pero sin ciclos).
Procedure backtrack (v[1….k])
{v es un vector k-prometedor }
si v es una solución al problema entonces escribir v
sino para cada vector “k+1-prometedor” w tal que
w[1…k] = v[1…k] hacer backtrack (w[1…k+1])
===============================================
No necesariamente exploramos todas las ramas del árbol : “poda”
Problema de las 8 reinas
Ubicar 8 reinas en el tablero de ajedrez sin que ninguna
“amenace” a otra.
• Cuántas operaciones tenemos que hacer si consideramos todas
las combinaciones del tablero tomadas de a 8 y después vemos
cuales sirven?
 64 = 442616536
 8
• Si mejoramos esto usando la representación
(i1, i2, i3 ,i4, i5 ,i6,i7 ,i8 ) donde ij indica que hay una reina en la
columna j y la fila ij y analizamos cuales son factibles o no.
cuántas operaciones tendremos?.
8 8 = 16777216
(se encuentra una solución después de revisar “sólo” 1299 852 posiciones)
• Si vemos que dos reinas no pueden estar ni en la misma
fila ni en la misma columna vemos que en realidad
podemos representar el problema mediante las
permutaciones de (1,2,3,4,5,6,7,8). Cuántas operaciones
tenemos en este caso?.
8! = 40320
(implementando este algoritmo podemos ver que hay que
revisar 2830 posiciones hasta encontrar una primera
solución)
Algoritmo de bactracking
Decimos que un arreglo V de los enteros 1 a 8 es kprometedor si ninguna de las reinas ubicadas en las
posiciones (1, v(1)), (2,v(2)), (3,v(3)),…..(k,v(k)), 1 k  n
amenaza a las otras.
O sea v es k-prometedor si para todo ij entre 1 y k, tenemos
que v(i) –v(j)  {i-j,0,j-i}.
Un vector 8-prometedor es solución del problema de las 8
reinas.
Algoritmo de backtracking
-----------------------------------------------------------------------------------Procedimiento Queens (k, try, diag45, diag 135)
{try[1,…k] es k-prometedor, col = {try [i], 1 i k}
diag45 = {try [i]-i +1, 1 i k}
diag135 = {try [i] + i - 1, 1 i k}}
Si k = 8
entonces {un vector 8-prometedor es solución}. Return try
Sino {explorar las k+1-prometedoras extensiones de try}
Para j=1,8
si j  col y j-k  diag45 y j+k  diag135 entonces
try[k+1] --j
{try [1,….k+1] es k+1-prometedor}
Queens (k+1, col U {j}, diag45 U {j}, diag135 U {j+k}
-------------------------------------------------------------------------------------
• Se puede ver computacionalmente que este árbol de
bactracking tiene 2057 nodos y que es suficiente explorar
114 para encontrar la primera solución. (Cómo se hace
para calcular esto?).
Cuál es la complejidad de este algoritmo de
bactracking?
Ejercicio: Mostrar un valor de n  2 para el cual el problema
de las n reinas no tiene solución.
Otros ejemplos
Encontrar, usando backtracking todas las soluciones enteras de
x1+ 2x2 + x3  10
1  xi  4
i =1,2,3
Escribir algoritmos de backtracking (con poda
cuando se pueda) para los siguiente problemas:
• Encontrar una solución para el siguiente puzzle: hay 4
cubos tales que cada una de las 24 caras de los mismos
están coloreadas de azul, rojo, verde y blanco. El juego
consiste en ubicar los cubos en hilera de modo que cada
color aparezca una vez arriba, otra abajo, otra en el frente y
otra en la parte de atrás.
• Un “desarreglo” de un conjunto de números {1,…n} es una
permutación p de dichos números tal que ninguno de ellos
queda en la posición indicada por su valor, es decir, p(i)  i
para todo 1 i  n .
Programación Dinámica
• Técnica bottom- up
• Principio de optimalidad : un problema de optimización satisface el
principio de optimalidad si en una sucesión óptima de decisiones o
elecciones, cada subsucesión es a su vez óptima. (es necesario que se
cumpla para poder usar la técnica de programación dinámica, no todos los
problemas lo cumplen)
Ejemplos:
•
•
•
•
•
•
Coeficientes binomiales
Producto de matrices
Comparación de secuencias de ADN
Subsecuencia creciente máxima
Arbol de búsqueda óptimo
etc.
Cálculo de coeficientes binomiales
Supongamos que queremos calcular los coeficientes binomiales
usando la siguiente función (“divide and conquer”):
--------------------------Función C(n,k)
si k = 0 or k = n entonces return 1
sino return C (n-1, k-1) + C (n-1, k)
---------------------------Es eficiente calcular así?.
No, los mismos valores de C(n,k) son calculados varias veces.
Cuál es la complejidad de este algoritmo?.
 ( n  )
k
Triángulo de Pascal
0
1
2
3
…….
k-1
k
0
1
.
.
.
.
.
.
1
1
1
.
.
2
1
2
1
.
.
…… .
.
.
.
.
n-1
.
.
.
.
.
n
.
.
.
..
.
.
C(n-1, k-1)
C(n-1, k)
C (n,k)
Cuál es la complejidad de este algoritmo?.
Cuánta memoria requiere?. Hay que guardar en memoria toda
la tabla?.
• La “fórmula” que usamos aquí es la misma que
en el algoritmo “divide and conquer”, pero acá
no calculamos varias veces lo mismo.
• Tenemos entonces una complejidad de O(nk).
Problema de la multiplicación de n matrices
Queremos ver la forma óptima de calcular
M= M1 M2 M3………. Mn
Por la propiedad asociativa del producto de matrices esto puede
hacerse de muchas formas. Queremos determinar la que
minimiza el número de operaciones necesarias.
Vale el principio de optimalidad acá?
Ejemplo: supongamos que A es de 13 x5, B de 5 x 89 , C de 89 x 3
y D de 3 x 34.
((AB)C)D requiere 10582 multiplicaciones
(AB)(CD) requiere 54201 multiplicaciones
(A(BC))D requiere 2856 multiplicaciones
A((BC)D) requiere 4055 multiplicaciones
A(B(CD)) requiere 26418 multiplicaciones
Para ver cuál es la forma óptima de multiplicar deberíamos
analizar todas las formas posibles de poner los paréntesis y ver
cual es la mejor.
Sea M =( M1 M2 M3………. Mi)(Mi+1 M i+2………. Mn)
Sea T(i) la cantidad de formas de poner los paréntesis en el lado
derecho y T(n-i) en el lado izquierdo. Entonces para cada i hay
T(i)T(n-i) formas de poner los paréntesis para toda la
expresión y
T(n) = i T(i) T(n-i)
A partir de que T(1) =1 se puede calcular la expresión para
cualquier n. (números catalanes).
n
1
2
3
4
5
10
15
T(n)
1
1
2
5
14
4862
267440
Encontrar de esta forma (algoritmo de fuerza bruta) la mejor manera de
calcular la cantidad de multiplicaciones que requiere cada posibilidad es del
orden  (4n/n).
2n
2n-2
Dem: ejercicio, usar que  n   4n / (2n+1) y T(n) = 1/n  n -1 )
Cómo usar Programación Dinámica para resolver este
problema?
Construimos una tabla mij (1 i  j  n) donde mij es la cantidad
mínima de multiplicaciones necesaria para calcular
Mi M i+1 M i+2…….Mj .
La solución al problema es entonces m1n.
Suponemos que las dimensiones de la matriz Mi están dadas por
un vector di , 0  i  n, donde la matriz Mi tiene di-1 filas y di
columnas y armamos la tabla diagonal por diagonal, para cada
diagonal s, s = 0,……., n-1
•
•
•
s=0 : mii = 0
para i=1,2,…n
s= 1 : mi i+1 = di-1didi+1 para i=1,2,…n-1
1< s < n : mi i+s = mini ≤ k < i+s {mik + mk+1 i+s + di-1dkdi +s}
para i=1,2,…n-s
En el ejemplo anterior d= ( 13,5,89,3,34) y entonces
. Para s =1 m12 = 5785 , m23 = 1335, m34 = 9078
. Para s =2 m13 = 1530, m24 = 1845
. Para s = 3 m14 = 2856
i=1
2
3
4
J=1
2
3
4
0
5785
1530
2856
s=3
0
1335
1845
s=2
0
9078
s=1
0
s=0
• Cómo guardar la información para saber además cuál es la forma de hacer
el producto tener este número de multiplicaciones?.
• Complejidad : en cada diagonal s hay que calcular n-s elementos y para
cada uno de ellos hay que elegir entre s posibilidades, entonces la cantidad
de operaciones del algoritmo es del orden de:
s (n-s)s = n s s - s s2 = n2 (n-1)/2 –n (n-1) (2n-1)/6
= (n3 –n )/6
o sea la complejidad del algoritmo es O(n3).
Triangulación de mínimo peso
Una triangulación de un polígono convexo P = {v1, v2.... vn, v1} es un
conjunto de diagonales que particiona el polígono en triángulos. El peso
de la triangulación es la suma de las longitudes de sus diagonales.
Ejemplo: dos triangulaciones de un mismo polígono.
Observar que cada eje del polígono original está en un solo triángulo.
Dado un segmento que une dos vértices adyacentes,
transformarlo en un triángulo implica determinar el tercer
vértice. Una vez identificado este vértice al polígono le
quedan dos polígonos menores que deben ser a su vez
triangulados.
Sea T [i,j] el costo de triangular una de las parte del polígono
que queda después de trazar una diagonal desde el vértice vi
al vértice vj,, sin tomar en cuenta la longitud dij de la cuerda
que va de vi a vj (para no contarla dos veces).
k
i
j
T [i,j] = min k = i,j {T [i,k] + T [k,j] + dik + dkj }
Caso base, cuando i y j son vecinos:
T [i, i+1] = 0
Algoritmo para triangulación del polígono
Triangulación-mínima (P)
para i = 1,n -1 hacer
T[i,i+1] =0
para gap = 1, n-1
para i = 1, n-gap hacer
j= i+gap
T [i,j] = min k=i,j {T [i,k] + T [k+1,j] + dik + dkj }
return T[1,n]
• Complejidad : O(n3)
• Complejidad espacial: O(n2)
===========================================
• Qué pasa si el polígono no es convexo?.
• Qué pasaría si hubiera puntos en el interior del polígono?.
El número de subregiones en cada paso ya no es 2 y crece
exponencialmente y entonces el algoritmo de programación dinámica
no es eficiente.
No se conocen algoritmos polinomiales para el problema de triangulación
en este caso.
Comparación de secuencias de ADN
Supongamos que tenemos dos secuencias de ADN
GACGGATTAG y GATCGGAATAG
Queremos decidir si son “parecidas” o no.
Para qué se usa esto?
Alineamiento
GA- CGGATTAG
GATCGGAATAG
Objetivo: construir un algoritmo que determine la mejor alineación global
entre dos secuencias (que pueden tener distinta longitud).
Asignamos valores a las coincidencias, a las diferencias y a los gaps.
(Las transparencias que siguen sobre este tema, fueron preparadas por
alumnos de la materia Biología Computacional, ver página web).
Scores
Ejemplo
T
S
GA–CGGATTAG
GATCGGAATAG
match = +1
mismatch = -1
gap penalty = -2
Score(T, S) = 9 x 1 + 1 x (-1) + 1 x (-2) = 6
Algoritmo de Programación dinámica
Needleman & Wunsch (1970)
• Encuentra el mejor scoring para un alineamiento global de
dos secuencias
• A partir de allí se determina un alineamiento que tiene ese
score óptimo.
De acuerdo a los parámetros elegidos la alineación óptima
puede variar.
• Se quiere alinear las secuencias S y T.
• Sea n el tamaño de S y m el de T.
• Hay n+1 posibles prefijos de S y m+1 posibles
prefijos de T (incluyendo el string vacío).
• Se genera una matriz F de (n+1)x(m+1) elementos.
• El elemento F(i, j) tendrá la mejor similitud entre
las subsecuencias S[1...i] y T[1...j].
Dadas las secuencias:
A
S = AAAC
T = AGC
A
A
A
C
G
C
Cálculo de la similitud óptima
F(i-1,j-1)
F(i,j-1)
F(i-1,j)
F(i, j)
F(i-1, j-1)+p(i,j)
F(i, j)= max F(i-1, j)-w
F(i, j-1)-w
w=Penalización del Gap
Llenado de la Matriz
• Se llena la matriz la primera fila y la primera columna con
múltiplos de la penalidad por espacio.
• Se llenan los elementos interiores
F[i,j] = max
F[i, j-1] + g
F[i-1, j-1] + p(i,j)
F[i-1, j] + g
• g = penalidad por espacio, p(i,j) = valor del matching entre
S(i) y T(j)
• Sigue cualquier orden que permita tener los valores F[i, j1], F[i-1, j-1], F[i-1, j]. Como fila por fila ( de izquierda a
derecha) o columna por columna (de arriba hacia abajo).
Algoritmo
n = long(S)
m = long(t)
Para i = 0 hasta n hacer
F[i, 0] = i g
Para i = 0, m hacer
F[0, i] = i g
Para i = 1, n hacer
Para j = 1, m hacer
F[i, j] = max (F[i-1, j] + g, F[i-1, j-1]
+ p(i,j), F[i, j-1] + g)
Return F
Inicio de la matriz:
A
0
G
-2
A
A
A
A
C
C
Inicio de la matriz:
0
A
A
A
C
A
G
C
-2
-4
-6
AAA C
AGC
0
A
-2
A
-4
A
-6
C
-8
A
G
C
-2
-4
-6
0
A
-2
A
-4
A
-6
C
-8
A
G
C
-2
-4
-6
1
-1
-1
1
-1
-1
1
-1
-1
-1
-1
1
0
?
?
A
F(i-1, j-1)+s(i,j)
F(i, j)= max F(i-1, j)-w
F(i, j-1)-w
-2
A
-4
A
-6
C
-8
A
G
C
-2
?
-4
-6
1
-1
-1
1
-1
-1
1
-1
-1
-1
-1
1
0
A
F(i-1, j-1)+s(i,j)
F(i, j)= max F(i-1, j)-w
F(i, j-1)-w
-2
A
-4
A
-6
C
-8
A
G
C
-2
-4
-6
1
-1
-1
-1
-1
1
-1
-1
-1
-1
1
1
1
A
A
0
A
F(i-1, j-1)+s(i,j)
F(i, j)= max F(i-1, j)-w
F(i, j-1)-w
-2
A
-4
A
-6
C
-8
A
G
C
-2
-4
-6
1
1
-1
-1
A
AG
-1
1
-1
-1
1
-1
-1
-1
-1
1
0
F(i-1, j-1)+s(i,j)
F(i, j)= max F(i-1, j)-w
F(i, j-1)-w
A
-2
A
-4
A
-6
C
-8
A
G
C
-2
-4
-6
1
1
1
-1
1
-1
-3
-1
-1
-1
0
-1
-1
-2
-1
-1
-1
1
-2
-5
-4
score óptimo global
-3
-1
-1
Tenemos el valor
de la similitud entre
las secuencias.
Ahora necesitamos
reconstruir el
alineamiento
0
A
-2
A
-4
A
-6
C
-8
A
G
C
-2
-4
-6
1
1
1
-1
1
-1
-3
-5
-1
-1
-1
0
-1
-2
-1
-4
-1
-3
-1
-2
-1
-1
1
-1
Determinación del alineamiento óptimo
• Se comienza desde el valor de F[n,m] y se realiza el
camino inverso hasta llegar al valor F[0,0].
• Por cada valor F[i,j] se testea si provino de F[i-1, j-1] +
p(i,j) o F[i-1, j] – g o F[i, j-1] – g.
Hay que tomar la última coincidencia del alineamiento y comenzar
a buscar un camino hacia atrás que maximice la función, es decir
siguiendo los valores más altos.
Cuando los valores coinciden, cualquier camino que se tome
conducirá a la formación de un alineamiento de igual score.
0
Máxima preferencia
A
-2
A
-4
A
-6
C
-8
A
G
C
-2
-4
-6
1
1
1
-1
1
-1
-3
-5
-1
-1
-1
0
-1
-2
-1
-4
-1 C
AA
-3
G C
-1
-2
-1
-1
1
-1
0
A
A
-2
-4
AAA C
AG C
A
C
-6
-8
A
G
C
-2
-4
-6
1
1
1
-1
1
-1
-3
-5
-1
-1
-1
-1
-1
0
-2
-1
-1
-1
1
-2
-4
-3
-1
-1
Algoritmo de reconstrucción
• Este algoritmo va a construir un
alineamiento óptimo dadas la matriz F y las
secuencias originales S y T.
• La variable “len” contiene la longitud del
alineamiento (que puede ser n+m)
• Los vectores (secuencia de valores) “aligS”
y “aligT” contienen los caracteres
alineados.
INPUT: indices i,j, array F.
Alinear(i, j)
Si i=0 y j = 0 entonces
j-1
len = 0
+p(i,j)
Sino Si i>0 y F[i,j] = F[i-1, j] + g entonces
i-1
Alinear(i-1, j, len)
len = len + 1
+g
i
aligS[len] = s[i]
aligT[len] = //(espacio)
Sino Si i>0 y j>0 y F[i,j] = F[i-1, j-1] + p(i,j) ent
Alinear(i-1, j-1, len)
len = len + 1
aligS[len] = s[i]
aligT[len] = t[j]
Sino
// debe ser F[i,j] = F[i, j-1] + g
Alinear(i, j-1, len)
len = len + 1
aligS[len] = //(espacio)
aligT[len] = t[j]
Fin
Devolver aligS, aligT y len.
j
+g
Complejidad
m = long(S)
n = long(t)
Para i, desde 0 hasta n hacer
n veces
F[i, 0] = i x g
Para i, desde 0 hasta m hacer
m veces
F[i, 0] = i x g
Para i, desde 1 hasta n hacer
m veces
n veces
Para j, desde 1 hasta m hacer
F[i, j] =máximo(F[i-1, j] + g, F[i-1, j-1]
+ p(i,j), F[i, j-1] + g).
Devolver F[n,m]
Complejidad
• Complejidad Temporal:
– O(mn) en armar la matriz
– O(m + n) en buscar en la matriz (recorrer la
secuencia resultante, que a lo sumo tiene n+m
elementos).
• Complejidad Espacial:
– O (mn) por el espacio necesario para la matriz
Algoritmo de programación dinámica para el problema
de la mochila
Trabajaremos con la siguiente versión del problema de la
mochila.
Problema P(n, k): dado un entero k y n items i de diferentes
tamaños ki, encontrar un subconjunto de los items tal que
sumen exactamente K o determinar que no existe tal
conjunto.
Cómo se expresa el principio de optimalidad acá?.
Algoritmo knapsack
Input: S un array con los tamaños de cada item y K.
Output: Un arreglo de dos dimensiones tal que P[i,k].exist =
true si existe una solución con los primeros i elementos para
una mochila de tamaño k y P[i,k].belong = true si el
elemento i pertenece a esa solución.
Algoritmo
Empezar
P[0,0] exist = true
para k = 1,K hacer
P[0,k].exist = false
para i =1,n hacer
para k = 0,K hacer
P[i,k]. exist = false
if P[i-1,k]. exist then
P[i,k]. exist = true
P[i,k]. belong = false
else if k-S[i]  0
if P[i-1,k-S[i]]. exist then
P[i,k]. exist = true
P[i,k]. belong = true
end
Ejemplo: aplicar el algoritmo anterior a 4 items de tamaño
2,3,5 y 6 tamaños de la mochila desde 2 a 16.
k
1
2
3
4
5
6
7
8
9
10 11
12 13 14 15 16
2
-
I
-
-
-
-
-
-
-
-
-
-
-
-
-
-
3
-
O
I
-
I
-
-
-
-
-
-
-
-
-
-
-
5
-
O
O
-
O
-
I
I
-
I
-
-
-
-
-
-
6
-
O
O
-
O
I
O
O
I
O
I
-
I
I
-
I
Referencias de la tabla:
I : se encontró una solución que contiene este item
O: se encontró una solución sin este item
- : no hay solución hasta el momento (si este símbolo aparece en la última línea
quiere decir que no hay solución para este tamaño de mochila)
Ejercicio: Pensar un algoritmo de programación dinámica
con ideas similares a este para el problema de la mochila
más general, es decir cuando queremos resolver
Max j cj xj
sujeto a que
j aj xj  b
xj  {0,1}
Subsecuencia creciente más larga
Determinar la subsecuencia creciente más larga de una sucesión de
números.
Ejemplo:
S = { 9,5,2,8,7,3,1,6,4)
Las subsecuencias más largas son {2,3,4} o {2,3,6 }
Para construir un algoritmo de programación dinámica definimos:
li = longitud de la secuencia creciente más larga que termina con el
número si
pi = predecesor de si en la secuencia creciente más larga que termina con
el número si
Vale el principio de optimalidad en este caso?. Cómo se expresaría?.
Relación de recurrencia:
li = maxj<i
l0 = 0
lj +1,
con sj < si
La solución al problema la da
max 1in li
sucesión
9 5
2
8
7
3
1
6
4
longitud
1 1
1
2
2
2
1
3
3
predecesor
-
-
2
2
2
-
3
3
-
Complejidad?
O(n2) ( se podría implementar en O(nlogn))
Cuál es la complejidad espacial?.
Cómo hacemos para tener también la sucesión y no sólo la
longitud.
Arboles de búsqueda óptimos
• árbol binario de búsqueda: cada nodo contiene una clave, y
el valor contenido en cada nodo interno es mayor o igual
(numérica o lexicográficamente) que él de sus descendientes
por la rama izquierda y menor o igual que los de la
derecha.
• Cuántos árboles binarios de búsqueda distintos puede haber
con un número dado n de palabras código (o clave)?.
• Cómo podemos determinar el árbol binario que minimiza el
número promedio de comparaciones necesarias para
encontrar una clave.
• Supongamos que tenemos un conjunto c1 < c2 < …..< cn de
claves con probabilidad pi de ser buscadas.
Si la clave ci está en el nivel di entonces hacen falta di + 1
comparaciones para encontrarla.
• Suponemos  pi = 1
Entonces para un árbol dado la cantidad de
comparaciones que tenemos que hacer en media
para buscar una clave es
C = i pi (di + 1)
Dado un conjunto de claves y sus probabilidades queremos
construir un árbol que minimice esta función, usando
programación dinámica.
Se cumple el principio de optimalidad?.
“ en un árbol óptimo todos sus subárboles son óptimos para
las claves que contienen”
Ejercicio: pensar una forma de determinar un árbol óptimo
usando programación dinámica.
Hay otros métodos para resolver este problema (Algoritmo de Huffman)
• La técnica de Programación Dinámica se aplica también a
problemas con variables continuas
( los ejemplos que vimos acá fueron todos con variables
discretas).
• En el caso de variables continuas puede ser necesario
resolver en el marco de las ecuaciones de recurrencia,
problemas de maximización o minimización continuos,
usando las herramientas habituales del cálculo u otras.
Algoritmos probabilísticos
• Cuando un algoritmo tiene que hacer una elección a veces es preferible
elegir al azar en vez de gastar mucho tiempo tratando de ver cual es la
mejor elección.
Pueden dar soluciones diferentes si se aplican dos veces a la misma
instancia. Los tiempos de ejecución también pueden ser muy diferentes.
• Tiempo promedio de un algoritmo determinístico: considera todas las
instancias de un mismo tamaño son “similares”. (ejemplo: quicksort)
• Tiempo esperado promedio de un algoritmo probabilístico : es el
tiempo medio de los tiempos de resolver la misma instancia del mismo
problema “muchas veces”.
• Peor tiempo esperado: tomando en cuenta el peor caso de todas las
instancias de un cierto tamaño.
Clasificación de algoritmos probabilisticos
• Algoritmos al azar para problemas numéricos: la respuesta es
siempre aproximada pero se espera que la solución sea mejor cuando
más tiempo hay para ejecutar el algoritmo. (simulación, integración
numérica).
• Algoritmos de Monte Carlo: se quiere una respuesta exacta. Por
ejemplo problemas de decisión. Un algoritmo Monte Carlo da siempre
una respuesta pero la respuesta puede no ser correcta. La
probabilidad de suceso, es decir de respuesta correcta crece con el
tiempo disponible para correr ese algoritmo. La principal desventaja
es que en general no se puede decidir eficientemente si la respuesta es
correcta o no.(ejemplo: decidir si un arreglo T tiene un elemento
mayoritario, o sea que aparece en T más de n/2 veces)
• Algoritmos Las Vegas: nunca dan una respuesta incorrecta pero
pueden no dar ninguna respuesta. También la probabilidad de
suceso, es decir de tener respuesta correcta crece con el tiempo
disponible para correr ese algoritmo. (ejemplo: en el problema de
las 8 reinas poner las reinas al azar en las filas que aún están
desocupadas, cuidando solamente que la solución en construcción
sea factible)
• Algoritmos Sherwood : en este caso el algoritmo siempre da una
respuesta y la respuesta es siempre correcta. Se usan cuando algún
algoritmo determinístico para resolver un algoritmo es mucho más
rápido en promedio que en el peor caso. Al incorporar un factor de
azar el algoritmo puede llegar a eliminar la diferencia entre buenas
y malas instancias. (ejemplo: Quicksort)
En todos estos casos suponemos que tenemos disponible un
generador de números al azar de costo computacional
unitario.
O sea dados reales a y b con a < b, uniform (a,b) devuelve un
número x elegido al azar en el intervalo a  x < b. La
distribución de x es uniforme en el intervalo y llamadas
sucesivas del generados dan valores independientes de x.
Para generar enteros al azar uniform (i…j) devuelve un
entero k elegido al azar uniformemente en el intervalo
i k  j
Ejemplo de algoritmo tipo Las Vegas: el problema de las 8 reinas.
Procedimiento Queens LV (var sol[1…8], sucess)
array ok[1…8] (posiciones disponibles)
col, diag45, diag135  
Para k = 0,7 hacer
(sol [1…k] es k prometedor, hay que ubicar a la reina k+1)
nb = 0
para j = 1, 8 hacer
si j  col y j-k  diag45 y j+k  diag135 entonces (la columna j esta
disponible)
nb = nb +1
ok[nb] = j
si nb = 0 entonces sucess =false
j = ok[uniform (1….nb)]
col = col U {j}
diag45 = diag45 U {j}
diag 135 = diag135 U {j+k}
sol[k+1] = j
return success = true
Más ejemplos de algoritmos probabilísticos
en el libro de Brassard y Bratley
Heurísticas
• Dado un problema Π , un algoritmo heurístico es un
algoritmo que intenta obtener soluciones para el problema
que intenta resolver pero no necesariamente lo hace en
todos los casos.
• Sea Π un problema de optimización, I una instancia del
problema, x*(I) el valor óptimo de la función a optimizar en
dicha instancia. Un algoritmo heurístico obtiene una
solución con un valor que se espera sea cercano a ese
óptimo pero no necesariamente el óptimo.
• Si H es un algoritmo heurístico para un problema de
optimización llamamos xH(I) al valor que devuelve la
heurística.
Porqué usar algoritmos heurísticos?. En que tipo de
problemas?
Las heurísticas que son de utilidad práctica son algoritmos
polinomiales.
Cuándo decimos que una heurística es “buena”?
Cómo hacemos para saber si una heurística es buena?
Algoritmos aproximados
Decimos que H es un algoritmo ε- aproximado para el problema Π si para
algún ε > 0
xH(I) - x*(I) | ≤ ε | x*(I) |
O equivalentemente un algoritmo es aproximado si existe  > 0
tal que para toda instancia I
xH(I) / x*(I) ≤ 
(problema de minimización)
Situación ideal, pero poco frecuente
Ejemplo de algoritmo aproximado
Cuando las variables son enteras, el algoritmo goloso que
vimos para el problema de la mochila es un algoritmo
aproximado.
Se puede demostrar que para toda instancia I de dicho
problema se verifica que:
xH(I) / x*(I)  (c1b/a1 )/ (c1 (b /a1)) =
= b/a1 / (b/a1)  0.5
(es un problema de maximización)
Cómo evaluamos una heurística?
• Comportamiento medio
• Análisis probabilístico
• Comportamiento en peor caso (algoritmos aproximados):
i) razón de perfomance en el peor caso :
r = sup {xH(I) / x*(I) } ó r = inf {xH(I) / x*(I) }
según estemos minimizando o maximizando.
ii) error relativo
er = | xH(I) - x*(I) | /| x*(I)|
En la práctica no podemos calcular exactamente estos valores
pero si a veces obtener cotas para ellos.