Está en la página 1de 41

Programación dinámica

Problemas, algoritmos y programación

31 de Agosto 2011

Problemas, algoritmos y programación Programación dinámica


Problema: diff

El comando diff
diff es un comando de Linux que muestra las diferencias en el
contenido de dos textos (equivalente a fc.exe de Windows/DOS)

¿Cúando se usa?
Se usa, entre otras cosas, para comparar ediciones (versiones) de
un mismo archivo

¿Cómo compara?
El criterio por defecto es buscar lı́neas comunes a ambos archivos.

Problemas, algoritmos y programación Programación dinámica


Problema: diff - Ejemplo

Nueva versión
/** Versión “original”
* main class of diff program */
*/ class Diff {
class Diff { int commonLines(File a, File b) {
/* calculate common lines */ String contentA = read(b);
int commonLines(File a, File b) { String contentB = read(b);
String contentA = read(a); //fixed String[] pA = split(contentA);
String contentB = read(b); String[] pB = split(contentB);
ParsedText pA = parse(contentA); int result = LCS.calculate(pA, pB);
ParsedText pB = parse(contentB); return result;
int result = LCS.calculate(pA, pB); }
return result; }
}
}

Referencia de colores
lı́neas sin cambios lı́neas nuevas lı́neas eliminadas

Problemas, algoritmos y programación Programación dinámica


Problema: diff

Operaciones de moficiación
Sin operación : las lı́neas comunes
Inserción : las lı́neas no comunes del nuevo
Borrado : las lı́neas no comunes del “original”

¿Cuál es el problema?
Para hacer una buena comparación de archivos, hay que tener un
algoritmo que encuentre la mayor cantidad de lı́neas comunes entre
ambos. Además, las lı́neas comunes tienen que aparecer con el
mı́smo orden en ambos archivos.

Problemas, algoritmos y programación Programación dinámica


Problema: diff
Generalización
A este problema se lo conoce como encontrar la sub-secuencia
común de mayor longitud (o Longest Common Subsequence)

Longest Common Subsequence


Entrada: dos sequencias (vectores, arreglos) A y B
Salida: la longitud de la sub-secuencia común a A y a B mas
larga posible
LCS : (T [], T []) → int

Ejemplos
LCS([X AABBDZ ], [AX AC BDY ]) = 4
LCS([], [XXYY ]) = 0
LCS([123456], [123456]) = 6
LCS([♠♠♠♥], [♥♠♠♠]) = 3
Problemas, algoritmos y programación Programación dinámica
Problema: diff - ¿Cómo se puede resolver?

Más problemas
Se calculan las LCS de todos los prefijos de A contra todos los
prefijos de B
Entre estos resultados está la LCS de A y B
Si n y m son el tamaño de A y B, la cantidad de LCS que se
van a calcular son (n + 1)(m + 1)
Cada prefijo de A se puede identificar con su longitud, que es
un número del 0 al n. De la misma forma, cada prefijo de B
se identifica con los números del 0 al m.
LCS2 : (int, int, T [], T []) → int

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede resolver?
Ejemplo con los archivos

