Técnicas de Otimização 1) Definição: buscam minimizar ou maximizar uma função através da escolha dos valores de variáveis reais ou inteiras dentro de um conjunto possível visando encontrar uma solução ótima para um dado problema. Subestrutura ótima: um problema apresenta subestrutura ótima se uma solução ótima para o problema contém dentro dela soluções ótimas para subproblemas. Propriedade chave para se avaliar a possibilidade de aplicação de programação dinâmica e algoritmos gulosos. a) Algoritmos Gulosos: (propriedade de escolha gulosa): uma solução globalmente ótima pode ser alcançada fazendo-se uma escolha localmente ótima (gulosa). Em outras palavras, quando estamos considerando que escolha fazer, efetuamos a escolha que parece melhor no problema atual, sem considerar resultados de subproblemas. Estes algoritmos são eficientes em alguns problemas de otimização. b) Programação Dinâmica: diferem dos Algoritmos Gulosos, pois a escolha em cada passo pode depender das soluções de subproblemas. A programação dinâmica permite transformar recursão em iteração armazenando soluções de subproblemas em uma tabela. 2) Exemplo: Soluções para o problema da mochila Descrição: temos que carregar uma mochila de capacidade limitada com diversos produtos de pesos e valores diferentes. Como devemos fazer para carregar o maior valor possível em produtos, respeitando a capacidade da mochila? Este problema apresenta duas versões, uma contínua e outra booleana. Enquanto na contínua consideramos ser possível carregar frações dos produtos, na booleana carregamos apenas o produto inteiro. Este problema é um dos 21 problemas NP-completos de Richard Karp, descritas no seu artigo de 1972. 2.1) Formulação: Considere: n: quantidade de produtos W: capacidade da mochila v[n]: vetor contendo o valor dos produtos w[n]: vetor contendo o peso dos produtos x[n]: vetor resultante, ou mochila resultante. x.w : produto escalar x[1]w[1] + . . . + x[n]w[n]. Assim, uma mochila é qualquer vetor x[1..n] tal que x·w W e 0 x[i] 1 para todo i. O valor de uma mochila x é obtido pelo produto escalar x·v. Na versão booleana vale a restrição: x[i] = 0 ou x[i] = 1. 2.2) Algoritmo Guloso - Versão Contínua da Mochila Os dados devem ser fornecidos em ordem decrescente de v/w: v[1]/w[1] v[2]/w[2] . . . v[N]/w[N] MOCHILA-CONTÍNUA (w, v, W, n) início j = 1; enquanto (j <= n e w[j] <= W) faça x[j] = 1; W = W-w[j]; j = j+1; fimenquanto se (j <= n) então x[j] = W/w[j]; fimse para k = j+1 até n faça x[k] = 0; fimpara escreva(x); fim. void mochila_continua(int *w, int *v, int W, int n, float *x) { int j, k; j=1; while (j<=n && w[j]<=W) { x[j]=1; W=W-w[j]; j=j+1; } if (j<=n) x[j]=W/w[j]; for(k=j+1; k<=n; k++) x[k]=0; for(j=1;j<=n; j++) printf("%.2f\n", x[j]); } Outra possível solução: MOCHILA-CONTÍNUA (w, v, W, n) inicio para j de 1 até n faça se (w[j] <= W) então x[j] = 1; W = W-w[j]; senão x[j] = W/w[j]; W = 0; fimse fimpara escreva(x); fim. void mochila_continua(int *w, int *v, int W, int n, float *x) { int i; for(i=1; i<=n; i++) { if (w[i]<=W) { x[i]=1; W=W-w[i]; } else { x[i]= (float) W/w[i]; W=0; } } for(i=1;i<=n; i++) printf("%.2f\n", x[i]); } O algoritmo guloso escolhe o primeiro objeto viável (na ordem dada) e não se preocupa com o futuro e nem pode voltar atrás sobre um valor atribuído a um componente de x. Consumo de tempo O(n), sem considerar o tempo O(n log(n)) necessário para ordenar os objetos antes de aplicar o algoritmo. 2.3) Algoritmo Recursivo – Mochila Booleana O algoritmo guloso não funciona para a versão booleana do problema da mochila. Uma possibilidade está em uma versão recursiva que retorna o valor x.v de uma mochila de valor máximo. MOCHILA-Bool (w, v, n, W) início se (n == 0) então retorne 0; senão A = MOCHILA-Bool (w, v, n-1, W) se (w[n] > W) então devolva A senão B = MOCHILA-Bool (w, v, n-1, W-w[n]) + v[n] retorne max(A,B); fimse fimse fim int mochila_rec(int *w, int *v, int n, int W) { int A, B; if (n==0) return 0; else { A=mochila_rec(w, v, n-1, W); if (w[n]>W) return A; else { B=mochila_rec(w, v, n-1, W- w[n]) + v[n]; return max(A,B); } } } Na literatura observa-se que este algoritmo é muito ineficiente uma vez que refaz, várias vezes, a solução de vários dos subproblemas. Uma possível solução está na programação dinâmica. 2.5) Algoritmo de Programação Dinâmica - Mochila Booleana Considere a tabela para armazenamento dos subproblemas como t[n, W], onde t[i,j] é o valor máximo da expressão x[1..i]·v[1..i] sujeita à restrição x[i]·w[i] <= j. 0in e 0jW Considere t[0,Y]=0 para todo Y. Se i > 0 então temos a recorrência: t[i,j] = A t[i,j] = max(A,B) se w[i]>j e se w[i]<= j, onde A = t[i-1,j] e B = t[i-1,j-w[i]] + v[i]. O consumo de tempo do algoritmo é O(nW). MOCHILA-BOOLEANA (w, v, n, W) inicio para Y de 0 até W faça t[0,Y] = 0; para i de 1 até n faça A = t[i-1,Y]; se (w[i] > Y) então B = 0; senão B = t[i-1,Y-w[i]] + v[i]; fimse t[i,Y] = max (A,B); fimpara fimpara Y = W; para i de n até 1 passo -1 faça se t[i,Y] = t[i-1,Y] então x[i] := 0 senão x[i] := 1 fimse Y := Y-w[i]; fimpara escreva(t); fim. 3) Aplicações da Programação Dinâmica 3.1) Reconhecimento de Padrões - Exemplo Algoritmo de Viterbi 3.2) Distância de Edição - Comparação de cadeias int ** mochila_booleana_pd(int *w, int *v, int n, int W, float *x){ int **t, i, y, A, B; t=create(n, W); for(y=0; y<=W; y++) { t[0][y]=0; for(i=1; i<=n; i++) { A=t[i-1][y]; if (w[i]>y) B=0; else B=t[i-1][y-w[i]] + v[i]; t[i][y]=max(A,B); } } y=W; for(i=n; i>=1; i--) { if (t[i][y]==t[i-1][y]) x[i]=0; else x[i]=1; y=y-w[i]; } return t; }