Programaç˜ao Dinâmica Programaç˜ao Dinâmica

Propaganda
Programação Dinâmica
Fernando Lobo
Algoritmos e Estrutura de Dados II
1 / 18
Programação Dinâmica
I
Outra técnica de concepção de algoritmos, tal como Divisão e
Conquista ou Estratégias Greedy.
I
O termo Programação Dinâmica é um bocado infeliz.
I
I
Programação ⇒ sugere programação de computadores.
I
Dinâmica ⇒ sugere valores que mudam ao longo do tempo.
A técnica de Programação Dinâmica não tem que ver com
uma coisa nem outra!
2 / 18
O que é então a Programação Dinâmica?
I
É uma técnica de resolução de problemas.
I
A ideia é resolver subproblemas pequenos e armazenar os
resultados.
I
Esses resultados são depois utilizados para resolver
subproblemas maiores (e armazenando novamente os
resultados).
I
E assim sucessivamente até se resolver o problema completo.
3 / 18
Comparação com Divisão e Conquista
Semelhanças
I
Para resolver um problema combinamos as soluções de
subproblemas.
Diferenças
I
Divisão e Conquista é eficiente quando os subproblemas são
todos distintos.
I
Se tivermos que resolver várias vezes o mesmo subproblema, a
Divisão e Conquista torna-se ineficiente.
I
Com Programação Dinâmica cada subproblema é resolvido
apenas uma vez.
4 / 18
Exemplos de Programação Dinâmica
I
A melhor maneira de aprender Programação Dinâmica é ver
alguns exemplos.
I
Exemplo simples: Calcular o n-ésimo número da sequência de
Fibonacci.
Fn

, se n = 0
 0
1
, se n = 1
=

Fn−1 + Fn−2 , se n > 1
5 / 18
Pseudocódigo
Fib-Rec(n)
if n == 0
return 0
if n == 1
return 1
return Fib-Rec(n − 1) + Fib-Rec(n − 2)
Este algoritmo é muito mau. Porquê?
6 / 18
Vejamos o que acontece com n = 5
7 / 18
Fibonacci: Algoritmo de Divisão e Conquista
I
Estamos a calcular a mesma coisa várias vezes!
I
Pode-se provar que Fn+1 /Fn ≈
=⇒ Fn > 1.6n
I
Qual a complexidade do algoritmo?
√
1+ 5
2
≈ 1.62
I
Fn resulta da soma das folhas da árvores.
I
Fn > 1.6n =⇒ árvore tem pelo menos 1.6n folhas.
I
Logo, o algoritmo tem complexidade Ω(1.6n ), o que é muito
mau.
I
Experimentem programá-lo e usar n = 50.
8 / 18
Fibonacci: Algoritmo de Divisão e Conquista
I
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
I
É trabalho desnecessário. Só deverı́amos calcular cada Fi uma
e uma só vez.
I
Podemos fazê-lo usando Programação Dinâmica.
9 / 18
Fibonacci: Algoritmo de Programação Dinâmica
I
A ideia é resolver o problema de baixo para cima, começando
pelos casos base e armazenando as soluções dos subproblemas.
Fib-PD(n)
F [0] = 0
F [1] = 1
for i = 2 to n
F [i] = F [i − 1] + F [i − 2]
return F [n]
I
Complexidade temporal? Θ(n).
I
Complexidade espacial? Θ(n).
10 / 18
Fibonacci: Algoritmo de Programação Dinâmica
I
Para calcular Fi basta ter armazenado as soluções dos dois
subproblemas Fi−1 e Fi−2 . Logo, podemos reduzir a
complexidade espacial de Θ(n) para Θ(1).
Fib-PD-v2(n)
if n == 0
return 0
if n == 1
return 1
back2 = 0
back1 = 1
for i = 2 to n
next = back1 + back2
back2 = back1
back1 = next
return next
11 / 18
Outro exemplo: Coeficientes binomiais
I
I
Ckn =
n
k
n
k
=
n!
k!(n−k)!
é o número de combinações de n elementos k a k.
I
Por palavras mais simples: número de maneiras distintas de
escolher grupos de k elementos a partir de um conjunto de n
elementos.
I
Exemplo: Dado um conjunto de 10 alunos, quantos
grupos
10!
distintos de 3 alunos se podem fazer? Resp: 10
3 = 3!7! = 120
I
A aplicação directa da fórmula pode facilmente dar um
overflow aritmético por causa dos factoriais, mesmo que o
resultado final caiba perfeitamente num inteiro.
12 / 18
Coeficientes binomiais (cont.)
I
I
I
Podemos definir
n
k
=
n−1
k−1
+
n
k
de modo recursivo.
n−1
k
I
1a parcela: k-ésimo elemento pertence ao grupo
⇒ é necessário escolher k − 1 dos restantes n − 1 elementos.
I
2a parcela: k-ésimo elemento não pertence ao grupo
⇒ é necessário escolher k dos restantes n − 1 elementos.
Casos base: k = 0, n = k ⇒
n
k
=1
13 / 18
Algoritmo naive (de força bruta)
Comb(n, k)
if k == 0 or n == k
return 1
else
return Comb(n − 1, k − 1) + Comb(n − 1, k)
14 / 18
Solução com Programação Dinâmica
Comb-PD(n, k)
for i = 0 to n
for j = 0 to min(i, k)
if j == 0 or j == i
A[i, j] = 1
else A[i, j] = A[i − 1, j − 1] + A[i − 1, j]
return A[n, k]
15 / 18
Exemplo de execução: Comb-PD(5,3)
j
i
0
1
2
3
4
5
0
1
2
3
1
1
1
1
1
1
1
2
3
4
5
1
3
6
10
1
4
10
16 / 18
Memoization
I
É uma técnica semelhante à Programação Dinâmica.
I
Mantém o algoritmo recursivo na forma top-down.
I
A ideia é inicializar as soluções dos subproblemas com o valor
“Desconhecido”.
I
Depois, quando queremos resolver um subproblema,
verificamos primeiro se já foi resolvido.
I
I
Se sim, retornamos a solução previamente armazenada.
I
Se não, resolvemos o subproblema e armazenamos a solução.
Cada subproblema só é resolvido uma vez.
17 / 18
Versão memoized de Comb
Comb-Memoized(n, k)
for i = 0 to n
for j = 0 to min(i, k)
C [i, j] = unknown
return M-Comb(n, k)
M-Comb(i, j)
if C [i, j] == unknown
if j == 0 or j == i
C [i, j] = 1
else C [i, j] = M-Comb(i − 1, j − 1) + M-Comb(i − 1, j)
return C [i, j]
18 / 18
Download