Prefijo 8
/** Prefijo 5
* main class of diff program */
class Diff {
*/
int commonLines(File a, File b) {
class Diff {
String contentA = read(b);
/* calculate common lines */
String contentB = read(b);
int commonLines(File a, File b) {
String[] pA = split(contentA);
String contentA = read(a); //fixed
String contentB = read(b);

Algunos valores de LCS2


LCS2 (8, 5, nuevo, original) = 3
LCS2 (4, 1, nuevo, original) = 1
LCS2 (5, 0, nuevo, original) = 0
LCS2 (5, 1, nuevo, original) = 1
LCS2 (6, 2, nuevo, original) = 2
Problemas, algoritmos y programación Programación dinámica
Problema: diff - ¿Cómo se puede resolver?

Caso 1/3 - Base


Alguno de los dos prefijos tiene longitud 0, entonces la LCS es 0
LCS2 (0, , , ) = 0
LCS2 ( , 0, , ) = 0

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede resolver?

Caso 2/3 - Terminan igual


Los dos prefijos terminan con el mismo elemento, entonces hay una
LCS para estos prefijos que termina en este elemento común.

Ejemplo con los archivos

Prefijo 6
/** Prefijo 2
* main class of diff program */
*/ class Diff {
class Diff { int commonLines(File a, File b) {
/* calculate common lines */
int commonLines(File a, File b) {

¿Cuánto vale LCS2 ?


A[i − 1] = B[j − 1] ⇒ LCS2 (i, j, A, B) =
1 + LCS2 (i − 1, j − 1, A, B)

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede resolver?
Caso 3/3 - Terminan distinto
Los dos prefijos terminan con distintos elementos, entonces hay
una LCS para estos prefijos que no usa, al menos, una terminación.

Ejemplo con los archivos

Prefijo 6
/** Prefijo 3
* main class of diff program */
class Diff {
*/
int commonLines(File a, File b) {
class Diff {
String contentA = read(b);
/* calculate common lines */
int commonLines(File a, File b) {

¿Cuánto vale LCS2 ?


A[i − 1] 6= 
B[j − 1] ⇒ LCS2 (i, j, A, B) =
LCS2 (i, j − 1, A, B)
max
LCS2 (i − 1, j, A, B)
Problemas, algoritmos y programación Programación dinámica
Problema: diff - ¿Cómo se puede resolver?

Definición partida de LCS2




 0 i =0∨j =0




1 + LCS2 (i − 1, j − 1, A, B) A[i − 1] = B[j − 1]

LCS2 (i, j, A, B) =

 
LCS2 (i, j − 1, A, B)


 max A[i − 1] 6= B[j − 1]


LCS2 (i − 1, j, A, B)

LCS en base a LCS2


LCS(A, B) = LCS2 ( largo(A), largo(B), A, B )

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede programar?

Primeras consideraciones
Si se dispone de la función LCS2 , la función LCS se programa
muy simplemente
La función LCS2 se puede programar recursivamente
Los argumentos A y B de LCS2 son siempre los mismos
Entonces LCS2 se puede programar como una función
recursiva de dos variables (los prefijos i y j). Los arreglos A y
B se pueden considerar como variables globales
El caso base de la recursión es cuando i = 0 o j = 0

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede programar?

Una cosa más: el “arbol” de llamadas


Para calcular LCS2 (i, j) se
necesita el valor de LCS2 (i − 1, j), LCS2 (i, j − 1) y LCS2 (i − 1, j − 1).
i − 3, j
i − 2, j ···
i − 1, j i − 2, j − 1
i, j i − 1, j − 1 ···
i, j − 1 i − 1, j − 2
i, j − 2 ···
i, j − 3

¡Hay múltiples llamadas iguales!


La función LCS2 recorre todas las sub-sequencias comunes. No hacer la misma llamada dos veces es fundamental
para resolver el problema con una complejidad temporal óptima

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede programar?

Primer programa: recursión “memorizada” (memoization)


T[] A, B;
int mem[][];
int lcs(T[] a, T[] b) {
A = a; B = b;
int n = length(A), m = length(B);
init(mem, n+1, m+1, -1);
return lcs2(n, m);
}
int lcs2(int i, int j) {
if( mem[i][j] == -1 ) {
if( i == 0 || j == 0 ) {
mem[i][j] = 0;
} else if( A[i-1] == B[j-1] ) {
mem[i][j] = 1 + lcs2(i - 1, j - 1);
} else {
mem[i][j] = max(lcs2(i - 1, j), lcs2(i, j - 1));
}
}
return mem[i][j];
}

Observación
Este programa calcula todos los valores de la función LCS2 en la
matriz mem
Problemas, algoritmos y programación Programación dinámica
Problema: diff - ¿Cómo se puede programar?

Recursión vs. Cálculo de valores de la matriz mem


0, 0 0, 1 0, 2 0, 3 0, 4 0, 5 0, 6 ··· 0, m

1, 0 1, 1 1, 2 1, 3 1, 4 1, 5 1, 6 ··· 1, m

2, 0 2, 1 2, 2 2, 3 2, 4 2, 5 2, 6 ··· 2, m

. . . . . . . . .
. . . . . . . .. .
. . . . . . . .

n, 0 n, 1 n, 2 n, 3 n, 4 n, 5 n, 6 ··· n, m

Recursión (“dependencia”) Cálculo de mem


i − 1, j − 1 i − 1, j 0, 0 0, m

i, j − 1 i, j n, 0 n, m

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede programar?

Observación
Se puede eliminar la recursión calculando los valores de mem
directamente. El orden en que se vayan calculando los valores debe
satisfacer todas las “dependencias”.

Segundo programa: sin recursión


int mem[][];
int lcs(T[] A, T[] B) {
int n = length(A), m = length(B);
init(mem, n+1, m+1, -1);
for(i : 0 ... n) {
for(j : 0 ... m) {
if( i == 0 || j == 0 ) {
mem[i][j] = 0;
} else if( A[i-1] == B[j-1] ) {
mem[i][j] = 1 + mem[i - 1][j - 1];
} else {
mem[i][j] = max(mem[i - 1, j], mem[i, j - 1]);
}
}
}
return mem[n][m];
}

Problemas, algoritmos y programación Programación dinámica


Problema: diff - ¿Cómo se puede programar?
Segundo programa: sin recursión
La complejidad temporal es O(n.m)
La complejidad espacial es O(n.m). ¿Se puede hacer mejor?
Se puede hacer en complejidad espacial O(n + m) (óptimo).

Observación: La comparación de elementos


La complejidad temporal O(n.m) asume que la comparación
de elementos es O(1) (constante)
Si son elementos complejos se puede hacer un hashing inicial
de los mismos para facilitar la comparación

Observación: Obtener la sub-sequencia


El algoritmo presentado calcula sólo la longitud, pero siguiendo la
tabla de atrás hacia adelante se puede obtener una sub-sequencia
común de mayor longitud
Problemas, algoritmos y programación Programación dinámica
Programación dinámica

Introducción
“Programación Dinámica” es una técnica para resolver cierto tipo
de problemas (como LCS)

Problemas, algoritmos y programación Programación dinámica


Programación dinámica: Definiciones
Problema
Un problema define un valor buscado (resultado) sobre ciertos
parámetros genéricos (entrada)

Ejemplo: LCS
Entrada: Dos arreglos
Resultado: La longitud de la sub-secuencia común mas larga

Ejemplo: Ordenamiento
Entrada: Un arreglo
Resultado: Un arreglo con los elementos ordenados

Ejemplo: SAT
Entrada: Un sistema de ecuaciones booleanas
Resultado: La satisfacibilidad del sistema
Problemas, algoritmos y programación Programación dinámica
Programación dinámica: Definiciones
Instancia de un problema
Es un problema con parámetros de entrada concretos (no
genéricos). Toda instancia tiene un resultado definido.

Ejemplo: LCS
LCS de “XAABBDZ” y “AXACBDY” → 4
LCS de “123456” y “123456” → 6

Ejemplo: Ordenamiento
Ordernar: [“perro”,“casa”,“gato”] → [“casa”,“gato”,“perro”]
Ordernar: [0, 3, 0, 3, 4, 5, 6] → [0, 0, 3, 3, 4, 5, 6]

Ejemplo: SAT

a ∨ ¬b
¿El sistema es satisfacible? → Sı́
b ∧ (¬a ⇒ b)
Problemas, algoritmos y programación Programación dinámica
Programación dinámica: Definiciones

Problema recursivo
En un problema recurisvo el resultado de una instancia se puede
obtener en base a otros resultados de instancias “mas chicas“ del
mismo problema (sub-problemas / sub-instancias)

Ejemplo: LCS
El resultado se puede obtener combinando resultados de los
prefijos.

Ejemplo: Ordenamiento
El resultado se puede obtener encontrando el menor,
intercambiarlo con el primero y ordenando el resto (Selection
sort)
El resultado se puede obtener ordenando dos mitades del
arreglo y combinando ambos resultados (Merge sort)

Problemas, algoritmos y programación Programación dinámica


Programación dinámica: Aplicaciones
Sub-problemas
La programación dinámica es útil si la cantidad de
sub-problemas es abaracable computacionalmente
Ejemplos: LCS y Ordenamiento
Mal ejemplo: SAT (por algo es NP completo)

Sub-problemas superpuestos
La programación dinámica es útil si distintos sub-problemas se
pueden resolver en base a sub-sub-problemas comunes
Ejemplo: LCS (ası́ se ve en el “arbol de llamadas”)
Mal ejemplo: Merge sort

Principio de optimalidad
La solución de un problema esta formada por soluciones de
sub-problemas (sub-soluciones)
Problemas, algoritmos y programación Programación dinámica
Programación dinámica: Aplicaciones

Principio de optimalidad - Ejemplos


Camino mı́nimo: Si el camino mas corto de A a B pasa por C,
ese camino está formado por un camino mı́nimo de A a C y
otro de C a B
LCS: La solución contiene pedazos que son LCS de sus prefijos

Principio de optimalidad y recursión


Un problema que cumpla con el principio de optimalidad se puede
abaracar como problema recursivo

sub-problemas sub-soluciones

Problemas, algoritmos y programación Programación dinámica


Programación dinámica: Resumen

Problemas
Con sub-problemas
Que cumplan el principio de optimalidad
Superpuestos

Algoritmos
Enfoque recursivo
Inducción / Inducción estructural
Divide & Conquer

Programación
Memoization (función recursiva memorizada)
Llenado de tabla

Problemas, algoritmos y programación Programación dinámica


Problema: Distancia de edicón (Edit Distance)

Problema
Encontrar la menor cantidad de ediciones para llegar de una
palabra a otra.

Operaciones de edición
Eliminación de una letra
Inserción de una letra
Substitución de una letra

Ejemplo: gato → blanco


gato (g → b) bato (+ n) banto (+ l) blanto (t → c) blanco
La distancia de edición es 4

Problemas, algoritmos y programación Programación dinámica


Problema: Distancia de edicón (Edit Distance)

Variantes del problema


Las variantes del problema se dan al cambiar las operaciones
posibles
Solo con eliminación e inserción se reduce a LCS
Eliminación, inserción y substitución: distancia de Levenshtein
Agregando “transposición” de letras: distancia de
Levenshtein-Damerau
Todas esta variantes se pueden resolver usando programación
dinámica

Usos comunes
Corregir errores de tipeo
Detección de fraude
Encontrar variaciones en ADN

Problemas, algoritmos y programación Programación dinámica


Problema: Distancia de edicón (Edit Distance)

Edit distance
Entrada: dos cadenas A y B
Salida: la distancia de Levenshtein entre A y a B
LEV : (String , String ) → int

Una solución con programación dinámica


Enfoque similar al usado en LCS
Se calculan las distancias de todos los prefijos de A a todos
los prefijos de B
LEV2 : (int, int, String , String ) → int

Problemas, algoritmos y programación Programación dinámica


Problema: Distancia de edicón (Edit Distance)

Determinar LEV2 (similar a LCS2 )


Caso base: alguno de los prefijos tiene longitud 0, la distancia
es la longitud del otro
Terminan igual: lo óptimo es que esas letras se
“correspondan” y no se haga ninguna operación al final de A
LEV ( “gato”, “blanco” ) = LEV ( “gat”, “blanc” )
Terminan distinto: se tiene que aplicar alguna operación al
final de A
LEV ( “gat”, “blanc” ) es el mı́nimo de:
(insertar c) 1 + LEV ( “gatc”, “blanc” )
(insertar c) 1 + LEV ( “gat”, “blan” )
(borrar t) 1 + LEV ( “ga”, “blanc” )
(t → c) 1 + LEV ( “gac”, “blanc” )
(t → c) 1 + LEV ( “ga”, “blan” )

Problemas, algoritmos y programación Programación dinámica


Problema: Distancia de edicón (Edit Distance)

Definición partida de LEV2


LEV2 (i, j, A, B) =


 i +j i =0∨j =0




 LEV2 (i − 1, j − 1, A, B) A[i − 1] = B[j − 1]



 LEV2 (i, j − 1, A, B)




1 + min LEV2 (i − 1, j, A, B) A[i − 1] 6= B[j − 1]




LEV2 (i − 1, j − 1, A, B)
 

Problemas, algoritmos y programación Programación dinámica


Problema: Distancia de edicón (Edit Distance)
Posible implementación (sin recursión)
int mem[][];
int lev(T[] A, T[] B) {
int n = length(A), m = length(B);
init(mem, n+1, m+1, -1);
for(i : 0 ... n) {
for(j : 0 ... m) {
if( i == 0 || j == 0 ) {
mem[i][j] = i + j;
} else if( A[i-1] == B[j-1] ) {
mem[i][j] = mem[i - 1][j - 1];
} else {
mem[i][j] = 1 + min(mem[i - 1, j], mem[i, j - 1], mem[i - 1, j - 1]);
}
}
}
return mem[n][m];
}

Observaciones
El orden de llenado satisface las “dependencias”
Complejidad temporal: O(n.m), espacial: O(n.m)
Se puede lograr complejidad espacial O(n + m)
Problemas, algoritmos y programación Programación dinámica
Otros problemas de cadenas
Transformar en palı́ndromo
Problema: Dada una cadena, encontrar la mı́nima cantidad de
ediciones para transformarla en palı́ndromo (capicúa)
Sub-problemas a considerar: todas las sub-cadenas (recortes)
de la original. Complejidad temporal: O(n2 )

Compresión RLE (run-length encoding)


Problema: Dada una cadena, encontrar una compresión RLE
de tamaño mı́nimo. Un ejemplo de compresión RLE de la
cadena “XAABCBCBCAABCBCBCX” es “X2(2A3(BC))X”.
Sub-problemas a considerar: todas las sub-cadenas (recortes)
de la original.
También hay que detectar cuales sub-cadenas son
“repeticiones”. AABCBCBCAABCBCBC → 2(AABCBCBC)
Complejidad temporal: O(n3 )
Problemas, algoritmos y programación Programación dinámica
Problema: Mayor sub-sequencia creciente
Descripción
Dada una sequencia de números, encontrar el máximo largo
de una sub-sequencia con sus elementos en orden
estrictamente creciente
LIS : (int[]) → int

Ejemplos
LIS([0, 8, 4, 12, 2, 10, 6, 14, 6, 9, 5]) → 4
LIS([2, 3, 7, 10, 15]) → 5
LIS([]) → 0
LIS([15, 10, 7, 3, 2]) → 1

Solución O(n2 ) usando LCS


Una sub-sequencia creciente de A es una sub-sequencia
~
~ LIS(A) = LCS(A, A) ~~
común entre A y A ordenada (A).
Problemas, algoritmos y programación Programación dinámica
Problema: Mayor sub-sequencia creciente

Considerando otros sub-problemas y sub-soluciones


De todas las sub-secuencias de un mismo tamaño siempre es
“más útil” la que termina en lo menor posible
Para cada prefijo Ai y cada tamaño j, se busca la terminación
“óptima” de una sub-sequencia creciente. j ∈ [1, LIS(Ai )]
Si Ti [j] son estos valores, entonces Ti es creciente y su
tamaño es LIS(Ai )

Definición recursiva de T [i]


T1 [1] = A[0] (caso base)
Todos los valores de Ti+1 son iguales a los de Ti excepto:
Si A[i] ≤ Ti [1] ⇒ Ti+1 [1] = A[i]
Si A[i] > Ti [LIS(Ai )] ⇒ Ti+1 [LIS(Ai ) + 1] = A[i]
Si Ti [j] < A[i] ≤ Ti [j + 1]] ⇒ Ti+1 [j + 1] = A[i]
El valor que cambia se puede encontrar con búsqueda binaria

Problemas, algoritmos y programación Programación dinámica


Problema: Mayor sub-sequencia creciente
Posible implementación
int lis(int[] A) {
if( length(A) == 0 ) return 0;
int[] T = [ A[0] ];
for(i : 1 ... length(A)) {
if ( A[i] <= T[0] ) {
T[0] = A[i];
} else {
int j = binary_search(T, A[i]); // mayor j tal que T[j] < A[i]
if(j == length(T) - 1) {
T.add(A[i]);
} else {
T[j+1] = A[i];
}
}
}
return length(T);
}

Observaciones
Complejidad temporal: O(n.log (n))
Complejidad espacial: O(n)
Se puede aplicar sobre cualquier conjunto con orden total
Problemas, algoritmos y programación Programación dinámica
Problema: Cálculo de probabilidades

Calcular la problabilidad de:


tirar un dado 6 veces y sumar 20
tirar una moneda 20 veces y sacar 11 caras
Algunos cálculos de probabilidad se pueden plantear recursivamente

Ejemplo
La probabiliad de tirar un dado 6 veces y sumar 20 es la suma de
+ 1/6× la probabilidad de tirar un dado 5 veces y sumar 19
+ 1/6× la probabilidad de tirar un dado 5 veces y sumar 18
+ 1/6× la probabilidad de tirar un dado 5 veces y sumar 17
+ 1/6× la probabilidad de tirar un dado 5 veces y sumar 16
+ 1/6× la probabilidad de tirar un dado 5 veces y sumar 15
+ 1/6× la probabilidad de tirar un dado 5 veces y sumar 14

Problemas, algoritmos y programación Programación dinámica


Problema: Cálculo de probabilidades
Generalización
Los problemas:
tirar un dado 6 veces y sumar 20
tirar una moneda 20 veces y sacar 11 caras
se pueden generalizar a calcular la probabilidad de sumar s tirando
n veces un dado de d caras. P(s, n, d) → [0, 1]

Definición recursiva
P(0, 0, ) = 1 y P( , 0, ) = 0
P(s, n, d) = di=1 d1 P(s − i, n − 1, d)
P

Observaciones
Hay sub-problemas superpuestos
d es siempre el mismo
Complejidad temporal: O(s.n.d) y espacial: O(s.n)
Problemas, algoritmos y programación Programación dinámica
Programación dinámica y juegos
Juegos
Algunos juegos pueden ser analizados con programación dinámica.
En especial aquellos que sean:
por turnos
finitos y sin empate

¿Qué se puede analizar?


Si un estado del juego es ganador o perdedor
Si existe alguna estrategia ganadora
En cuántas jugadas termina

¿Cómo se pueden analizar estos juegos recursivamente?


Los estados terminales del juego son los casos bases
Un estado no terminal es ganador si existe una jugada que
lleva a un estado perdedor (no ganador)
Problemas, algoritmos y programación Programación dinámica
Programación dinámica sobre subconjuntos
Problema: el viajante de comercio
Entrada: un conjunto de n ciudades y los costos cij de viajar
de la ciudad i a la j. 0 ≤ i, j < n
Salida: el costo del itinerario mas barato que visita todas las
ciudades una sola vez
C (int, int[][]) → int

Sub-problemas
El itinerario más barato para todos los sub-conjuntos de ciudades
(sub-grafo inducido) y todas sus terminaciones.
La cantidad de sub-grafos es 2n
La cantidad de sub-problemas es O(2n .n)
C2 (int, int[][], SubConjunto, int) → int
C (n, c) = min0≤i<n {C2 (n, c, S, i)} siendo S el sub-conjunto
de todas las ciudades (impropio)
Problemas, algoritmos y programación Programación dinámica
Programación dinámica sobre subconjuntos

Definición recursiva de C2
C2 ( , , {t}, t) = 0
C2 (n, c, S, t) = mini∈S−{t} {c[i][t] + C2 (n, c, S − {t}, i)}

Manipulación de sub-conjuntos (máscara de bits)


Los sub-conjuntos de un conjunto de n elementos se pueden
asociar, uno a uno, con los números del 0 al 2n
El sub-conjunto S se representa con el número
m(S) = i∈S 2i
P

El número s representa al sub-conjunto


S = {i ∈ [0, n − 1]/biti (s) = 1}
Si T ⊂ S ⇒ m(T ) < m(S)
Si t ∈ S ⇔ (m(S)&2t ) = 2t (& es el “and” de bits)
Si t ∈ S ⇒ m(S − {t}) = m(S) − 2t
Problemas, algoritmos y programación Programación dinámica
Programación dinámica sobre subconjuntos

Nueva definición recursiva de C2


C2 (int, int[][], int, int) → int
C2 ( , , 2t , t) = 0
C2 (n, c, m, t) = minm&2i =2i ∧i6=t {c[i][t] + C2 (n, c, m − 2t , i)}

Dependencias de C2
El valor de C2 (n, c, m(S), t) depende de los valores de C2 para
todos los sub-conjuntos de S
Los sub-conjuntos de S se representan con un número menor
a m(S)
C2 se puede calcular en orden creciente de m(S)

Problemas, algoritmos y programación Programación dinámica


Programación dinámica sobre subconjuntos
Posible implementación
int C(int n, int[][] c) {
int mem[1 << n][n]; // 1 << n = 2^n
for(m : 1 ... (1 << n) ) {
for(t : 0 ... (n - 1) ) {
if(bit_count(m) == 1) {
mem[m][t] = 0;
} else {
mem[m][t] = INF;
for(i : 0 ... (n - 1)) if ( (m & (1 << i) == (1 << i)) && i != t) {
mem[m][t] = min(mem[m][t], mem[m-(1<<t)][i]);
}
}
}
}
int r = INF;
for(i : 0 ... (n - 1)) r = min(r, mem[1<<n - 1][i];
return r;
}

Observaciones
Complejidad temporal: O(2n .n2 ), espacial: O(2n .n)
Es mejor que la fuerza bruta O(n!), para n = 25 es ∼ 1015
veces más rápido y necesita ∼ 1G de memoria.
Problemas, algoritmos y programación Programación dinámica

También podría gustarte