Ata: Aula do dia 02/04/2003
Assunto: Capítulo 15 (Tópicos 15.1 e 15.2)
Professor: João Meidanis
Redator: Thiago Alves da Silva
A figura sobre linha de montagem foi obtida do site
http://www.introductiontoalgorithms.com
e é reproduzida aqui seguindo as normas vigentes de copyright
(
http://www.mcgraw-hill.com/copyrttm.htm ).
2. Programação de linha de montagem
2.1. A estrutura de uma solução ótima
2.2. Uma solução recursiva
2.3. Cálculo do valor de uma solução
ótima
2.4. Construção de uma solução
ótima
3. Multiplicação de cadeias de matrizes
3.1. A estrutura de uma solução ótima
3.2. Uma solução recursiva
3.3. Cálculo do valor de uma solução
ótima
3.4. Construção de uma solução
ótima
4. Conclusão
5. Referências Bibliográficas
O desenvolvimento de um algoritmo de programação dinâmica pode ser desmembrado em uma seqüência de 4 tapas:
1. Caracterizar a estrutura de uma solução ótima
2. Definir recursivamente o valor de uma solução ótima
3. Calcular o valor de uma solução ótima
4. Construir uma solução ótima a partir de informações
calculadas
Veremos a seguir dois exemplos onde a programação dinâmica
é empregada com sucesso.
Visto isso, podemos definir o problema que será resolvido pela programação dinâmica, que é determinar que estações escolher na linha 1 e quais escolher na linha 2 para minimizar o tempo total de passagem de um único automóvel pela fábrica.
Caso fossemos determinar o caminho mais rápido pela fábrica enumerando todos os modos possíveis e calculando quanto tempo cada um deles demora, levaríamos o tempo de Ômega(2^n), que é inviável quando n é grande.
Dicas do Professor:
O professor chama a atenção para a definição
do custo, que nesse caso é apenas o tempo.
Analisando a representação gráfica da linha de
montagem, o professor alerta para que nos preocupemos com as entradas:
"e's", "a's", "t's" e os "x's", pois é o que o programa receberá.
No caso da programação da linha de montagem, uma solução ótima para um problema (encontrar o caminho mais rápido passando pela estação S_i,j) contém em seu interior uma solução ótima para subproblemas (encontrar a passagem mais rápida por S_1,j-1 ou S_2,j-1). Essa é a propriedade da subestrutura ótima.
Concluindo, temos que para resolver o problema de encontrar o caminho mais rápido através da estação j de uma das linhas, resolvemos os subproblemas de encontrar os caminhos mais rápidos pela estação j -1 em ambas as linhas. Isso permite que você construa a solução pouco a pouco.
O professor completa essa análise dizendo que a propriedade da subestrutura ótima verifica-se neste caso da seguinte forma: um prefixo B de uma solução ótima A também é uma solução ótima para o subproblema correspondente, porque se ela não fosse, existiria outra melhor que poderia substituí-la, melhorando também a solução A. Para o problema em questão, podem existir várias soluções com o mesmo tempo, ou seja, a ótima não é necessariamente única.
De acordo com nosso problema, temos que o caminho mais rápido até completar a estação S_1,j é o caminho mais rápido até completar a estação S_1,j-1, e depois passar diretamente pela a estação S_1,j, ou o caminho mais rápido até completar a estação S_2,j-1, uma transferência da linha 2 para a linha 1, e depois passar pela estação S_1,j (toma-se o que for menor dos dois). Um racioncínio análogo vale para completar S_2,j. As recorrências a seguir mostram isso:
| e_1 + a_1,1
se j = 1,
f_1[j] = { | min(f_1[j-1] + a_1,j , f_2[j-1] + t_2,j-1 + a_1,j) se j >= 2. |
| e_2 + a_2,1
se j = 1,
f_2[j] = { | min(f_2[j-1] + a_2,j , f_1[j-1] + t_1,j-1 + a_2,j) se j >= 2. |
Por fim temos que o tempo mais rápido para levar um chassi por todo o percursso na fábrica é:
min(f_1[n] + x_1, f_2[n] + x_2). |
Discussão em aula:
TEMPO(n)
Se (f_1(n) + x_1) < (f_2(n) + x_2) tempominimo = f_1(n) + x_1 saidaporlinha = 1 Senao tempominimo = f_2(n) + x_2 saidaporlinha = 2 Fim-se f_i(n)
|
FASTEST-WAY(a, t, e, x, n)
1 f_1[1] <- e_1 + a_1,1 2 f_2[1] <- e_2 + a_2,2 3 for j <- 2 to n 4 do if f_1[j-1] + a_1,j <= f_2[j-1] + t_2,j-1 + a1,j 5 then f_1[j] <- f_1[j-1] + a_1,j 6 l_1[j] <- 1 7 else f_1[j] <- f_2[j-1] + t_2,j-1 + a_1,j 8 l_2[j] <- 2 9 if f_2[j-1] + a_2,j <= f_1[j-1] + t_1,j-1 + a_2,j 10 then f_2[j] <- f_2[j-1] + a_2,j 11 l_2[j] <- 2 12 else f_2[j] <- f_1[j-1] + t_1,j-1 + a2,j 13 l_2[j] <- 1 14 if f_1[n] + x_1 <= f_2[n] + x_2 15 then f* = f_1[n] + x_1 16 l* = 1 17 else f* = f_2[n] + x_2 18 l* = 2 |
O professor Meidanis conclui esse tópico chamando a atenção para o uso de 4 vetores no lugar da recursão. Vetores esses: f_1, f_2, l_1 e l_2.
PRINT-STATIONS(l,n)
1 i <- l* 2 imprimir "linha" i ",estação" n 3 for j <- n downto 2 4 do i <- l_i[j] 5 imprimir "linha" i ",estação" j-1 |
Se fossemos utilizar um algoritmo de força bruta para verificar exaustivamente todas as possíveis colocações entre parênteses, obteríamos a seguinte recorrência:
| 1
se n = 1,
P(n) = { | n-1 | P (n) = soma P(k)P(n-k) se n >= 2. k=1 |
Antes de iniciarmos nossa busca por uma solução eficiente utilizando programação dinâmica, o professor chama a atenção para a definição da entrada, que nesse caso é um vetor de 0 até n, que contém inteiros Pi. O vetor inicia de 0, pois a dimensão da matriz Ai é Pi-1 * Pi. Também devemos nos preocupar em definir a saída, nesse caso como sendo duas matrizes m e s, onde somente a diagonal principal será utilizada.
Logo, uma solução ótima para uma instância do problema contém soluções ótimas para as sub-instâncias do mesmo problema, o que permite o emprego da programação dinâmica.
Para i < j, podemos usar a estrutura ótima delineada na etapa 1. Assim Ai..j pode ser dividido em duas partes Ai..k e Ak+1..j, onde i <= k < j. Então m[i,j] é igual ao menor custo para calcular Ai..k e Ak+1..j, mais o custo para multiplicar essas duas matrizes. O custo para multiplicar Ai..k.Ak+1..j exige Pi-1.Pk.Pj multiplicações escalares.
Temos a seguinte definição recursiva para o custo mínimo de colocar entre parênteses o produto AiAi+1...Aj:
| 0
se i = j,
m[i,j] = { | min{m[i,k] + m[k+1,j] + Pi-1.Pk.Pj} se i < j. | i <= k < j |
Neste ponto deve-se elaborar um algoritmo para a solução do problema, fazendo os cálculos de tal forma que nenhuma solução seja requisitada antes que a mesma já tenha sido calculada. O procedimento a seguir utiliza uma tabela auxiliar m[1..n,1..n] para armazenar os custos de m[i,j] e uma tabela auxiliar s[1..n,1..n] que registra qual índice de k alcançou o custo ótimo no cálculo de m[i,j]. Na construção de uma solução ótima usaremos a tabela s.
MATRIX-CHAIN-ORDER(P)
1 n <- comprimento[P] -1 2 for i <- 1 to n 3 do m[i,j] <- 0 4 for l <- 2 to n // l é o comprimento da cadeia 5 do for i <- 1 to n - l + 1 6 do j <- i + l - 1 7 m[i,j] <- infinito 8 for k <- i to j - 1 9 do q <- m[i,k] + m[k+1,j] + Pi-1.Pk.Pj 10 if q < m[i,j] 11 then m[i,j] <- q 12 s[i,j] <- k 13 return m e s |
PRINT-OPTIMAL-PARENS(s,i,j)
1 if i = j 2 then print "A"i 3 else print "(" 4 PRINT-OPTIMAL-PARENS(s,i,s[i,j]) 5 PRINT-OPTIMAL-PARENS(s,s[i,j]+1,j) 6 print ")" |
Discussão em aula:
Mesmo sendo considerado um assunto complexo para alguns alunos, o professor deixa claro que outros problemas serão estudados a fim de fortalecermos a idéia da programação dinâmica, e assim assimilarmos melhor o conteúdo desse capítulo.