programa˘c~ao din^amicafibonacci: algoritmo de programa˘c~ao din^amica para calcular f i basta ter...
TRANSCRIPT
Programacao Dinamica
Fernando Lobo
Algoritmos e Estrutura de Dados
1 / 56
Programacao Dinamica
Outra tecnica de concepcao de algoritmos, tal como Divisao eConquista.
O termo Programacao Dinamica e um bocado infeliz.
I Programacao ⇒ sugere programacao de computadores.
I Dinamica ⇒ sugere valores que mudam ao longo do tempo.
A tecnica de Programacao Dinamica nao tem que ver com uma coisanem outra.
2 / 56
O que e entao a Programacao Dinamica?
E uma tecnica de resolucao de problemas.
A ideia e resolver subproblemas pequenos e armazenar os resultados.
Esses resultados sao depois utilizados para resolver subproblemasmaiores (e armazenando novamente os resultados).
E assim sucessivamente ate se resolver o problema completo.
3 / 56
Comparacao com Divisao e Conquista
Semelhancas
Para resolver um problema combinamos as solucoes de subproblemas.
Diferencas
Divisao e Conquista e eficiente quando os subproblemas sao todosdistintos.
Se tivermos que resolver varias vezes o mesmo subproblema, aDivisao e Conquista torna-se ineficiente.
Com Programacao Dinamica cada subproblema e resolvido apenasuma vez.
4 / 56
Exemplos de Programacao Dinamica
A melhor maneira de aprender Programacao Dinamica e ver algunsexemplos.
Exemplo simples: Calcular o n-esimo numero da sequencia deFibonacci.
Fn =
0 , se n = 01 , se n = 1Fn−1 + Fn−2 , se n > 1
5 / 56
Pseudocodigo
Fib-Rec(n)
if n == 0return 0
if n == 1return 1
return Fib-Rec(n − 1) + Fib-Rec(n − 2)
Este algoritmo e muito mau. Porque?
6 / 56
Vejamos o que acontece com n = 5
7 / 56
Fibonacci: Algoritmo de Divisao e Conquista
Estamos a calcular a mesma coisa varias vezes!
Pode-se provar que Fn+1/Fn ≈ 1+√5
2 ≈ 1.62=⇒ Fn > 1.6n
Qual a complexidade do algoritmo?
I Fn resulta da soma das folhas da arvores.
I Fn > 1.6n =⇒ arvore tem pelo menos 1.6n folhas.
Logo, o algoritmo tem complexidade Ω(1.6n), o que e muito mau.
Experimentem programa-lo e usar n = 50.
8 / 56
Fibonacci: Algoritmo de Divisao e Conquista
No exemplo com n = 5, calculamos:
I F4 → 1 vez
I F3 → 2 vezes
I F2 → 3 vezes
I F1 → 5 vezes
I F0 → 3 vezes
E trabalho desnecessario. So deverıamos calcular cada Fi uma e umaso vez.
Podemos faze-lo usando Programacao Dinamica.
9 / 56
Fibonacci: Algoritmo de Programacao Dinamica
A ideia e resolver o problema de baixo para cima, comecando peloscasos base e armazenando as solucoes dos subproblemas.
Fib-PD(n)
F [0] = 0F [1] = 1for i = 2 to n
F [i ] = F [i − 1] + F [i − 2]return F [n]
Complexidade temporal? Θ(n).
Complexidade espacial? Θ(n).
10 / 56
Fibonacci: Algoritmo de Programacao Dinamica
Para calcular Fi basta ter armazenado as solucoes dos doissubproblemas Fi−1 e Fi−2. Logo, podemos reduzir a complexidadeespacial de Θ(n) para Θ(1).
Fib-PD-v2(n)
if n == 0return 0
if n == 1return 1
back2 = 0back1 = 1for i = 2 to n
next = back1 + back2back2 = back1back1 = next
return next
11 / 56
Outro exemplo: Coeficientes binomiais
Cnk =
(nk
)= n!
k!(n−k)!(nk
)e o numero de combinacoes de n elementos k a k .
Por palavras mais simples: numero de maneiras distintas de escolhergrupos de k elementos a partir de um conjunto de n elementos.
Exemplo: Dado um conjunto de 10 alunos, quantos grupos distintosde 3 alunos se podem fazer? Resp:
(103
)= 10!
3!7! = 120
A aplicacao directa da formula pode facilmente dar um overflowaritmetico por causa dos factoriais, mesmo que o resultado final caibaperfeitamente num inteiro.
12 / 56
Coeficientes binomiais (cont.)
Podemos definir(nk
)de modo recursivo.(n
k
)=(n−1k−1)
+(n−1
k
)I 1a parcela: k-esimo elemento pertence ao grupo⇒ e necessario escolher k − 1 dos restantes n − 1 elementos.
I 2a parcela: k-esimo elemento nao pertence ao grupo⇒ e necessario escolher k dos restantes n − 1 elementos.
Casos base: k = 0, n = k ⇒(nk
)= 1
13 / 56
Algoritmo naive (de forca bruta)
Comb(n, k)
if k == 0 or n == kreturn 1
elsereturn Comb(n − 1, k − 1) + Comb(n − 1, k)
14 / 56
Solucao com Programacao Dinamica
Comb-PD(n, k)
for i = 0 to nfor j = 0 to min(i , k)
if j == 0 or j == iA[i , j ] = 1
else A[i , j ] = A[i − 1, j − 1] + A[i − 1, j ]return A[n, k]
15 / 56
Exemplo de execucao: Comb-PD(5,3)
ij
0 1 2 3
0 11 1 12 1 2 13 1 3 3 14 1 4 6 45 1 5 10 10
16 / 56
Memoization
E uma tecnica semelhante a Programacao Dinamica.
Mantem o algoritmo recursivo na forma top-down.
A ideia e inicializar as solucoes dos subproblemas com o valor“Desconhecido”.
Depois, quando queremos resolver um subproblema, verificamosprimeiro se ja foi resolvido.
I Se sim, retornamos a solucao previamente armazenada.
I Se nao, resolvemos o subproblema e armazenamos a solucao.
Cada subproblema so e resolvido uma vez.
17 / 56
Versao memoized de Comb
Comb-Memoized(n, k)
for i = 0 to nfor j = 0 to min(i , k)
C [i , j ] = unknownreturn M-Comb(n, k)
M-Comb(i , j)
if C [i , j ] == unknownif j == 0 or j == i
C [i , j ] = 1else C [i , j ] = M-Comb(i − 1, j − 1) + M-Comb(i − 1, j)
return C [i , j ]
18 / 56
Outro exemplo: cortar tubos (rod cutting)
Problema: Dado um tubo de comprimento n e uma tabela com precospi para pedacos de tubo de comprimento i (i = 1, 2, . . n), determinaro valor maximo de receita rn que se pode obter se podermos cortar otubo em pedacos e vende-los separadamente, assumindo que os cortessao gratuitos e os comprimentos dos pedacos de tubo sao numerosinteiros.
Exemplo: n = 4
length i 1 2 3 4
price pi 1 5 8 9
19 / 56
8 maneiras de cortar
20 / 56
Melhor solucao: cortar em 2 pedacos de comprimento 2. Receita totale 5 + 5 = 10.
Para cada i = 1 . . n − 1, cortamos ou nao cortamos =⇒ 2n−1
maneiras de cortar o tubo.
21 / 56
Sub-estrutura optima
Vamos tentar definir a solucao optima em termos de solucoes optimasde subproblemas.
Seja ri a receita maxima para um tubo de comprimento i .
Entao rn sera o maximo de:I pnI r1 + rn−1
I r2 + rn−2
I . . .I rn−1 + r1
22 / 56
Uma decomposicao mais simples
Toda a solucao optima tem um pedaco “mais a esq.”(potencialmentede comprimento n no caso de nao haver qualquer corte).
A receita total vai ser o custo desse pedaco mais o custo da melhorreceita que se consegue obter com cortes no pedaco de tubo quesobrar.
rn e o maximo de:I p1 + rn−1
I p2 + rn−2
I . . .I pn + r0
23 / 56
Pseudocodigo
Cut-Rod(p, n)
if n == 0return 0
q = −∞for i = 1 to n
q = max(q, p[i ] + Cut-Rod(p, n − i))return q
E muito ineficiente, tal como nos algoritmos de forca-bruta para osnumeros de Fibonacci e para as Combinacoes.
24 / 56
Arvore de chamadas recursivas de Cut-Rod com n = 4Calcula o mesmo subproblema vezes sem fim.
Complexidade: Θ(2n).
25 / 56
Abordagem com programacao dinamica
Resolver cada subproblema apenas uma vez, e armazenar o resultadopara uso futuro.
Fazer uma abordagem bottom-up: resolver primeiro os subproblemasmais pequenos.
Quando necessitamos de resolver um subproblema maior, usamos osresultados (ja calculados) dos subproblemas mais pequenos.
26 / 56
Algoritmo de programacao dinamica
Bottom-Up-Cut-Rod(p, n)
Let r [0 . . n] be a new arrayr [0] = 0for j = 1 to n
q = −∞for i = 1 to j
q = max(q, p[i ] + r [j − i ])r [j ] = q
return r [n]
27 / 56
28 / 56
Complexidade temporal
Dois ciclos encadeados que dependem linearmente de n, e tempoconstante em cada iteracao. Complexidade temporal e Θ(n2).
Passamos de tempo exponencial para tempo polinomial.
29 / 56
Reconstrucao da solucaoO algoritmo retorna o valor da solucao optima, mas nao a solucao.
Podemos obter a solucao com a seguinte modificacao: s[i ] guarda otamanho do pedaco de tubo mais a esquerda da solucao optima deum problema rod-cut de comprimento i .
Extended-Bottom-Up-Cut-Rod(p, n)
Let r [0 . . n] and s[0 . . n] be new arraysr [0] = s[0] = 0for j = 1 to n
q = −∞for i = 1 to jif p[i ] + r [j − i ] > q
q = p[i ] + r [j − i ]s[j ] = i
r [j ] = qreturn r and s
30 / 56
Reconstrucao da solucao
i 0 1 2 3 4
r [i ] 0 1 5 8 10s[i ] 0 1 2 3 2
Print-Cut-Rod-Solution(p, n)
(r , s) = Extended-Bottom-Up-Cut-Rod(p, n)while n > 0
print s[n]n = n − s[n]
31 / 56
Memoization
Uma tecnica semelhante a Programacao Dinamica.
Mantem o algoritmo na forma recursiva (top-down).
A ideia e usar uma flag para indicar se um subproblema ja estaresolvido.
Depois, para resolver um subproblema temos de primeiro verificar seja foi resolvido.
I Se sim, basta retornar a solucao previamente armazenada.
I Se nao, resolvemos o subproblema e guardamos a solucao para usofuturo.
Cada subproblema so e resolvido uma vez.
32 / 56
Verao memoized de Cut-Rod
Memoized-Cut-Rod(p, n)
Let r [0 . . n] be a new arrayfor i = 0 to n
r [i ] = −∞return Memoized-Cut-Rod-Aux(p, n, r)
Memoized-Cut-Rod-Aux(p, n, r)
if r [n] ≥ 0return r [n]
if n == 0q = 0
else q = −∞for i = 1 to n
q = max(q, p[i ] + Memoized-Cut-Rod-Aux(p, n − i , r))r [n] = qreturn q
33 / 56
Complexidade temporal
Cada subproblema so e resolvido uma vez.
Os subproblems tem tamanho 0, 1, . . . , n, e requerem um ciclo forsobre o seu tamanho
Complexidade tambem e Θ(n2).
34 / 56
Outro exemplo: Longest Common Subsequence (LCS)
Dadas duas sequencias, X = x1x2 . . . xm e Y = y1y2 . . . yn, encontraruma subsequencia comum a X e Y que seja o mais longa possıvel.
Exemplo:
I X = n o c t u r n o
I Y = m o s q u i t e i r o
I LCS(X ,Y ) = o t r o (tambem podia ser o u r o)
35 / 56
Algoritmo de forca bruta
Gerar todas as subsequencias de X e verificar se tambem esubsequencia de Y , e ir guardando a subsequencia mais longa vistaate ao momento.
Complexidade?
I Θ(2m)→ para gerar todas as subsequencias de X .
I Θ(n)→ para verificar se uma subsequencia de X e subsequencia de Y .
I Total: Θ(n 2m)
I E exponencial. Muito mau!
36 / 56
Sera que podemos aplicar Programacao Dinamica?
Se sim teremos de conseguir definir o problema recursivamente emtermos de subproblemas.
O no de subproblemas tem de ser relativamente pequeno (polinomialem n e m) para que a Programacao Dinamica seja util.
Depois de definir o problema em termos de subproblemas, podemosresolver o problema de baixo para cima, comecando pelos casos basee armazenando as solucoes dos subproblemas.
37 / 56
Subestrutura optima
Vamos olhar para prefixos de X e Y .
Seja Xi o prefixo dos i primeiros elementos de X .
Exemplo: X = n o c t u r n o
I X4 = n o c t
I X0 = ∅I X3 = n o c
I X8 = n o c t u r n o
38 / 56
Subestrutura optima
Seja X = x1x2 . . . xm e Y = y1y2 . . . yn.
Seja Z = z1z2 . . . zk uma LCS entre X e Y .
Tres casos:
1 Se xm = yn, entao zk = xm = yn e Zk−1 e uma LCS entre Xm−1 e Yn−1.
2 Se xm 6= yn e zk 6= xm, entao Z e uma LCS entre Xm−1 e Yn.
3 Se xm 6= yn e zk 6= yn, entao Z e uma LCS entre Xm e Yn−1.
39 / 56
Demonstracao do caso 1
Caso 1: Se xm = yn, entao zk = xm = yn e Zk−1 e uma LCS entre Xm−1 eYn−1.
Teremos de provar que zk = xm = yn. Suponhamos que tal nao everdade. Entao a subsequencia Z ′ = z1z2 . . . zkxm e umasubsequencia comum a X e Y e tem comprimento k + 1.
I ⇒ Contradiz o facto de Z ser uma LCS entre X e Y .
40 / 56
Demonstracao do caso 1 (cont.)
Agora temos de provar que Zk−1 e uma LCS entre Xm−1 e Yn−1.Suponhamos que existe uma subsequencia W comum a Xm−1 e Yn−1que e mais longa que Zk−1.
I ⇒ comprimento de W ≥ k.
A subsequencia W ′ = W || xm e comum a X e Y e tem comprimento≥ k + 1.
I Contradiz o facto de Z ser uma LCS entre X e Y .
41 / 56
Demonstracao dos casos 2 e 3
Caso 2: Se xm 6= yn e zk 6= xm, entao Z e uma LCS entre Xm−1 e Yn.
Suponhamos que existe uma subsequencia W comum a Xm−1 e Yn
com comprimento > k . Entao W e uma subsequencia comum entreX e Y .
I =⇒ Contradiz o facto de Z ser uma LCS entre X e Y .
Caso 3: Se xm 6= yn e zk 6= yn, entao Z e uma LCS entre Xm e Yn−1.
A demonstracao do caso 3 e analoga a do caso 2.
42 / 56
Resumindo
Podemos definir LCS(Xm,Yn) em termos de subproblemas.
LCS(Xm,Yn) =
∅ , se m = 0 ou n = 0
LCS(Xm−1,Yn−1) || xm , se xm = yn
LCS(Xm−1,Yn) ouLCS(Xm,Yn−1) , se xm 6= yn
43 / 56
Comprimento de LCS(X ,Y )
Vamos tentar primeiro resolver um problema mais simples: Obter|LCS(X ,Y )| → o comprimento de LCS(X ,Y )
Seja c[i , j ] = |LCS(Xi ,Yj)|
Queremos obter c[m, n]
44 / 56
Definicao recursiva de c[i , j ]
c[i , j ] =
0 , se i = 0 ou j = 0
c[i − 1, j − 1] + 1 , se i , j > 0 e xi = yj
max(c[i − 1, j ], c[i , j − 1]) , se i , j > 0 e xi 6= yj
45 / 56
Algortimo recursivo
LCS-Length-Rec(X ,Y , i , j)
if i == 0 or j == 0return 0
elseif X [i ] == Y [j ]return LCS-Length-Rec(X ,Y , i − 1, j − 1) + 1
else a = LCS-Length-Rec(X ,Y , i − 1, j)b = LCS-Length-Rec(X ,Y , i , j − 1)return max(a, b)
Chamada inicial: LCS-Length-Rec(X ,Y ,m, n)
Tal como em Fib-Rec e Comb-Rec, a arvore da origem a muitossubproblemas repetidos.
O algoritmo e exponencial. Mas o numero de subproblemas distintos= m · n.
46 / 56
Podemos usar Programacao Dinamica
LCS-Length-DP(X ,Y )
m = X . lengthn = Y . lengthfor i = 1 to m
c[i , 0] = 0for j = 0 to n
c[0, j ] = 0for i = 1 to m
for j = 1 to nif X [i ] == Y [j ]
c[i , j ] = c[i − 1, j − 1] + 1elseif c[i − 1, j ] ≥ c[i , j − 1]
c[i , j ] = c[i − 1, j ]else c[i , j ] = c[i , j − 1]
return c[m, n]
47 / 56
Demo
c[i , j ] e preenchida linha a linha, da esquerda para a direita.
48 / 56
Como obter a LCS propriamente dita?
O nosso algoritmo apenas obteve o comprimento da LCS.
A ideia e alterar o codigo de LCS-Length-DP e, de cada vez queobtemos um c[i , j ], registamos como e que ele foi obtido.
Isso permite-nos reconstruir a solucao.
49 / 56
Aqui vai o codigo alterado
LCS-Length-DP-v2(X ,Y )
...for i = 1 to m
for j = 1 to nif X [i ] == Y [j ]
c[i , j ] = c[i − 1, j − 1] + 1b[i , j ] = “ ”
elseif c[i − 1, j ] ≥ c[i , j − 1]c[i , j ] = c[i − 1, j ]b[i , j ] = “ ↑ ”
else c[i , j ] = c[i , j − 1]b[i , j ] = “← ”
return c[m, n] , b
50 / 56
Demo
As setas ↑, ← e sao armazenadas em b[i , j ].b[i , j ] indica o subproblema escolhido para obter c[i , j ].
51 / 56
Uma vez tendo a informacao em b, podemos obter uma LCS entre Xe Y .
A chamada inicial e Print-LCS(b,X ,m, n)
Print-LCS(b,X , i , j)
if i == 0 or j == 0return // Nao faz nada
if b[i , j ] == “ ”Print-LCS(b,X , i − 1, j − 1)print X [i ]
elseif b[i , j ] == “ ↑ ”Print-LCS(b,X , i − 1, j)
elsePrint-LCS(b,X , i , j − 1)
52 / 56
Complexidade
A complexidade e Θ(m · n)
A Programacao Dinamica reduziu a complexidade de exponencialpara polinomial.
No livro tem mais exemplos de problemas resolvidos comProgramacao Dinamica.
53 / 56
Versao memoized de LCS-Length
LCS-Length-Memoized(X ,Y )
m = X . lengthn = Y . lengthfor i = 0 to m
for j = 0 to nc[i , j ] = unknown
return M-LCS-Length(X ,Y ,m, n)
54 / 56
M-LCS-Length(X ,Y , i , j)
if c[i , j ] == unknownif i == 0 or j == 0
c[i , j ] = 0elseif X [i ] == Y [j ]
c[i , j ] = M-LCS-Length(X ,Y , i − 1, j − 1) + 1else a = M-LCS-Length(X ,Y , i − 1, j)
b = M-LCS-Length(X ,Y , i , j − 1)c[i , j ] = max(a, b)
return c[i , j ]
55 / 56
Como aplicar a Programacao Dinamica?
Para aplicarmos Programacao Dinamica ou Memoization para resolver umproblema, temos de fazer 4 coisas:
1 Caracterizar a estrutura de uma solucao optima.
2 Definir o valor da solucao optima recursivamente em termos desubsolucoes optimas.
3 Calcular o valor de uma solucao optima de baixo para cima (no casode P.D.) ou de cima para baixo (no caso de Memoization).
4 Obter a solucao optima atraves da informacao calculada earmazenada no passo 3.
56 / 56