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