Complexidade e estruturas
Limites assimptóticos, tamanho da entrada e o custo das estruturas que suportam os algoritmos.
Procurar um valor num vetor desordenado de elementos pode exigir comparações no pior caso. Se o mesmo vetor estiver ordenado, a pesquisa binária compara com o elemento do meio e descarta metade dos candidatos em cada passo, com comparações. A ordenação prévia torna possível essa escolha, mas tem um custo próprio que a análise deve incluir quando ainda está por fazer.
Para comparar algoritmos sem depender da máquina, identifica três elementos antes de anunciar um custo: o tamanho da entrada, as operações contadas e a estrutura que as suporta. Uma escolha matematicamente correta pode ficar lenta se a implementação repetir procuras ou copiar dados em cada passo.
O que é o tamanho da entrada
Num vetor, costuma ser o número de elementos. Num grafo, escreve para os vértices e para as arestas. Uma lista de adjacência ocupa espaço. Dizer apenas que um algoritmo de grafos é linear esconde a representação, por isso indica sempre se o custo é em , em ou nos dois.
Numa mochila, é uma capacidade numérica e não o número de bits da entrada. O inteiro precisa de bits para ser escrito. Uma tabela com colunas pode ser enorme mesmo quando escrever ocupa poucos caracteres. Esta diferença entre valor e representação explica o custo pseudopolinomial da programação dinâmica: a tabela cresce com o valor, não com o tamanho da sua escrita.
A análise habitual conta operações elementares de custo constante. Quando os números podem crescer sem limite, somar ou multiplicar inteiros passa a depender do número de bits. Nos exemplos de C++ desta cadeira, assume valores que cabem nos tipos usados e respeita os limites indicados em cada enunciado.
O, Ω e Θ
Para funções não negativas, significa que existem e tais que para todo . É um limite superior. troca essa desigualdade por e dá um limite inferior. exige os dois.
Por exemplo, . Para , temos , logo . Também é verdade que , mas esse limite informa menos. Dizer não afirma que toda a entrada demora exatamente esse tempo, nem que o melhor caso é quadrático.
A ordem dos crescimentos mais usados é
A base do logaritmo altera uma constante. Uma potência com fixo é polinomial. Em , acrescentar um elemento duplica o número de candidatos; em , a multiplicação depende de .
Contar ciclos
Estes dois ciclos não fazem o mesmo trabalho:
// Excertos: n é positivo e cada operação do corpo custa O(1).
for (int i = 0; i < n; ++i)
for (int j = 0; j < i; ++j)
operacao();
for (int i = 1; i < n; i *= 2)
operacao();
O primeiro faz operações, portanto . O segundo visita e termina ao atingir , portanto faz operações. O código real deve evitar overflow de i; aqui o excerto descreve apenas a contagem.
Uma sequência de dois passos soma os custos. Um ciclo soma o custo de cada iteração. Multiplicar dois limites só é correto quando esse produto corresponde ao número de vezes que o trabalho interno ocorre. Se uma BFS percorre a lista de cada vértice uma vez, o total dessas listas é , não .
Melhor caso, pior caso e custo amortizado
Na procura linear, um alvo na primeira posição exige uma comparação; um alvo ausente exige . Temos melhor caso e pior caso . O caso médio exige uma distribuição de entradas. Não podemos obtê-lo apenas pela média desses dois limites.
Uma inserção no fim de vector é amortizadamente . Algumas inserções provocam realocação e copiam elementos, mas uma sequência de inserções tem custo total com crescimento geométrico da capacidade. Amortizado é uma garantia sobre sequências de operações; médio é uma expectativa sobre uma distribuição.
Estruturas e operações
| Estrutura | Operações úteis | Custo e condição |
|---|---|---|
| Vetor | Acesso por índice | |
| Vetor ordenado | Pesquisa binária | ; manter a ordem pode exigir deslocar elementos |
| Fila/pilha | Inserir e retirar na extremidade adequada | nas implementações habituais |
| Heap binário | Inserir, extrair mínimo | ; consultar mínimo é |
| Árvore de pesquisa equilibrada | Procurar, inserir, remover | |
| Tabela de dispersão | Procurar e inserir | esperado com dispersão e ocupação adequadas; pior caso |
| Union-find | Unir componentes, encontrar representante | amortizado com compressão de caminhos e união por rank/tamanho |
A fila de prioridade da STL não tem decreaseKey. Em Dijkstra podemos inserir um novo par (distância, vértice) e ignorar os pares antigos quando saem. Essa decisão muda a contagem de entradas da heap. Não alteres um campo usado pelo comparador de um objeto já na heap e esperes que ela se reorganize sozinha.
Representar grafos
Uma matriz de adjacência usa espaço e testa se existe uma aresta em . Percorrer todos os vizinhos de um vértice custa , mesmo que tenha poucas arestas.
Uma lista usa espaço e percorre os vizinhos em . Num grafo não dirigido guardamos normalmente cada aresta nos dois sentidos, o que multiplica o espaço por dois e mantém a mesma ordem assimptótica. Arestas paralelas e pesos zero devem ter uma representação explícita; zero não deve significar simultaneamente “não há aresta” e “há uma aresta de custo zero”.
Um grafo esparso pode ter ; um grafo simples denso pode ter . Assim, e são alternativas diferentes conforme a densidade. A escolha vem das operações necessárias e dos limites da entrada.
Compor subrotinas sem esconder o crescimento
Quando um algoritmo chama outro, o tamanho da entrada da chamada interna pode depender da entrada original. Se uma fase produz uma lista de tamanho e a fase seguinte a ordena, o custo total inclui , não apenas o custo da segunda fase no seu próprio parâmetro. Substitui o tamanho efetivo antes de somar.
Operações aritméticas também dependem da representação. Somar números de bits custa ; multiplicar pode custar mais conforme o algoritmo usado. Nos limites de DA, cada operação sobre índices e pesos cabe nos tipos indicados e conta . Quando a capacidade ou o alvo fazem parte da entrada binária, o número de bits é o tamanho relevante, e uma tabela com colunas é exponencial nesse tamanho.
O que entregar numa análise
Para uma solução, identifica os parâmetros, conta estados ou operações, inclui a estrutura de suporte e separa tempo de memória auxiliar. Por exemplo: “a BFS com listas inicializa marcas e examina arestas, pelo que usa tempo e memória auxiliar, além do grafo”.
Exercícios
Contar trabalho dependente do índice
Um algoritmo percorre i de 1 a n e, para cada i, executa uma operação constante i vezes. Deriva a contagem exata e um limite Θ. Outro ciclo começa com i=1 e repete enquanto i < n, duplicando i em cada passo; compara os limites.
Pista
Soma os comprimentos das iterações, em vez de multiplicar o maior por todas.
Mais uma pista
A soma de 1 a n é n(n+1)/2. Para duplicações, conta quantas potências de dois são menores que n.
Solução
O primeiro executa n(n+1)/2 operações e tem custo Θ(n²), porque essa expressão fica entre múltiplos positivos de n² para n suficientemente grande. O ciclo que duplica tem Θ(log n) iterações, assumindo que o tipo numérico não transborda. Os limites medem operações constantes e excluem outro trabalho dentro do corpo.
Confere a tua resposta:
- Derivei n(n+1)/2.
- Justifiquei Θ(n²) e Θ(log n).
- Indiquei custo constante por operação.
Erros frequentes
Contar n² pode dar um limite correto, mas perde a contagem exata. Confundir duplicar o índice com percorrer todos os índices dá um limite linear incorreto.
Uma capacidade curta de escrever
Uma mochila tem 20 objetos e capacidade W = 2^30. Um algoritmo usa 20W células. Explica por que este número é pseudopolinomial e estima quantas células usa.
Pista
Compara o valor da capacidade com o número de bits que a representa.
Mais uma pista
2^30 tem 31 bits em representação binária usual, mas há 1 073 741 824 valores até essa ordem.
Solução
A contagem é 20 × 1 073 741 824 = 21 474 836 480 células. O custo é polinomial no valor numérico W, mas exponencial no expoente 30, que está relacionado com o comprimento binário de W. Não é um algoritmo polinomial no tamanho completo da entrada binária apenas por a fórmula ser 20W.
Confere a tua resposta:
- Calculei 21 474 836 480 células.
- Distingui valor W e comprimento binário.
- Expliquei pseudopolinomial sem concluir P = NP.
Erros frequentes
Usar 30 como número de colunas confunde o expoente com a capacidade. O número de células ainda não é o número de bytes; cada célula tem o seu tipo e há outras despesas de memória.