Programación Dinámica

Programación Dinámica
Héctor Navarro
Introducción
• Colección de herramientas matemáticas
usadas para analizar procesos de toma de
decisiones
• Toma ventaja de la duplicación de subproblemas, almacenando soluciones para su
uso posterior
• Es una técnica bottom-up
Introducción
• En muchas ocasiones esto puede llevar a
soluciones polinómicas aunque puede ser
necesario enumerar todas las soluciones
Principio de Optimalidad
• Es un principio que debe cumplir todo
problema para poder resolverse con P.D.
• La solución óptima a un problema es una
combinación de soluciones a algunos de sus
subproblemas
Método básico
• Plantear una solución recurrente al problema
• Convertir la solución a una solución en donde
se combinan soluciones a instancias pequeñas
del problema para obtener soluciones a
instancias mayores del problema
• Idear una forma para almacenar resultados
previos
Método básico
• Otra forma es usar el método de
memorización que consiste en resolver el
problema con una estrategia top-down, pero
almacenando en alguna estructura de datos
los resultados a los subproblemas ya resueltos
• Suele utilizar una gran cantidad de memoria
Fibonacci
• La forma recursiva de implementar Fibonacci
es implementando la fórmula recurrente:
1
n  0 o n 1

fib (n)  
n 1
 fib (n  1)  fib (n  2)
• El problema con esa implementación es que
crece exponencialmente
• Calcular fib(100) tardaría años si se
implementa así
Fibonacci
fib(5)
fib(4)
fib(3)
fib(3)
fib(2)
fib(1)
fib(2)
fib(1)
fib(0)
15 llamadas
fib(1)
fib(2)
fib(0)
fib(1)
fib(1)
fib(0)
Fibonacci
fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(2)
fib(1)
fib(0)
9 llamadas
fib(3)
Fibonacci
• Implementación con PD
int fib(int n)
{
if(n==0 || n==1) return 1;
int f0 = 0, f1 = 1, r;
for(int i=1;i<n;i++)
{
r = f1 + f0; // f1 += f0
f0 = f1;
// f0 = f1 - f0
f1 = r;
}
return r;
}
Fibonacci
• Implementación con memorización
int memo[1000]; // calcula hasta fib(1000)
int fib(int n)
{
if(n==0 || n==1) return 1;
int & f = memo[n];
if(f==-1)
{
f = fib(n-1) + fib(n-2);
}
return f;
}
memset(memo, -1, sizeof(memo));
Combinaciones
• Una manera de calcular Cn,m es mediante la
fórmula:
n
n!
  
 m  (n  m)!m!
• Para valores relativamente pequeños de n y m
resulta imposible calcularlo de esta forma ya
que n! no puede representarse
Combinaciones
• Una alternativa es usar la fórmula recurrente:


1
m0
n 
   
1
nm
 m   n  1   n  1 1  m  n
 m  1   m 
 


