Programación dinámica

Anuncio
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 {
/* calculate common lines */
int commonLines(File a, File b) {
String contentA = read(a); //fixed
String contentB = read(b);
ParsedText pA = parse(contentA);
ParsedText pB = parse(contentB);
int result = LCS.calculate(pA, pB);
return result;
}
}
class Diff {
int commonLines(File a, File b) {
String contentA = read(b);
String contentB = read(b);
String[] pA = split(contentA);
String[] pB = split(contentB);
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
/**
* main class of diff program */
*/
class Diff {
/* calculate common lines */
int commonLines(File a, File b) {
String contentA = read(a); //fixed
String contentB = read(b);
Prefijo 5
class Diff {
int commonLines(File a, File b) {
String contentA = read(b);
String contentB = read(b);
String[] pA = split(contentA);
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
/**
* main class of diff program */
*/
class Diff {
/* calculate common lines */
int commonLines(File a, File b) {
Prefijo 2
class Diff {
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
/**
* main class of diff program */
*/
class Diff {
/* calculate common lines */
int commonLines(File a, File b) {
Prefijo 3
class Diff {
int commonLines(File a, File b) {
String contentA = read(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
LCS2 (i, j, A, B) =

0







1 + LCS2 (i − 1, j − 1, A, B)




LCS2 (i, j − 1, A, B)


 max
LCS2 (i − 1, j, A, B)
i =0∨j =0
A[i − 1] = B[j − 1]
A[i − 1] 6= B[j − 1]
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 − 1, j − 1
i, j
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
Recursión (“dependencia”)
.
..
···
.
.
.
n, m
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
Problemas, algoritmos y programación
sub-soluciones
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








 LEV2 (i − 1, j − 1, A, B)
i =0∨j =0
A[i − 1] = B[j − 1]




 LEV2 (i, j − 1, A, B)




LEV2 (i − 1, j, A, B)
1
+
min



LEV2 (i − 1, j − 1, A, B)
Problemas, algoritmos y programación
A[i − 1] 6= B[j − 1]
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]);
if(j == length(T) - 1) {
T.add(A[i]);
} else {
T[j+1] = A[i];
}
}
}
return length(T);
}
// mayor j tal que T[j] < A[i]
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
P(s, n, d) = di=1 d1 P(s − i, n − 1, d)
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
P
m(S) = i∈S 2i
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
Descargar