Você está na página 1de 42

Aula 6

Análise de Eficiência de Algoritmos

11 de Abril de 2023 – Brası́lia


Idéia Geral

Usualmente quando pensamos em Análise de Algoritmos


consideramos ”eficiência”.
Existem questões mais importantes do que eficiência?
Idéia Geral

Além de eficiência, quais caracterı́sticas buscamos em programas


computacionais?

Nós queremos algoritmos


Simples
Corretos
Seguros
Escaláveis
Amigáveis
Modulares
Fáceis de manter (manutenção)
Estáveis
Idéia Geral

Note que
Programas mais simples tem menos chance de apresentar
erros, são mais seguros e fáceis e manter.
Programas eficientes e mais estáveis são mais amigáveis
Programas modulares são mais fáceis de manter

..
.
Idéia Geral

Quando estamos interessados em eficiência, normalmente


consideramos
Eficiência temporal
Eficiência espacial
Embora em algumas situações Eficência Espacial possa ser uma
questão, nós usualmente nos concentramos em Eficiência
Temporal.
Idéia Geral

De uma forma geral, quanto maior o tamanho da input, maior o


tempo gasto para computar a solução de um problema. Dessa
forma, faz sentido investigar a eficiência do algoritmo como função
de um parâmetro n que representa o tamanho.
Idéia Geral

Uma forma de medir eficiência temporal é simplesmente fazer um


gráfico relacionando tempo gasto para rodar o algoritmo e o
tamanho do input.
Idéia Geral

Por que isso não é interessante?


Dependência da velocidade do computador
Dependência da qualidade do programa, programador ou
compilador que gera o código
Dificuldade em medir o tempo usado exatamente para rodar o
algoritmo
Idéia Geral

import time
import numpy as np
import matplotlib.pyplot as plt

def fibonacci_smart_recursive(n,x1,x2):
if(n==1):
return x1
else:
if(n==2):
return x2
else:
return fibonacci_smart_recursive(n-1,x2,x1+x2)

def fibonacci(n):
fm2=0
fm1=1
if(n>2):
for i in range(2,n):
f=fm1+fm2
fm2=fm1
fm1=f
return f
else:
if(n==2):
return 1
else:
return 0
Idéia Geral

if __name__ == '__main__':

n=10
theTimes=np.empty([n,2])
theN=np.empty([n])
step=50
N=-step+1

for i in range(0,n):
N=N+step
theN[i]=N
print(i)

beg_ts = time.time()
fibonacci(N)
end_ts = time.time()
theTimes[i,0]=end_ts-beg_ts

beg_ts = time.time()
fibonacci_smart_recursive(N,0,1)
end_ts = time.time()
theTimes[i,1]=end_ts-beg_ts

ax = plt.subplot(111)
ax.plot(theN,theTimes[:,0],color='b')
ax.plot(theN,theTimes[:,1],color='r')
ax.set_ylabel('Execution Time')
ax.set_xlabel('n')
plt.show()

plt.savefig('thetimes.eps')
Idéia Geral

0.00045

0.00040

0.00035

0.00030
Execution Time

0.00025

0.00020

0.00015

0.00010

0.00005

0.00000
0 100 200 300 400 500
n
Idéia Geral

Uma outra forma é contar o número de vezes em que cada


operação do algoritmo é executada. Isso é difı́cil, muito trabalhoso
e em geral desnecessário.
Como estudamos eficiência de algoritmos

Normalmente identificamos a operação básica (a operação que


mais contribui para o tempo de computação) e calculamos o
número de vezes que essa operação é executada.
Notação Assintótica

Definição
Big O - Notation: Uma função t(n) é dita ser O(g (n)), denotada
por t(n) ∈ O(g (n)), se existe uma constante positiva c e um
inteiro não negativo n0 tal que

t(n) ≤ cg (n) ∀n ≥ n0

Exemplo
t(n) = n ∈ O(n2 ). Qual o valor de c e n0 ?

Exemplo
t(n) = 100n + 5 ∈ O(n2 ). Qual o valor de c e n0 ?

Exemplo
t(n) = 12 n(n − 1) ∈ O(n2 ). Qual o valor de c e n0 ?
Notação Assintótica - continuação

Definição
Big Ω - Notation: Uma função t(n) é dita ser Ω(g (n)), denotada
por t(n) ∈ Ω(g (n)), se existe uma constante positiva c e um
inteiro não negativo n0 tal que

t(n) ≥ cg (n) ∀n ≥ n0

Exemplo
t(n) = 1/2n2 é Ω(n2 ). Qual o valor de c e n0 ?
Notação Assintótica - continuação

Definição
Big Θ - Notation: Uma função t(n) é dita ser Θ(g (n)), denotada
por t(n) ∈ Θ(g (n)), se existem constantes positivas c1 e c2 e um
inteiro não negativo n0 tal que

c1 g (n) ≤ t(n) ≤ c2 g (n) ∀n ≥ n0