Combinaciones
• Esta fórmula dio origen al triángulo de Pascal
para calcular los factores del binomio de
Newton:
n=0
1
n=1
1
n=2
1
n=3
1
n=4
1
n=5
n=6
1
1
2
3
4
5
6
1
3
6
10
15
1
1
4
10
20
1
5
15
1
6
1
Combinaciones
C5,2
C4,2
C4,1
C3,2
C2,2
C3,1
C2,1
C1,1
C2,1
C1,0
C1,1
C3,1
C2,0
C1,0
C4,0
Combinaciones
m
0
n=0
1
n=1
1
n=2
1
n=3
1
n=4
1
n=5
1
n=6
1
1
2
3
4
5
6
1
1
1
1
1
1
Combinaciones
m
0
1
n=0
1
n=1
1
1
n=2
1
2
n=3
1
n=4
1
n=5
1
n=6
1
2
3
4
5
6
1
1
1
1
1
Combinaciones
m
0
1
2
n=0
1
n=1
1
1
n=2
1
2
1
n=3
1
3
3
n=4
1
n=5
1
n=6
1
3
4
5
6
1
1
1
1
Combinaciones
m
0
1
2
3
4
5
n=0
1
n=1
1
1
n=2
1
2
1
n=3
1
3
3
1
n=4
1
4
6
4
1
n=5
1
5
10
10
5
1
n=6
1
6
15
20
15
6
6
1
Combinaciones
int comb(int n, int m)
{
for(int i = 0; i <= n; i++)
{
_comb[0] = _comb[i] = 1; // marca los casos base
int ant = _comb[0];
for(int j = 1; j < i; j++)
{
int aux = _comb[j];
_comb[j] = ant + _comb[j];
ant = aux;
}
}
return _comb[m];
}
Dar vuelto
• Ya vimos este problema, y vimos que para
algunos conjuntos de monedas el problema
puede resolverse con un algoritmo voraz
• Otros conjuntos de monedas necesitan probar
diferentes combinaciones, por ejemplo
mediante un backtracking
Dar vuelto
• Una solución recursiva (usando Haskell) se
plantea aquí:
vuelto _ 0 = 0
vuelto (h:t) v = v < 0 : 999999
h <= v : min (1+vuelto(h:t)(v-h)) (vuelto t v)
h > v : vuelto t v
Dar vuelto
• La línea: h <= v : min (1+vuelto(h:t)(v-h)) (vuelto t v)
indica que si la moneda actual (h) es menor al
vuelto que hay que dar, existen dos opciones:
1. Tomar la moneda: (1+vuelto(h:t)(v-h))
2. No tomar la moneda: vuelto t v
• Estas llamadas generan un comportamiento
exponencial que hace imposible su cálculo
para números muy grandes
Dar vuelto
• Para implementar la solución usando DP
tendremos una matriz S[M][V+1] en donde V
es el vuelto que deseamos dar y M el número
de tipos de monedas
• S[i][j] contiene el número óptimo de monedas
para dar vuelto j considerando monedas
desde la 0 hasta la i
• Es obvio que S[i][0]=
Dar vuelto
• Por ejemplo, dar vuelto 11 con el conjunto de
monedas: {2,3,5,7}
Dar vuelto
0
2
3
5
7
1
2
3
4
5
6
7
8
9
10
11
Dar vuelto
• La solución final se encuentra en la posición
S[M-1][V]
• La solución es de O(MxV)
• ¿Cómo saber cuáles monedas debemos usar?
KSP
• Knapsack Problem (problema de la mochila)
• Dado un conjunto de objetos con peso wi y
valor vi; y una mochila con capacidad K,
seleccionar cuáles transportar para maximizar
el valor de los objetos transportado
KSP
• Existen diversas versiones:
– Bounded: existe una cantidad limitada de cada
tipo de objeto
– Unbounded: existe una cantidad ilimitada de cada
tipo de objeto
– 0/1 existe un solo objeto de cada tipo
KSP - Unbounded
• Se puede pensar en ordenar los objetos según
la relación valor/peso
• Por ejemplo:
Peso
Valor
Valor/peso
3
4
1.33
4
5
1.25
2
1
0.5
KSP - Unbounded
Peso
Valor
Valor/peso
3
4
1.33
4
5
1.25
2
1
0.5
• Para K = 10:
– Se transportan 3 objetos del primer tipo. Valor
total: 12
– Sin embargo puede transportarse los objetos con
valor 4+4+5 = 13
KSP - Unbounded
• La heurística no funciona
• Supongamos la función ksp(i,j) – retorna el valor
máximo que se puede transportar si se toman en
cuenta los i+1 primeros objetos, con una mochila
de capacidad j
• Para calcular ksp(i,j) existen siempre dos
opciones:
– No colocar más objetos de tipo i: ksp(i,j)=ksp(i-1,j)
– Colocar un objeto de tipo i: ksp(i,j)=v[i]+ksp(i,j-w[i])
KSP - Unbounded
• Finalmente:
ksp(i,j)=max(v[i]+ksp(i,j-w[i]), ksp(i-1,j))
• ksp(i,0) = 0
• Para j>w(i): ksp(i,j) = ksp(i-1,j)
KSP - Unbounded
0
3/4
0
4/5
0
2/1
0
1
2
3
4
5
6
7
8
9
10
Maxsum
• Dada una secuencia de números enteros
S1,…Sn, ¿cuál es la subsecuencia cuyos valores
sumen un máximo valor?
• Si los elementos Si son todos positivos la
respuesta es trivial
• Si existen números negativos la solución
anterior no funciona
• Existe una solución trivial de O(N2)
Maxsum
• Definimos M(j) como la suma máxima de
todas las secuencias que terminan en j
• Por ejemplo, si S = {3,1,-5,4,-1,-1,4,3,-6,2}
• M(0) = 3
• M(1) = 1
• M(2) = -1
• M(3) = 4
Maxsum
• En cada posición j existen dos opciones:
– Agregar Sj a la secuencia M(j-1)
– Comenzar una nueva secuencia que comienza en
Sj
• Si se agrega Sj a la secuencia M(j-1):
M(j) = M(j-1) + Sj
• Si se comienza una nueva secuencia:
M(j) = Sj
Maxsum
• Como queremos la suma máxima:
M(j) = max(M(j-1) + Sj, Sj)
Ejemplo:
i
0
1
2
3
4
5
6
7
8
9
Si
3
1
-5
4
-1
-1
4
3
-6
2
M(i)
Maxsum
• ¿Cómo aplicar el mismo principio a una
matriz?
Multiplicación encadenada de
matrices
• La multiplicación de dos matrices Anxm, Bmxp es
una operación que involucra n x m x p
multiplicaciones simples
• Ahora si deseamos multiplicar varias matrices,
por ejemplo A5x3 x B3x9 x C9x2, podemos
hacerlo de varias formas:
– (AxB)xC
– Ax(BxC)
Multiplicación encadenada de
matrices
• ¿De qué forma es más conveniente realizar el
cálculo?
• (A5x3 x B3x9 ) x C9x2: A x B: 135 multiplicaciones.
Luego (A x B) x C: 90 multiplicaciones. En total:
225 multiplicaciones
• A5x3 x (B3x9 x C9x2): B x C: 54 multiplicaciones.
Ax(BxC): 30 multiplicaciones. Total: 84
multiplicaciones
Multiplicación encadenada de
matrices
• Podemos enumerar las n matrices desde la 0
hasta la n-1. De esta forma, para multiplicar
desde la matriz i hasta la j podemos hacer la
multiplicación en j-i puntos distintos:
Mi x Mi+1 x Mi+2 x … x Mj
Multiplicación encadenada de
matrices
• Por ejemplo, para multiplicar desde i=2 hasta
j=5:
– M2 x (M3 x M4 x M5)
– (M2 x M3)x (M4 x M5)
– (M2 x M3 x M4 ) x M5
• Recursivamente podemos saber cómo realizar
eficientemente cada una de las
multiplicaciones resultantes
Multiplicación encadenada de
matrices
• Definimos M(i,j) la función que retorna cómo
calcular óptimamente desde la matriz i hasta
la j, i<=j.
• Es obvio que:
M(i,j)=0
M(i,i+1)=filas(i) x cols(i) x cols(i+1)
Multiplicación encadenada de
matrices
• Ahora, para calcular M(i,j) hay que probar
todas las j-i opciones distintas, es decir:
M (i, j )  min kj 1i {M (i, k )  K (k  1, j )  fil (i)cols (k )cols ( j )}
Multiplicación encadenada de
matrices
• Por ejemplo: A2x5, B5x1, C1x3, D3x2, E2x4
0 (A)
1 (B)
2 (C)
3 (D)
4 (E)
A
B
C
D
E
0
1
2
3
4
0
10
0
15
0
6
0
24
0
Longest Common Substring (LCS)
• Dados un par de strings, encontrar la
secuencia común a ellos más larga. Por
ejemplo, si S=“xygxxzyfa” y T = “xyxzyfx”,
LCS(S,T)=“xzyf”
• Basta con encontrar el sufijo común mas largo
para todos los pares de prefijos de los strings
Longest Common Substring (LCS)
• Basta con encontrar el sufijo común mas largo
para todos los pares de prefijos de los strings
• S=“xygxxzyfa” y T = “xyxzyfx”
Prefijos:
x
xy
xyg
xygx
xygxx
xygxxz
xygxxzy
xygxxzyf
xygxxzyfa
Prefijos:
x
xy
xyx
xygxz
xygxzy
xygxzyf
Xygxzyfx
Longest Common Substring (LCS)
• Basta con encontrar el sufijo común mas largo
para todos los pares de prefijos de los strings
• S=“xygxxzyfa” y T = “xyxzyfx”
Prefijos:
x
xy
xyg
xygx
xygxx
xygxxz
xygxxzy
xygxxzyf
xygxxzyfa
Prefijos:
x
xy
xyx
xygxz
xygxzy
xygxzyf
Xygxzyfx
Longest Common Substring (LCS)
• Si S tiene n caracteres y T tiene m caracteres,
existen n sufijos para S y m para T
• Existen en total n x m pares de prefijos
posibles
• Sea Si el prefijo de S de tamaño i,
comenzaremos por comparar Si y Tj
• Si Si=Tj, LCS(Si,Tj)=1+LCS(Si-1,Tj-1)
• Si Si≠Tj, LCS(Si,Tj)=0
Longest Common Substring (LCS)
• La implementación se lleva a cabo sobre una
matriz de nxm, en donde LCS[i][j]=LCS(i,j)
X
X
Y
G
X
X
Z
Y
F
A
Y
X
Z
Y
F
X
String Distance
• Dados un par de strings A,B, la distancia entre
ellos (Distancia de Levenshtein) se define
como la menor cantidad de cambios que
deben hacérsele a A, para convertirlo en B
• Un cambio puede ser eliminar un carácter,
agregar un carácter o modificarlo
• Por ejemplo, A=“casa” puede convertirse en
B=“azar”, haciendo tres cambios: eliminar ‘c’,
agregar ‘r’, cambiar ‘s’ por ‘z’
String Distance
• Supongamos que n es el tamaño de A, y m el
tamaño de B
• Si n=0: sd(A,B)= m (insertar m caracteres)
• Si m=0: sd(A,B) = n (eliminar m caracteres)
String Distance
• Si A[i]==B[j]:
– A: xxxxxxxC
– B: yyyyyyyC
• sd(Ai, Bj) = sd(Ai-1, Bi-1) ya que los caracteres
son iguales
String Distance
• Si A[i]≠B[j]:
– A: xxxxxxxC
– B: yyyyyyyD
• Opción 1: Cambiar C por D:
• sd(Ai, Bj) = 1+sd(Ai-1, Bi-1)
String Distance
• Si A[i]≠B[j]:
– A: xxxxxxxC
– B: yyyyyyyD
• Opción 2: Eliminar C:
• sd(Ai, Bj) = 1+sd(Ai-1, Bi)
String Distance
• Si A[i]≠B[j]:
– A: xxxxxxxC
– B: yyyyyyyD
• Opción 3: Insertar D en A:
• sd(Ai, Bj) = 1+sd(Ai, Bi-1)
String Distance
• Finalmente, cuando A[i]≠B[j]:
String Distance
PALABRA
A
P
A
L
A
B
R
A
L
U
ALUMBRA
M
B
R
A
Longest Common Sequence
• Dado un conjunto (generalmente de dos)
secuencias, encontrar la subsecuencia más
larga común a todas las secuencias
• Por ejemplo:
• S=5,4,6,7,3,2,7,9,2,6,4,2
• T=8,4,5,6,11,7,9,6,3,14,1,2
• LCS(S,T) = 4,6,7,9,6,2
Longest Common Sequence
• Examinemos que sucede con el último
elemento de la secuencia
• Si el último elemento de Sn es igual al último
elemento de Tm:
• LCS(Sn, Tm) = 1 + LCS(Sn-1, Tm-1)
Longest Common Sequence
• Si el último elemento de Sn es distinto al
último elemento de Tm:
• S = XXXXXXA
• T = YYYYYYB
• Hay dos opciones: tratar de que A coincida
con otro elemento de T, o que B coincida con
otro elementos de S
Longest Common Sequence
• LCS(Sn, Tm) = max(LCS(Sn-1, Tm), LCS(Sn, Tm-1))
A = 4, 8, 2, 9, 1, 7
?
B = 6, 7, 8, 1, 2, 7, 5
Longest Common Sequence
8
5
4
6
7
3
2
7
9
2
6
4
2
4
5
6
11
7
9
6
3
14
1
2
Longest Increasing Sequence
• Dada una secuencia de números enteros, la
secunecia incremental más larga es una
subsecuencia de números que están
ordenados ascendentemente
• La secuencia no es necesariamente contigua
• Por ejemplo, para S =
0,8,4,12,2,10,6,14,1,9,5,13,3,11,7,15
• LIS(S) = 0,2,6,9,13,15 (no es única)
Longest Increasing Sequence
• Definimos qk(S) como la LIS de S que termina
en Sk
• Supongamos que estamos considerando la
posición i de la secuencia
• En cada iteración se debe tomar la decisión
sobre incluir o no Si en la LIS
• Para incluir Si en la LIS es necesario ver a cual
de todas las secuencias anteriores podemos
incluirlo
Longest Increasing Sequence
• Por ejemplo, para S = 4,11,2,9,5,6:
• El 6 puede agregarse a la secuencia que
termina en 4,2 o 5. Pero, ¿a cuál conviene
más?
• Obviamente a la que sea más larga entre estas
Longest Increasing Sequence
• qk(S) = maxi<k(qk-1(S)+1|Si<Sk)
• 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
TSP
• Agente viajero
• Dado un grafo que representa ciudades
conectadas, cuál es el circuito más corto que
conecta a todas las ciudades
TSP
1
2
2
4
3
3
0
2
2
3
4
5
3
4
TSP
• Puede usarse una máscara de bits para marcar
cuáles nodos están en el camino
• Podemos suponer que el circuito siempre
parte de algún nodo (por ejemplo el 0)
• Luego debemos tomar en cuenta cuáles nodos
están en el camino que se ha seleccionado
• Para poder agregar un nuevo nodo al camino
es necesario llevar el control del último nodo
del camino actual
TSP
1
2
2
4
TSP(111111, 0) es la solución
3
3
0
2
2
3
4
5
3
4
TSP(111111,0) = min{
TSP(111110,1)+arco(1,0),
TSP(111110,2)+arco(2,0),
TSP(111110,3)+arco(3,0)}
TSP(111110,2) = min{
TSP(111010,1)+arco(1,2),
TSP(111010,3)+arco(3,2),
TSP(111010,5)+arco(5,2)}
TSP
0
1
3
2
1
2
2
1
3
4
3
3
4