Complexidade e estruturas

Limites assimptóticos, tamanho da entrada e o custo das estruturas que suportam os algoritmos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Procurar um valor num vetor desordenado de nn elementos pode exigir nn 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 O(log⁡n)O(\log n) 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, nn costuma ser o número de elementos. Num grafo, escreve n=∣V∣n=|V| para os vértices e m=∣E∣m=|E| para as arestas. Uma lista de adjacência ocupa Θ(n+m)\Theta(n+m) espaço. Dizer apenas que um algoritmo de grafos é linear esconde a representação, por isso indica sempre se o custo é em nn, em mm ou nos dois.

Numa mochila, WW é uma capacidade numérica e não o número de bits da entrada. O inteiro WW precisa de Θ(log⁡(W+1))\Theta(\log(W+1)) bits para ser escrito. Uma tabela com W+1W+1 colunas pode ser enorme mesmo quando escrever WW 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, f∈O(g)f\in O(g) significa que existem c>0c>0 e n0n_0 tais que f(n)≤cg(n)f(n)\le cg(n) para todo n≥n0n\ge n_0. É um limite superior. f∈Ω(g)f\in\Omega(g) troca essa desigualdade por f(n)≥cg(n)f(n)\ge cg(n) e dá um limite inferior. f∈Θ(g)f\in\Theta(g) exige os dois.

Por exemplo, f(n)=3n2+10n+5f(n)=3n^2+10n+5. Para n≥1n\ge1, temos 3n2≤f(n)≤18n23n^2\le f(n)\le18n^2, logo f∈Θ(n2)f\in\Theta(n^2). Também é verdade que f∈O(n3)f\in O(n^3), mas esse limite informa menos. Dizer O(n2)O(n^2) não afirma que toda a entrada demora exatamente esse tempo, nem que o melhor caso é quadrático.

A ordem dos crescimentos mais usados é

1≺log⁡n≺n≺nlog⁡n≺n2≺n3≺2n≺n!.1\prec\log n\prec n\prec n\log n\prec n^2\prec n^3\prec 2^n\prec n!.

A base do logaritmo altera uma constante. Uma potência nkn^k com kk fixo é polinomial. Em 2n2^n, acrescentar um elemento duplica o número de candidatos; em n!n!, a multiplicação depende de nn.

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 0+1+⋯+(n−1)=n(n−1)/20+1+\cdots+(n-1)=n(n-1)/2 operações, portanto Θ(n2)\Theta(n^2). O segundo visita 1,2,4,…1,2,4,\ldots e termina ao atingir nn, portanto faz Θ(log⁡n)\Theta(\log n) 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 é mm, não nmnm.

Melhor caso, pior caso e custo amortizado

Na procura linear, um alvo na primeira posição exige uma comparação; um alvo ausente exige nn. Temos melhor caso Θ(1)\Theta(1) e pior caso Θ(n)\Theta(n). 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 O(1)O(1). Algumas inserções provocam realocação e copiam Θ(n)\Theta(n) elementos, mas uma sequência de nn inserções tem custo total O(n)O(n) 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

EstruturaOperações úteisCusto e condição
VetorAcesso por índiceΘ(1)\Theta(1)
Vetor ordenadoPesquisa bináriaO(log⁡n)O(\log n); manter a ordem pode exigir deslocar O(n)O(n) elementos
Fila/pilhaInserir e retirar na extremidade adequadaO(1)O(1) nas implementações habituais
Heap binárioInserir, extrair mínimoO(log⁡n)O(\log n); consultar mínimo é O(1)O(1)
Árvore de pesquisa equilibradaProcurar, inserir, removerO(log⁡n)O(\log n)
Tabela de dispersãoProcurar e inserirO(1)O(1) esperado com dispersão e ocupação adequadas; pior caso O(n)O(n)
Union-findUnir componentes, encontrar representanteO(α(n))O(\alpha(n)) 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 Θ(n2)\Theta(n^2) espaço e testa se existe uma aresta em Θ(1)\Theta(1). Percorrer todos os vizinhos de um vértice custa Θ(n)\Theta(n), mesmo que tenha poucas arestas.

Uma lista usa Θ(n+m)\Theta(n+m) espaço e percorre os vizinhos em Θ(deg⁡(u))\Theta(\deg(u)). 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 m=O(n)m=O(n); um grafo simples denso pode ter m=Θ(n2)m=\Theta(n^2). Assim, O((n+m)log⁡n)O((n+m)\log n) e O(n2)O(n^2) 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 Θ(n2)\Theta(n^2) e a fase seguinte a ordena, o custo total inclui Θ(n2log⁡n)\Theta(n^2\log n), 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 kk bits custa Θ(k)\Theta(k); 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 O(1)O(1). Quando a capacidade WW ou o alvo TT fazem parte da entrada binária, o número de bits Θ(log⁡W)\Theta(\log W) é o tamanho relevante, e uma tabela com WW 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 nn marcas e examina mm arestas, pelo que usa O(n+m)O(n+m) tempo e O(n)O(n) 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.

Voltar à explicação
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.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.