Aula 04 - Análise de Algoritmos e Notação Assintótica
Universidade Federal do Rio Grande do Sul
Instituto de Informática
Departamento de Informática Teórica
Estes slides utilizam conteúdo adaptado da bibliografia da disciplina e também de notas de aula e slides prévios dos professores Bruno Grisci, Rodrigo Machado e André Grahl.
Quando analisamos algoritmos, precisamos de uma métrica de custo.
Mas o que exatamente devemos medir?
Possíveis Medidas:
P: Qual dessas medidas é a mais relevante para analisar algoritmos?
Modelo RAM (Random Access Machine):
Obs. Em geral é desnecessário definir o custo de cada instrução, assumimos um custo constante para qualquer instrução.
Def. Toda operação simples leva tempo constante:
- Adição, subtração, multiplicação, divisão, \dots
- Movimentos de dados (acesso e escrita na memória).
- Controle condicional.
Obs. Operações complexas não levam tempo constante:
- Loops.
- Chamadas a sub-rotinas como ordenação.
P: Como analisar o tempo de execução de um algoritmo?
Obs. O tempo requerido por um algoritmo depende da entrada:
- Ordenar 100.000 números leva mais tempo que ordenar 3 números.
Precisamos levar o tamanho da entrada em consideração.
Tamanho da Entrada:
- Depende do problema a ser estudado.
- Geralmente o número de itens na entrada (números).
- Multiplicar dois números inteiros: número de bits.
- Pode ser descrito por mais de um número: grafos (V e E).
P: Qual o tamanho da entrada de um Cubo Mágico?
Def. (Tempo de Execução): Para uma entrada específica, é o número de instruções primitivas (steps) executadas.
Instruções independentes de máquina:
T(n) = número de instruções executadas para entrada de tamanho n.
O teste do laço é executado uma vez a mais que o conteúdo do bloco:
| Linha | Custo | Repetições |
|---|---|---|
| 1. | \textcolor{red}{c_1} | n |
| 2. | \textcolor{red}{c_2} | n-1 |
| 3. | 0 | n-1 |
| 4. | \textcolor{red}{c_4} | n-1 |
| 5. | \textcolor{red}{c_5} | \sum_{j=2}^n t_j |
| 6. | \textcolor{red}{c_6} | \sum_{j=2}^n (t_j - 1) |
| 7. | \textcolor{red}{c_7} | \sum_{j=2}^n (t_j - 1) |
| 8. | \textcolor{red}{c_8} | n-1 |
t_j: nº de repetições do laço da linha 5 para aquele valor de j.
Somando todos os custos:
T(n) = \textcolor{red}{c_1} n + \textcolor{red}{c_2}(n-1) + 0(n-1) + \textcolor{red}{c_4}(n-1) + \textcolor{red}{c_5} \sum_{j=2}^n t_j + \\ \textcolor{red}{c_6} \sum_{j=2}^n (t_j - 1) + \textcolor{red}{c_7} \sum_{j=2}^n (t_j - 1) + \textcolor{red}{c_8}(n-1)
Problema: Como nos livramos de t_j? O valor de t_j para cada j depende da sequência de entrada, que não temos como saber a priori.
Solução: Podemos analisar o melhor e o pior caso! Isto é, podemos inferir limites inferiores e limites superiores para t_j.
\begin{align*} T(n) &= \textcolor{red}{c_1} n + \textcolor{red}{c_2}(n-1) + 0(n-1) + \textcolor{red}{c_4}(n-1) + \textcolor{red}{c_5} \sum_{j=2}^n t_j + \\ &\quad \textcolor{red}{c_6} \sum_{j=2}^n (t_j - 1) + \textcolor{red}{c_7} \sum_{j=2}^n (t_j - 1) + \textcolor{red}{c_8}(n-1) \end{align*}
\begin{align*} T(n) &= \textcolor{red}{c_1} n + \textcolor{red}{c_2}(n-1) + 0(n-1) + \textcolor{red}{c_4}(n-1) + \textcolor{red}{c_5} \sum_{j=2}^n 1 + \\ &\textcolor{red}{c_6} \sum_{j=2}^n (1 - 1) + \textcolor{red}{c_7} \sum_{j=2}^n (1 - 1) + \textcolor{red}{c_8}(n-1)\\ &= \textcolor{red}{c_1} n + \textcolor{red}{c_2}(n-1) + \textcolor{red}{c_4}(n-1) + \textcolor{red}{c_5}(n-1) + \textcolor{red}{c_8}(n-1) \\ &= (\textcolor{red}{c_1} + \textcolor{red}{c_2} + \textcolor{red}{c_4} + \textcolor{red}{c_5} + \textcolor{red}{c_8})n - (\textcolor{red}{c_2} + \textcolor{red}{c_4} + \textcolor{red}{c_5} + \textcolor{red}{c_8}) \\ &= \textcolor{#7c3aed}{a} \cdot n + \textcolor{#7c3aed}{b} \end{align*}
Lembrando que \sum_{j=2}^{n} 1 = n - 1.
Melhor Caso: Função linear em n.
\sum_{j=2}^n j = \frac{n(n+1)}{2} - 1 \quad \text{e} \quad \sum_{j=2}^n (j-1) = \frac{n(n-1)}{2}
\begin{align*} T(n) &= \textcolor{red}{c_1} n + \textcolor{red}{c_2} (n-1) + \textcolor{red}{c_4} (n-1) + \textcolor{red}{c_5} \left(\frac{n(n+1)}{2}-1\right) + \\ &=\textcolor{red}{c_6} \left(\frac{n(n-1)}{2}\right) + \textcolor{red}{c_7} \left(\frac{n(n-1)}{2}\right) + \textcolor{red}{c_8} (n-1) \\ &= \left( \frac{\textcolor{red}{c_5}}{2} + \frac{\textcolor{red}{c_6}}{2} + \frac{\textcolor{red}{c_7}}{2} \right) n^2 + \left( \textcolor{red}{c_1} + \textcolor{red}{c_2} + \textcolor{red}{c_4} + \frac{\textcolor{red}{c_5}}{2} - \frac{\textcolor{red}{c_6}}{2} - \frac{\textcolor{red}{c_7}}{2} + \textcolor{red}{c_8} \right) n - (\textcolor{red}{c_2} + \textcolor{red}{c_4} + \textcolor{red}{c_5} + \textcolor{red}{c_8})\\ &= \textcolor{#7c3aed}{a} n^2 + \textcolor{#7c3aed}{b} n + \textcolor{#7c3aed}{c} \end{align*}
Pior Caso: t_j = j
Pior Caso: Função quadrática em n.
P: Por que o pior caso?
Obs. Nós em geral nos preocupamos em encontrar o tempo de execução do pior caso:
- O maior tempo de execução para qualquer entrada de tamanho n.
- O pior caso dá uma garantia para qualquer entrada.
- Para alguns algoritmos o pior caso ocorre com frequência:
- Exemplo: busca de um item não presente em um vetor.
P: Alternativas ao pior caso?
P: Qual é o caso médio do algoritmo de Ordenação por Inserção?
Obs. Para ordenar um vetor ordenado randomicamente com elementos distintos:
- Em média A[j] é menor que metade dos elementos em A[1 \dots j-1].
- Assim, o algoritmo deve comparar o elemento com metade dos elementos até encontrar a posição de key.
Obs. Então t_j = \frac{j}{2}, isso é aproximadamente metade do pior caso, mas ainda é uma função quadrática.
P: Quais instâncias aparecem no mundo real?
No nosso caso, abstraímos com unidades constantes de computação.
Passos unitários contados:
Obs. Nessa disciplina, também iremos considerar unidades constantes de armazenamento para analisar a complexidade de espaço.
Exemplo: Procurar x=23 em A[1..10]
| Índice | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Valor | 42 | 7 | 85 | 13 | 66 | 90 | 23 | 5 | 77 | 31 |
Exemplo: Procurar x=42 em A[1..10]
| Índice | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Valor | 42 | 7 | 85 | 13 | 66 | 90 | 23 | 5 | 77 | 31 |
Exemplo: Procurar x=15 em A[1..10]
| Índice | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Valor | 42 | 7 | 85 | 13 | 66 | 90 | 23 | 5 | 77 | 31 |
Melhor Caso: quando encontramos o elemento logo na primeira comparação.
Pior Caso: quando precisamos examinar todos os elementos do vetor.
Caso Médio: quando o elemento pode aparecer em qualquer posição com igual probabilidade.
P: Qual desses casos é o mais relevante para analisar algoritmos em teoria da computação?
Benchmark inicial: comparação com busca força bruta sobre todo o espaço de soluções.
P: Qual o pior caso?
T(n) = 3n + 1
P: Qual o pior caso?
T(n) = 2 + 2 + 1 = 5
T(n) = (n-1)(n-1)(1+5) = 6n^2 - 12n + 6
Obs. Focar a análise nas características importantes.
Eficiência: Diferentes algoritmos para resolver o mesmo problema podem diferir dramaticamente em sua eficiência.
Problema de Ordenação: Uma sequência de n=10.000.000 números.
Algoritmo A: T(n) = 2n^2.
Algoritmo B: T(n) = 50n \log n.
Constantes \textcolor{red}{c_1}=2 e \textcolor{red}{c_2}=50 não dependem de n.
P: Qual será a diferença de eficiência dos dois algoritmos?
Computador 1: Executa 10 bilhões de instruções por segundo.
A-1: \dfrac{2 \cdot (10^7)^2 \text{ instruções}}{10^{10} \text{ instruções/segundo}} = 20.000 \text{ segundos } (5{,}5 \text{ horas}).
B-1: \dfrac{50 \cdot (10^7) \cdot \log(10^7) \text{ instruções}}{10^{10} \text{ instruções/segundo}} = 1{,}16 \text{ segundos}.
Computador 2: Executa 10 milhões de instruções por segundo.
A-2: \dfrac{2 \cdot (10^7)^2 \text{ instruções}}{10^{7} \text{ instruções/segundo}} = 20.000.000 \text{ segundos } (231 \text{ dias}).
B-2: \dfrac{50 \cdot (10^7) \cdot \log(10^7) \text{ instruções}}{10^{7} \text{ instruções/segundo}} = 1.163 \text{ segundos } (\approx 19 \text{ minutos}).
Tempo de execução para diferentes algoritmos em entradas de tamanho crescente em um computador executando um milhão de instruções por segundo. Em casos com tempo \geq 10^{25} anos substituído por very long. Para referência, a idade estimada do universo é de aproximadamente 4 \times 10^{17} segundos.
Espaço de Busca:
P: Exemplo?
Obs. Em geral, nós consideramos o algoritmo mais eficiente se seu tempo de execução no pior caso tem ordem de crescimento menor (tese de Cobham-Edmonds).
P: Uma definição para um algoritmo eficiente?
Def. Nós dizemos que um algoritmo é eficiente se tem tempo de execução polinomial.
P: E se for n^{100} vs. 1.001^{n}?
Justificativa:
Exexceptions:
Ideia: Suprimir fatores constantes e termos de baixa ordem.
Exemplo: 8n^3 + 50n + 86 cresce como n^3.
Terminologia: O algoritmo tem tempo de execução O(n^3), onde n é o tamanho da entrada.
Problema: O vetor A contém o número t?
P: Qual é o tempo de execução?
P: Quanto espaço é usado?
Problema: O vetor A contém o número t?
P: Qual é o tempo de execução? O(n)
Problema: O vetor A ou o vetor B contém o número t?
P: Qual é o tempo de execução?
Problema: O vetor A ou o vetor B contém o número t?
P: Qual é o tempo de execução? O(n)
Problema: O vetor A e o vetor B contêm um número em comum?
P: Qual é o tempo de execução?
Problema: O vetor A e o vetor B contêm um número em comum?
P: Qual é o tempo de execução? O(n^2)
Problema: O vetor A tem um elemento duplicado?
P: Qual é o tempo de execução? O(n^2)
Def. T(n) é O(f(n)) se e somente se existe uma constante \textcolor{red}{c} > 0 e um \textcolor{#2e7d32}{n_0} \geq 0 tal que T(n) \leq \textcolor{red}{c} \cdot f(n) para todo n \geq \textcolor{#2e7d32}{n_0}.
Exemplo: T(n) = 32n^2 + 17n + 1
Obs. \textcolor{red}{c} e \textcolor{#2e7d32}{n_0} não podem depender de n.

Def. T(n) é O(f(n)) se e somente se existe uma constante \textcolor{red}{c} > 0 e um \textcolor{#2e7d32}{n_0} \geq 0 tal que T(n) \leq \textcolor{red}{c} \cdot f(n) para todo n \geq \textcolor{#2e7d32}{n_0}.
T(n) = 3n + 1
T(n) é O(n): \textcolor{red}{c} = 4 e \textcolor{#2e7d32}{n_0} = 1, \textcolor{red}{c} = 3{,}5 e \textcolor{#2e7d32}{n_0} = 2, \dots
T(n) é O(n^2), O(n^3), \dots
T(n) = (n-1)(n-1)(1+5) = 6n^2 - 12n + 6
T(n) é O(n^2): \textcolor{red}{c} = 6 e \textcolor{#2e7d32}{n_0} = 0, \dots
T(n) é O(n^3), O(n^4), \dots
T(n) não é O(n).
Def. T(n) é O(f(n)) se e somente se existe uma constante \textcolor{red}{c} > 0 e um \textcolor{#2e7d32}{n_0} tal que T(n) \leq \textcolor{red}{c} \cdot f(n) para todo n \geq \textcolor{#2e7d32}{n_0}.
Afirmação. Se T(n) = a_{\textcolor{orange}{k}} n^{\textcolor{orange}{k}} + \dots + a_1 n^1 + a_0 então T(n) = O(n^{\textcolor{orange}{k}}).
Prova. Escolha \textcolor{#2e7d32}{n_0}=1 e \textcolor{red}{c}=|a_{\textcolor{orange}{k}}|+\dots+|a_1|+|a_0|.
Nós temos que mostrar que \forall n \geq \textcolor{#2e7d32}{n_0}, T(n) \leq \textcolor{red}{c} \cdot n^{\textcolor{orange}{k}}.
T(n) \leq |a_{\textcolor{orange}{k}}|n^{\textcolor{orange}{k}}+\dots+|a_1|n^1+|a_0|n^{0}.
T(n) \leq |a_{\textcolor{orange}{k}}|n^{\textcolor{orange}{k}}+\dots+|a_1|n^{\textcolor{orange}{k}}+|a_0|n^{\textcolor{orange}{k}}.
T(n) \leq (|a_{\textcolor{orange}{k}}| + \dots + |a_1| + |a_0|)n^{\textcolor{orange}{k}}.
T(n) \leq \textcolor{red}{c} \cdot n^{\textcolor{orange}{k}} \blacksquare
?