Exemplo
t(n) = n2 é Θ(n2 ). Qual os valores de c1 e c2 e n0 ?
Receita para analisar a
eficiência de algoritmos não-recursivos

1 Encontre o parâmetro que indica o tamanho da input


2 Identifique a operação básica do algoritmo
3 Cheque se o número de vezes que a operação básica é
executada depende somente do tamanho da input. Em caso
contrário, devem ser considerados os piores casos, casos
médios e melhores casos.
4 Monte uma soma expressando o número de vezes que as
operações básicas são executadas
5 Manipule a expressão para chegar a uma fórmula de contagem
ou ordem de crescimento.
Receita para analisar a
eficiência de algoritmos não-recursivos

Exemplo
Considere o problema de achar o maior valor de uma lista de n
números.
Receita para analisar a
eficiência de algoritmos não-recursivos

def max_list(myList):
maxVal=myList[0]
n=len(myList)
for i in range(1,n):
if(myList[i]>maxVal):
maxVal=myList[i]
return maxVal

if __name__ == '__main__':

myList=[4,3,2,1,9, 17,6]
print(max_list(myList))
Receita para analisar a
eficiência de algoritmos não-recursivos

Medida do tamanho da input: n.


Existem duas operações:
Comparação (que é feita em toda repetição)
Atribuição (que é feita apenas quando a Comparação é
verdadeira)
Logo, a Comparação é a operação básica.
Receita para analisar a
eficiência de algoritmos não-recursivos

Seja Cn o número de vezes que essa comparação é executada. A


comparação é executada toda vez que uma repetição é executada.
Logo,
n−1
X
C (n) = 1 = (n − 1) ∈ Θ(n)
i=1
Receita para analisar a
eficiência de algoritmos recursivos

1 Encontre o parâmetro que indica o tamanho da entrada.


2 Identifique a operação básica do algoritmo.
3 Cheque se o número de vezes que a operação básica é
executada depende somente do tamanho da input. Em caso
contrário, devem ser considerados os piores casos, casos
médios e melhores casos.
4 Monte a relação de recorrência, com a condição inicial
apropriada, para o número de vezes que a operação básica é
executada.
5 Solucione a relação de recorrência ou encontre a ordem de
crescimento.
Receita para analisar a
eficiência de algoritmos recursivos

Exemplo
Calcule o fatorial F (n) = n!.
Receita para analisar a
eficiência de algoritmos recursivos

def F(n):
if(n==0):
return 1
else:
return n*F(n-1)

if __name__ == '__main__':
n=5
print(F(n))
Receita para analisar a
eficiência de algoritmos recursivos

O indicador do tamanho da input é n [Existe um ”porém” aqui!]


A operação básica é a multiplicação.
Denote M(n) o número de multiplicações para uma input de
tamanho n.
Uma vez que F (n) = n F (n − 1) ∀n > 0
então M(n) = M(n − 1) + 1 ∀n > 0.
Receita para analisar a
eficiência de algoritmos recursivos

Note que M(n) é uma relação de recorrência e nosso objetivo é


resolvê-la.

Para isso, precisamos de uma condição inicial. Nós podemos obter


essa condição inicial usando
if n == 0 return 1

Então quando n == 0 ⇒ M(0) = 0 (pois nenhuma multiplicação é


realizada).
Receita para analisar a
eficiência de algoritmos recursivos
Logo, queremos resolver

M(n) = M(n − 1) + 1 ∀n > 0

M(0) = 0
Então, temos

M(n) = M(n − 1) + 1
Substituindo M(n − 1) = M(n − 2) + 1, temos

M(n) = M(n − 2) + 1 + 1
chegando a

M(n) = n ∈ Θ(n)
Receita para analisar a
eficiência de algoritmos recursivos

Aquele ”porém”:

Note que nessa análise assumimos que o custo computacional da


”multiplicação” é constante e independente do tamanho da
entrada.

Se n cresce demais, então custo da multiplicação também aumenta


com a entrada.
Solução de equações de recorrência:
Método da Substituição (Chute e Teste)

Apenas existe um método que funciona sempre: “Chute”.

Chute a solução e confirme usando indução.


Solução de equações de recorrência:
Método da Substituição (Chute e Teste)

Exemplo
(Torre de Hanoi) Lembre que a solução convencional do problema
conhecido como Torre de Hanoi é dada pela seguinte relação de
recorrência:

T (n) = 2T (n − 1) + 1
Ou seja, T (0) = 0, T (1) = 1, T (2) = 3, T (3) = 7 · · ·
Parece que T (n) = 2n − 1.
Vamos testar:

T (0) = 20 − 1 = 0

T (n) = 2T (n − 1) + 1 = 2(2n−1 − 1) + 1 = 2n − 1
Solução de equações de recorrência:
árvores recursivas

Em muitas situações, podemos usar árvores recursivas para


encontrar bons ”chutes”.

