Análise de Algoritmos

Ronaldo Cristiano Prati

Bloco A, sala 513-2

ronaldo.prati@ufabc.edu.br

Abordagem Gulosa

Abordagem dividir-e-conquistar

Programação dinâmica

Sequência de Fibonacci

  • Por exemplo, considere a sequência de Fibonacci:

Fn={1se n=11se n=2Fn1+Fn2se n>2F_n = \begin{cases} 1 &\text{se } n=1 \\ 1 &\text{se } n=2 \\ F_{n-1}+F_{n-2}&\text{se } n>2 \end{cases}

Sequência de Fibonacci

Fibonacci recursivo

  • Cuja complexidade é dada pela equação de recorrência:

T(n)=T(n1)+T(n2)+1T(n) =T(n-1)+T(n-2)+1

Qual é a complexidade?

  • Vamos usar o método da substituição para mostrar que Ω((1+52)n)\Omega\left(\left(\frac{1+\sqrt{5}}{2}\right)^n\right)
  • Inicialmente, vamos mostrar que T(n)xnT(n) \geq x^n para x>1x>1.
  • Seja T(1)=1T(1) = 1, e T(2)=3T(2)=3, e n2n\geq 2
  • Suponha que T(m)xnT(m)\geq x^n, para 2mn12\leq m \leq n-1. Assim, aplicando a T(n)T(n), temos:

T(n)=T(n1)+T(n2)+1xn1+xn2xn2(1+x)\begin{aligned} T(n) &=T(n-1)+T(n-2)+1 \\ & \geq x^{n-1}+x^{n-2} \\ & \geq x^{n-2}(1+x) \end{aligned}

Qual é a complexidade?

  • Note que 1+xx21+x\geq x^2 para (15)/2x(1+5)/2(1-\sqrt{5}) / 2 \leq x \leq(1+\sqrt{5}) / 2.
  • Portanto, fazendo x=(1+5)/2x=(1+\sqrt{5}) / 2 e substituindo em T(n)T(n), temos

T(n)(1+52)n2(1+(1+52))(1+52)n2(1+52)2=(1+52)n\begin{aligned} T(n) & \geq\left(\frac{1+\sqrt{5}}{2}\right)^{n-2}\left(1+\left(\frac{1+\sqrt{5}}{2}\right)\right) \\ & \geq\left(\frac{1+\sqrt{5}}{2}\right)^{n-2}\left(\frac{1+\sqrt{5}}{2}\right)^{2} \\ &=\left(\frac{1+\sqrt{5}}{2}\right)^{n} \end{aligned}

Dá pra fazer melhor?

  • Observe que na abordagem puramente recursiva, precisamos resolver o mesmo problema várias vezes.

  • Podemos criar uma memória auxiliar para armzenar valores intermediários.

    • Criar um vetor de tamanho nn
    • A medida que formos calculando FiF_i, armanezenamos esse valor para consultar posteriormente.

Fibonacci com memória

Fibonacci com memória

  • Uma outra abordagem é preencher o vetor em uma abordagem bottom-up

Fibonacci com memória

  • Esse é um exemplo de programação dinâmica!

  • É fácil ver que a complexidade do algoritmo é O(n)O(n) já que a consulta ao vetor pode ser feita em O(1)O(1)

  • Entretanto, precisamos de uma memória auxiliar para armazenar os subproblemas já solucionados

Corte da barra de ferro

  • Considere um problema em que temos uma barra de ferro de tamanho nn, e queremos dividí-la em pedaços, cujos tamanho também são inteiros
  • Suponha também que os pedaços tem preços diferentes
p1p_1 p2p_2 p3p_3 p4p_4 p5p_5 p6p_6 p7p_7 p8p_8 p9p_9 p10p_{10}
1 5 8 9 10 17 17 20 24 30
  • Como cortar as barra para otimizar o lucro?

Corte da barra de ferro

Tamanho Lucro Peças
1 1 1 (sem corte)
2 5 2 (sem corte)
3 8 3 (sem corte)
4 10 2 e 2
5 13 2 e 3
6 17 6 (sem corte)
7 18 1 e 6 ou 2, 2 e 3
8 22 2 e 6
9 25 3 e 6
10 30 10

Corte da barra de ferro

  • Quantas combinações de corte existem?
  • Cada ponto de corte é uma variável aleatória binária (00 ou 11)
  • Temos n1n-1 possíveis pontos de corte
  • O total de possibilidades é 2n12^{n-1}

Corte da barra de ferro

  • Uma abordagem simplista consiste em gerar todas as possíveis combinações
  • Calcular o lucro de cada uma delas
  • Selecionar aquela que maximiza o lucro
  • Claramente essa abordagem tem uma complexidade O(2n)O(2^n), já que temos 2n12^{n-1} combinações

Corte da barra de ferro

  • Uma outra possibilidade é um algoritmo recursivo

  • Um algoritmo recursivo para o problema do corte da barra escolhe uma posição pip_i para fazer o corte e faz uma chamada recursiva

  • Como não sabemos qual o tamanho do primeiro corte, testamos todas as possibilidades de 11 a nn

Corte da barra de ferro recursivo

Corte da barra de ferro recursivo

  • A equação de recorrência desse algoritmo é
    T(n)=1+i=1nT(ni)T(n) = 1+\sum_{i=1}^nT(n-i)

  • Podemos usar o método da substituição para mostrar que esse algoritmo é O(2n)O(2^n). Suponha que T(m)2mT(m)\geq2^m para 0mn10\leq m\leq n-1

T(n)=1+T(0)+T(1)++T(n1)1+(20+21++2n1)=2n\begin{aligned}T(n)&=1+T(0)+T(1)+\cdots+T(n-1)\\ & \geq 1+\left(2^{0}+2^{1}+\cdots+2^{n-1}\right)\\ & =2^{n}\end{aligned}

Corte da barra de ferro recursivo

Corte da barra de ferro

  • O problema do corte da barra de ferro é um problema de otimização

  • Ele tem a propriedade de subestrutura ótima

    • Você precisa saber o corte ótimo do sub-problema para calcular o corte ótimo
  • Tem uma solução recursiva exponencial

Corte da barra de ferro

  • Vamos usar um vetor BB de nn posições para armezar o custo de um sub-problema já resolvido
  • Além disso, um vetor SS em que a posição S[j]S[j] indica a posição em que a barra de tamanho jj deve ser cortada

Corte da barra de ferro


Abordagem BottomUP

  • Também podemos ter uma abordagem BottomUp

Corte da barra de ferro

  • Considerando os preços
p1p_1 p2p_2 p3p_3 p4p_4 p5p_5 p6p_6 p7p_7 p8p_8 p9p_9 p10p_{10}
1 5 8 9 10 17 17 20 24 30
  • O algoritmo calcula
ii 0 1 2 3 4 5 6 7 8 9 10
B[i]B[i] 0 1 5 8 10 13 17 18 22 25 30
S[i]S[i] 0 1 2 3 2 2 6 1 2 3 10

Corte da barra de ferro

  • O primeiro passo do algoritmo é verificar se o subproblema já foi resolvido.
  • Caso não esteja resolvido, ele faz isso de uma maneira parecida com o algoritmo CorteBarra.
  • A diferença é que o primeiro local pra fazer o corte de uma barra de tamanho ii é armazenada em S[i]S[i], e o maior lucro em B[i]B[i].

Complexidade

  • Observe que a cada chamada de CorteBarraRecursivoTopDown, um subproblema que já foi resolvido retorna imediatamente.
  • As demais linhas são executadas em tempo constante.
  • Como armazenamos o resultado sempre que resolvemos um subproblema, cada subproblema é resolvido uma vez.
  • O tempo de execução é então

T(m)=1+2++m=Θ(m2)T(m)=1+2+\cdots+m=\Theta\left(m^{2}\right)

Impressão dos Cortes

  • Para saber os pontos de corte, podemos percorrer o vertor S[i]S[i] para encontrar os pontos de corte.
  • Observe que em S[n]S[n], temos o tamanho do primeiro corte.
  • Após esse corte, a barra tem tamanho nS[]n-S[-]

Mochila inteira

  • Dado um conjunto I={1,2,,n}I=\{1,2,\cdots,n\} contendo nn itens
  • Cada item iIi\in I tem um peso wiw_i e um valor viv_i
  • Como podemos selecionar um subconjutno de SIS \subseteq I uma mochila com capacidade WW tal que:
    • O peso dos itens selecionados cabe na mochila: iSwiW\sum_{i \in S} w_i \leq W
    • O valor dos itens selecionados é maximo: maxiSvi\max \sum_{i \in S} v_i

Mochila inteira

  • Por exemplo, dado o seguinte conjunto de itens
item 1 2 3 4 5
peso 6 2 4 3 11
valor 20 8 14 13 35
  • Como podemos selecionar itens para preencher uma mochila de tamanho 10?

Mochila inteira

  • Podemos resolver o problema por força bruta enumerando todos os subconjuntos possíveis
  • Verificar quais subconjuntos cabem na mochila
  • Calcular o valor total dos suconjuntos que cabem e armazenar o maior deles

Quantos subconjuntos existem?

  • Para cada item, temos a opção de colocá-lo ou não
  • Temos um total de 2n2^n subconjuntos
  • Para cada conjunto, levamos O(n)O(n) para verificar se os itens cabem na mochila e qual é o seu valor total
  • Ou seja, esse algoritmo leva temp O(n2n)O(n2^n)

Subestrutura do problema

  • Seja uma instância do problema por K(In,v,wW)K\left(I_{n}, v, w W\right), em que:
    • In={1,2,,n}I_n = \{1,2,\cdots,n\} um conjunto de itens
    • vv é uma lista dos valores dos itens
    • ww é uma lista dos pesos dos itens
    • WW é a capacidade da mochila

Subestrutura do problema

  • SS^* é uma solução ótima para K(In,v,w,W)K(I_{n}, v, w, W), e S,InS^*\subseteq, I_n com um valor total ótimo Vn,W=iSviV_{n,W} = \sum_{i \in S^*} v_i

    • Se nSn \notin S, então SS^* também é uma solução para K(In1,W)K\left(I_{n-1}, W\right)
    • Se nSn \in S, então SS^* também é uma solução para K(In1,Wwn)K\left(I_{n-1}, W - w_n\right)
  • Como saber se nn está em SS^*?

    • Máximo entre selecionar ou não o item nn, se ele cabe na mochila
    • Valor do problema desconsiderando o item nn, se ele não cabe na mochila

Vn,W={max{Vn1,W,Vn1,Wwn+vn} se wnWVn1,W se wn>WV_{n, W}=\left\{\begin{array}{ll}{\max \left\{V_{n-1, W}, V_{n-1, W-w_{n}}+v_{n}\right\}} & {\text { se } w_{n} \leq W} \\ {V_{n-1, W}} & {\text { se } w_{n}>W}\end{array}\right.

Mochila inteira

Mochila inteira

  • Uma solução direta recursiva baseada nessa regra:

Mochila inteira

  • A equação de recorrência para esse algoritmo é T(n)=2(Tn1)T(n) = 2(Tn-1), cuja solução é O(2n)O(2^n)

  • Olhando a árvore de recursão, é fácil perceber que esse algoritmo resolve o mesmo subproblema várias vezes.

  • Observe que os subproblemas dependem tanto do número de itens nn, quanto da capacidade WW do subproblema.

    • Poderíamos armazenar as soluções usando um vetor de tamanho nWnW
    • Entretanto, usar uma matriz n×Wn \times W facilita a indexação

Mochila inteira

Mochila inteira

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Mochila inteira

item 1 2 3
peso 1 2 3
valor 1 4 6

Imprimindo a mochila

  • Na última célula da matriz M[n][W]M[n][W] contém o valor ótimo.

  • Mas não sabemos quais itens devem estar na mochila

  • No entanto, a maneira como a matriz foi preenchida nos permite obter quais são esses elementos

Imprimindo a mochila

Complexidade

  • É fácil perceber que a complexidade do algoritmo é proporcioal ao tamanho da matriz, ou seja O(nW)O(nW)

  • No entanto, observe que precisamos de uma coluna na tabela para cada possível tamanho da mochila dos subproblemas. Esse número, no entanto, não é polinomial no tamanho do da entrada

  • Considerando pesos inteiros, teríamos O(2logW)O(2^{\log W}) possíveis valores para WW, e a complexidade do algoritmo é O(n2logW)O(n2^{\log W})

  • Esse algoritmo é o que chamamos de pseudo-polinomal

Programação dinâmica

  1. Identificar subestrutura ótima
  2. Encontrar uma formulação recursiva
  3. Usar programação dinâmica para encontrar o valor da função ótima
  4. Se necessário, armazenar informação adicional de tal maneira que no passo 3 podemos encontrar a solução ótima