Programação dinâmica é um método para a construção de algoritmos para a resolução de problemas computacionais, em especial os de otimização combinatória. Ela é aplicável a problemas nos quais a solução ótima pode ser computada a partir da solução ótima previamente calculada e memorizada - de forma a evitar recálculo - de outros subproblemas que, sobrepostos, compõem o problema original.
O que um problema de otimização deve ter para que a programação dinâmica seja aplicável são duas principais características: subestrutura ótima e superposição de subproblemas. Um problema apresenta uma subestrutura ótima quando uma solução ótima para o problema contém em seu interior soluções ótimas para subproblemas. A superposição de subproblemas acontece quando um algoritmo recursivo reexamina o mesmo problema muitas vezes.
Problemas de programação dinâmica podem ser abordados de forma top-down ou bottom-up.
Como os próprios nomes sugerem, a abordagem top-down aborda o problema de forma recursiva comum, ou seja, no exemplo do algoritmo de Fibonacci (mais abaixo), começamos pelo n-ésimo número e em, recursivamente, ir calculando os valores de F, F, F,..., F, F. Nesta abordagem, os valores são armazenados em uma tabela e, para a i-ésima iteração, verifica-se se F já foi calculado.
Na abordagem bottom-up, vamos calculando os subproblemas menores e aumentando a complexidade com o passar do tempo, ou seja, para o exemplo de Fibonacci, começaríamos calculando F, depois F, F, e assim por diante até F. Observe que, nesta abordagem, na nós sabemos que, na i-ésima iteração, F já foi resolvido, logo não precisamos verificar toda vez como na top-down. Ao consultar o exemplo 1, do Algoritmo de Fibonacci, isso deve ficar mais claro.
Em geral, ambas as abordagens tem o mesmo tempo assintótico de execução[carece de fontes].
Como exemplo simples de programação dinâmica, com alguma frequência, alguns textos, utilizam a determinação recursiva da Sequência de Fibonacci.
Quando implementada de forma recursiva sem maiores cuidados e sem memorização, a dupla chamada a F na segunda linha causa a necessidade da repetição de cálculos, que faz com que o tempo cresça exponencialmente.
Para implementar em PD, tem-se os seguintes pseudocódigos:
ALGORITMO FIBONACCI-TOPDOWN(n)
Criar vetor F com n posicoes, iniciando em 1
F = 1
F = 2
Para i = 3 até n faça
F = -1
retorne FIBONACCI-RECURSIVO-TOPDOWN(N)
FIM
Esse primeiro irá inicializar as posições 1 e 2 com o valor 1 e o resto com "-1". Esse primeiro algoritmo somente cria e inicializa o vetor, e ao final chama o algoritmo que de fato irá calcular os valores de Fibonacci:
ALGORITMO FIBONACCI-RECURSIVO-TOPDOWN(n)
se F > 0 entao
retorne F
F = FIBONACCI-RECURSIVO-TOPDOWN(n - 1) + FIBONACCI-RECURSIVO-TOPDOWN(n - 2)
retorne F
FIM
Este segundo algoritmo, a princípio, irá ir até à posição 1 e 2 e, então, somar o valor de ambas (1 + 1). Em segunda, o algoritmo recursivo irá adicionar esse valor ao valor da posição 3. Observe que, ao tentar calcular F, o algoritmo irá calcular F e F, mas F e F já foram calculados, e a primeira linha do pseudocódigo verifica se F já existe e, caso seja verdadeiro, irá apenas retornar seu valor. Com isso, o tempo de execução será Θ(n).
Imaginemos o processo de multiplicação de matrizes com dimensões , respectivamente. O problema em essência é trivial, pois se multiplicarmos sequencialmente as matrizes obteremos facilmente o resultado esperado.
É interessante lembrar que a multiplicação de matrizes não é comutativa (em geral, ), mas associativa, o que significa que .
O maior desafio que reside neste problema é, então, otimizar o desempenho do programa que o resolve, ou seja, aproveitar-se da propriedade associativa para encontrar a ordem ótima de se realizar a multiplicação das matrizes, de modo que o número de multiplicações escalares seja mínimo.
Podemos observar que, em geral, multiplicar uma matriz por uma matriz exige operações de multiplicação. As matrizes são multiplicadas aos pares.
Multiplicar com dimensões , , e , respectivamente. Quantas multiplicações são realizadas nessa sequência?
Vamos exemplificar algumas maneiras de realizarmos esta operação:
Organização dos parênteses | Computação do custo | Custo |
Como podemos observar, a ordem da multiplicação faz uma grande diferença no tempo de execução final.
Para resolver este problema, tudo que precisamos é saber qual o melhor índice tal que onde varia de 1 até (n−1).
Note que esse problema foi decomposto em dois subproblemas. Mais precisamente defina como o número mínimo de multiplicações (ou o custo mínimo) para realizar o produto .
Portanto,
onde varia de i até (j -1), fornece o custo mínimo de multiplicações. Daí, note que:
Esta expressão constitui uma recorrência típica do método de programação dinâmica. Ela sugere um algoritmo recursivo, mas, uma implementação “bottom up” (de baixo para cima) é mais eficiente. Um algoritmo nesta fórmula tem os seguintes passos iterativos:
Assim, vamos aplicar o algoritmo acima para resolver o exemplo dado anteriormente. Observe que devemos calcular o custo mínimo na ordem crescente da diferença entre e . Então, no primeiro passo iterativo, a diferença entre e é zero, pois, , o que implica:
No segundo passo, temos i - j = 1, logo,
No terceiro passo, j - i = 2 e com isso, o varia de 1 a 2. Utilizando a fórmula recursiva:
Para variando de 2 a 3 temos:
No último passo, j − i = 3 e com isso, o .
Por fim, a solução ótima:
Tentar todas as ordens possíveis de multiplicações para avaliar o produto de matrizes é um processo ordem exponencial em .
Usando programação dinâmica é possível resolver este problema na ordem polinomial, ou seja .
package Classes;
/**
*
*
*/
public class Matriz {
/*Atributos da classe*/
/*string: atributo que recebe os dados de saída de printOptmalParents()
* para poder exibir o resultado da parentização. */
private static String string;
/*m: atributo que recebe os valores das multiplicações feitas para o melhor custo*/
private int m;
/*s: atributo que recebe o valor das posições de melhor multiplicação*/
private int s;
/*linhas: recebe o valor total das linhas das matrizes.*/
private int linhas;
/*colunas: recebe o valor total das colunas das matrizes*/
private int colunas;
/*inicoMatriz: atributo tipo flag, para marcar a inicialização de matrizes
* no método recursiveMatrixChain(int p, int i, int j)*/
private boolean inicioMatriz;
/*Métodos geters e setters para entrada e saída de dados nos atributos*/
public int getM() {
return m;
}
public void setM(int m) {
this.m = m;
}
public int getS() {
return s;
}
public void setS(int s) {
this.s = s;
}
public int getLinhas() {
return linhas;
}
public void setLinhas(int linhas) {
this.linhas = linhas;
}
public int getColunas() {
return colunas;
}
public void setColunas(int colunas) {
this.colunas = colunas;
}
public Matriz() {
string = "";
}
/*Métodos da Classe*/
/* Método para calcular o melhor custo.
*Parametros: p é um vetor com as posições das matrizes.
*Retorno: int.*/
public static int MatrixChainOrder(int p) {
int retorno = new int;
try {
int i = 0; //linhas
int j = 0; //colunas
int k = 0;
int q = 0;
int infinito = Integer.MAX_VALUE; // tipo infinito positivo (para simular o infinito)
int n = p.length - 1;
int m = new int; // ixj
int s = new int;
for (i = 0; i < n; i++) {
m = 0;
}
for (int l = 1; l < n; l++) {
for (i = 0; i < n - l; i++) {
j = i + l;
m = infinito;
for (k = i; k < j; k++) {
q = m + m + p * p * p;
if (q < m) {
m = q;
s = k + 1;
}
}
}
}
retorno = s;
} catch (Exception e) {
System.out.println("Erro: " + e);
e.printStackTrace();
}
return retorno;
}
/*Método para alocar os parênteses de uma forma ótima para a multiplicação.
*Parâmetros: s é a matriz que contém as posições de melhor multiplicação;
* i é o índice inicial;
* j é o índice final.
*Retorno: String.*/
public String printOptmalParents(int s, int i, int j) {
if (i == j) {
this.string += "A" + (i + 1) + " ";
} else {
this.string += " ( ";
printOptmalParents(s, i, s - 1);
printOptmalParents(s, s, j);
this.string += " ) ";
}
return this.string;
}
/*Método para inicializar os atributos da classe que serão utilizados em métodos.
*Parâmetros: p é um vetor com as posições das matrizes.
*Retorno: não há.*/
public void inicializaRecursiveMatrixChain(int p) {
int n = p.length - 1;
this.m = new int; // ixj
this.s = new int;
this.inicioMatriz = true;
}
/*Método para calcular o melhor custo; porém com chamadas recursivas.
*Parâmetros: p é um vetor com as posições das matrizes;
* i é o índice inicial;
* j é o índice final.
*Retorno: int.*/
public int recursiveMatrixChain(int p, int i, int j) {
int retorno = 0;
if (this.inicioMatriz) {
if (i == j) {
retorno = 0;
} else {
this.m = Integer.MAX_VALUE;
for (int k = i; k <= j - 1; k++) {
int q = recursiveMatrixChain(p, i, k) + recursiveMatrixChain(p, k + 1, j) + p * p * p;
if (q < this.m) {
this.m = q;
this.s = k + 1;
}
}
retorno = this.m;
}
}
return retorno;
}
}