Normalmente, as usamos em recursões do tipo:

T (n) = aT (n/b) + f (n),


onde a e b são constantes e f (n) é uma função qualquer.
Solução de equações de recorrência: árvores recursivas

A raiz da árvore é f (n) e cada raiz tem a filhos com recursão


T (n/b).
Depois somamos todos os ”custos”.
Solução de equações de recorrência:
árvores recursivas

Exemplo
Seja n uma potência de 2 e considere a seguinte recorrência:

T (n) = 2T (n/2) + cn.

cn
cn cn
2 2
cn cn cn cn
4 4 4 4

Note que em cada nı́vel temos cn computações e o número de


nı́veis é log2 n. Dando um total de Θ(nlogn).
Master Theorem (Master Method)

Teorema
(Master Theorem) Seja a ≥ 1 e b > 1 constantes, f (n) uma
função e T (n) definida usando a recorrência

T (n) = aT (n/b) + f (n)


onde n/b significa bn/bc ou dn/be.
Então T (n) tem os seguintes limitantes:
1 Se f (n) = O(nlogb a− ) para algum  > 0, então
T (n) = Θ(nlogb a ).
2 Se f (n) = Θ(nlogb a ) para algum  > 0, então
T (n) = Θ(nlogb a logb n).
3 Se f (n) = Ω(nlogb a+ ) para algum  > 0 e se af (n/b) ≤ cf (n)
para algum c < 1, então T (n) = Θ(f (n)).
Master Theorem (continuação)
Motivação

Seja n uma potência de 2 e considere a seguinte recorrência:

T (n) = 2T (n/2) + cn,


onde a = 2, b = 2 e f (n) = cn.

cn
cn cn
2 2
cn cn cn cn
4 4 4 4

Note que em cada nı́vel temos cn computações e o número de


nı́veis é log2 n (caso 2 acima).
Master Theorem (continuação)

Exemplo
(Problema da Moeda Falsa) Suponha que existem n (onde n é
potência de 2) moedas onde uma é falsa e mais leve. Como
encontrar essa moeda usando uma balança de dois pratos?
Master Theorem (continuação)

Vamos considerar uma solução bem simples. Divida a amostra em


duas partes e sempre elimine o prato mais pesado.
Master Theorem (continuação)
Recorrência associada ao problema da moeda

T (n) = T (n/2) + Θ(1)


Logo, a = 1, b = 2 e f (n) = 1.
Então, esse é um exemplo do Caso 2 do Master Theorem, pois
log 1 1
n 2 = n0 = 1.
Então, usando esse teorema, concluı́mos que T (n) = Θ(1 × log n).
Exercı́cios

1. Calcule a complexidade das implementações recursivas da série


de Fibonacci que vimos na Aula 3.
2. Use o método da árvore recursiva para encontrar a forma da
solução da recorrência dada porém

T (n) = T (n/4) + T (n/2) + n2

3. Use o master method para resolver as seguintes recursões:


a)
T (n) = 4T (n/2) + n
b)
T (n) = 4T (n/2) + n2
c)
T (n) = 4T (n/2) + n3
4. (?) Você pode resolver qualquer exercı́cio apresentado no Capı́tulo 3
”Runtime analysis of recursive algorithms“ do livro ”Introduction to
Recursive Programming“ by Manuel Rubio-Sanchez. A solução inclui:
a) Enunciar cuidadosamente o problema em português;
b) Referenciar o problema, isto é, ”Esse problema xxx apresentado no
Livro XXX no Capı́tulo XXX e na página XXX“.
c) Se depender de alguma teoria não estudada explicitamente em sala de
aula, fazer cuidadosamente a conexão com a teoria em sala de aula
apresentando os detalhes que permitam que seus colegas entendam a
solução do seu problema.
d) Mesmo quando não é pedido no problema, você deve implementar a
solução ou algo relacionado com a solução na linguagem de sua escolha.
Quando for o caso use e abuse de figuras geradas por você (e apresente
os códigos usados para isso).
5. (?) Você pode resolver qualquer problema apresentado no Capı́tulo 8
do livro ”Combinatorial Optimization: Algorithms and Complexity“ de
Christos H. Papadimitriou e Kenneth Steiglitz. A solução inclui:
a) Enunciar cuidadosamente o problema em português;
b) Referenciar o problema, isto é, ”Esse problema xxx apresentado no
Livro XXX no Capı́tulo XXX e na página XXX“.
c) Se depender de alguma teoria não estudada explicitamente em sala de
aula, fazer cuidadosamente a conexão com a teoria em sala de aula
apresentando os detalhes que permitam que seus colegas entendam a
solução do seu problema.
d) Mesmo quando não é pedido no problema, você deve implementar a
solução ou algo relacionado com a solução na linguagem de sua escolha.
Quando for o caso use e abuse de figuras geradas por você (e apresente
os códigos usados para isso).
Fim
11 de Abril de 2023 – Brası́lia

Você também pode gostar