Traduzindo código para matemática
Dada uma semântica operacional (mais ou menos) formal , você pode traduzir o código (pseudo-) de um algoritmo literalmente em uma expressão matemática que fornece o resultado, desde que você possa manipular a expressão em uma forma útil. Isso funciona bem para aditivos medidas custo, tais como o número de comparações, swaps, declarações, acessos à memória, ciclos algumas necessidades máquina abstrata, e assim por diante.
Exemplo: Comparações no Bubblesort
Considere este algoritmo que classifica uma determinada matriz A
:
bubblesort(A) do 1
n = A.length; 2
for ( i = 0 to n-2 ) do 3
for ( j = 0 to n-i-2 ) do 4
if ( A[j] > A[j+1] ) then 5
tmp = A[j]; 6
A[j] = A[j+1]; 7
A[j+1] = tmp; 8
end 9
end 10
end 11
end 12
Digamos que queremos realizar a análise usual do algoritmo de classificação, que é contar o número de comparações de elementos (linha 5). Notamos imediatamente que essa quantidade não depende do conteúdo da matriz A
, apenas do seu comprimento . Portanto, podemos traduzir os loops (aninhados) literalmente em somas (aninhadas); a variável de loop se torna a variável de soma e o intervalo é transferido. Nós temos:nfor
Ccmp(n)=∑i=0n−2∑j=0n−i−21=⋯=n(n−1)2=(n2) ,
onde é o custo para cada execução da linha 5 (que contamos).1
Exemplo: Swaps no Bubblesort
por o subprograma que consiste em linhas para e por os custos para executar esse subprograma (uma vez).Pi,ji
j
Ci,j
Agora, digamos que queremos contar swaps , é com que frequência é executado. Este é um "bloco básico", que é um subprograma que é sempre executado atomicamente e tem algum custo constante (aqui, ). A contratação de tais blocos é uma simplificação útil que geralmente aplicamos sem pensar ou falar sobre isso.P6,81
Com uma tradução semelhante à anterior, chegamos à seguinte fórmula:
Cswaps(A)=∑i=0n−2∑j=0n−i−2C5,9(A(i,j)) .
A(i,j) indica o estado da matriz antes da -ésima iteração de .(i,j)P5,9
Observe que eu uso vez de como parâmetro; em breve veremos o porquê. Eu não adiciono e como parâmetros de já que os custos não dependem deles aqui (no modelo de custo uniforme , que é); em geral, eles apenas podem.AnijC5,9
Claramente, os custos de dependem do conteúdo de (os valores e , especificamente), portanto, temos que prestar contas disso. Agora enfrentamos um desafio: como "desembrulhamos" ? Bem, podemos deixar explícita a dependência do conteúdo de :P5,9AA[j]
A[j+1]
C5,9A
C5,9(A(i,j))=C5(A(i,j))+{10,A(i,j)[j]>A(i,j)[j+1],else .
Para qualquer matriz de entrada, esses custos são bem definidos, mas queremos uma declaração mais geral; precisamos fazer suposições mais fortes. Vamos investigar três casos típicos.
O pior caso
Apenas olhando a soma e observando que , podemos encontrar um limite superior trivial para o custo:C5,9(A(i,j))∈{0,1}
Cswaps(A)≤∑i=0n−2∑j=0n−i−21=n(n−1)2=(n2) .
Mas isso pode acontecer , ou seja, existe um para esse limite superior ser alcançado? Como se vê, sim: se introduzirmos uma matriz inversamente classificada de elementos distintos em pares, toda iteração deve executar uma troca¹. Portanto, derivamos o número exato de piores casos de trocas de Bubblesort.A
O melhor caso
Por outro lado, há um limite inferior trivial:
Cswaps(A)≥∑i=0n−2∑j=0n−i−20=0 .
Isso também pode acontecer: em uma matriz que já está classificada, o Bubblesort não executa uma única troca.
O caso médio
O pior e o melhor dos casos abrem uma lacuna. Mas qual é o número típico de swaps? Para responder a essa pergunta, precisamos definir o que "típico" significa. Em teoria, não temos motivos para preferir uma entrada a outra e, portanto, geralmente assumimos uma distribuição uniforme entre todas as entradas possíveis, ou seja, todas as entradas são igualmente prováveis. Nós nos restringimos a matrizes com elementos distintos aos pares e, portanto, assumimos o modelo de permutação aleatória .
Em seguida, podemos reescrever nossos custos dessa maneira²:
E[Cswaps]=1n!∑A∑i=0n−2∑j=0n−i−2C5,9(A(i,j))
Agora temos que ir além da simples manipulação de somas. Observando o algoritmo, observamos que toda troca remove exatamente uma inversão em (nós sempre trocamos os vizinhos³). Ou seja, o número de swaps realizadas em é exatamente o número de inversões de . Assim, podemos substituir as duas somas internas e obterAAinv(A)A
E[Cswaps]=1n!∑Ainv(A) .
Para nossa sorte, o número médio de inversões foi determinado como sendo
E[Cswaps]=12⋅(n2)
qual é o nosso resultado final. Observe que esse é exatamente metade do custo do pior caso.
- Observe que o algoritmo foi cuidadosamente formulado para que "a última iteração"
i = n-1
do loop externo que nunca faz nada não seja executada.
- " " é uma notação matemática para "valor esperado", que aqui é apenas a média.E
- Aprendemos ao longo do caminho que nenhum algoritmo que apenas troca elementos vizinhos pode ser assintoticamente mais rápido que o Bubblesort (mesmo em média) - o número de inversões é um limite inferior para todos esses algoritmos. Isso se aplica a, por exemplo, Classificação de inserção e Classificação de seleção .
O método geral
Vimos no exemplo que temos que traduzir a estrutura de controle em matemática; Vou apresentar um conjunto típico de regras de tradução. Também vimos que o custo de qualquer subprograma pode depender do estado atual , que é (aproximadamente) os valores atuais das variáveis. Como o algoritmo (geralmente) modifica o estado, o método geral é um pouco complicado de anotar. Se você começar a se sentir confuso, sugiro que você volte ao exemplo ou crie o seu.
Denotamos com o estado atual (imagine-o como um conjunto de atribuições de variáveis). Quando executamos um programa iniciando no estado , acabamos no estado (fornecido termina).ψP
ψψ/PP
Declarações individuais
Dada apenas uma declaração S;
, você atribui a ela . Isso normalmente será uma função constante.CS(ψ)
Expressões
Se você tiver uma expressão E
da forma E1 ∘ E2
(por exemplo, uma expressão aritmética em que ∘
possa haver adição ou multiplicação, adicione custos recursivamente:
CE(ψ)=c∘+CE1(ψ)+CE2(ψ) .
Observe que
- o custo de operação pode não ser constante, mas depende dos valores de e ec∘E1E2
- avaliação de expressões pode mudar o estado em vários idiomas,
então você pode ter que ser flexível com esta regra.
Seqüência
Dado um programa P
como sequência de programas Q;R
, você adiciona os custos ao
CP(ψ)=CQ(ψ)+CR(ψ/Q) .
Condicionais
Dado um programa P
do formulário if A then Q else R end
, os custos dependem do estado:
CP(ψ)=CA(ψ)+{CQ(ψ/A)CR(ψ/A),A evaluates to true under ψ,else
Em geral, a avaliação A
pode muito bem mudar o estado, daí a atualização para os custos de cada filial.
For-Loops
Dado um programa P
do formulário for x = [x1, ..., xk] do Q end
, atribua custos
CP(ψ)=cinit_for+∑i=1kcstep_for+CQ(ψi∘{x:=xi})
onde é o estado antes do processamento para obter valor , ou seja, após a iteração ter sido definida como , ..., .ψiQ
xi
x
x1
xi-1
Observe as constantes extras para manutenção de loop; a variável do loop deve ser criada ( ) e atribuída seus valores ( ). Isso é relevante, poiscinit_forcstep_for
- computar o próximo
xi
pode ser caro e
- um
for
loop com corpo vazio (por exemplo, depois de simplificar em uma melhor configuração com um custo específico) não terá custo zero se executar iterações.
While-Loops
Dado um programa P
do formulário while A do Q end
, atribua custos
CP(ψ) =CA(ψ)+{0CQ(ψ/A)+CP(ψ/A;Q),A evaluates to false under ψ, else
Ao inspecionar o algoritmo, essa recorrência costuma ser bem representada como uma soma semelhante à dos for-loops.
Exemplo: considere este pequeno algoritmo:
while x > 0 do 1
i += 1 2
x = x/2 3
end 4
Ao aplicar a regra, obtemos
C1,4({i:=i0;x:=x0}) =c<+{0c+=+c/+C1,4({i:=i0+1;x:=⌊x0/2⌋}),x0≤0, else
com alguns custos constantes para as declarações individuais. Assumimos implicitamente que estes não dependem do estado (os valores de e ); isso pode ou não ser verdade na "realidade": pense em transbordamentos!c…i
x
Agora temos que resolver essa recorrência para . Observamos que nem o número de iterações, nem o custo do corpo do loop dependem do valor de , para que possamos eliminá-lo. Ficamos com esta recorrência:C1,4i
C1,4(x)={c>c>+c+=+c/+C1,4(⌊x/2⌋),x≤0, else
Isso resolve com meios elementares para
C1,4(ψ)=⌈log2ψ(x)⌉⋅(c>+c+=+c/)+c> ,
reintroduzir o estado completo simbolicamente; se , então .ψ={…,x:=5,…}ψ(x)=5
Chamadas de procedimento
Dado um programa P
do formulário M(x)
para alguns parâmetros em x
que M
é um procedimento com o parâmetro (nomeado) p
, atribua custos
CP(ψ)=ccall+CM(ψglob∘{p:=x}) .
Observe novamente a constante extra (que pode de fato depender de !). As chamadas de procedimento são caras devido à maneira como são implementadas em máquinas reais e às vezes dominam o tempo de execução (por exemplo, avaliando a recorrência do número de Fibonacci ingenuamente).ccallψ
Descrevo alguns problemas semânticos que você possa ter com o estado aqui. Você desejará distinguir o estado global e o local para chamadas de procedimento. Vamos supor que passamos apenas para o estado global aqui e obtemos M
um novo estado local, inicializado definindo o valor de p
para x
. Além disso, x
pode ser uma expressão que (geralmente) supomos que seja avaliada antes de ser aprovada.
Exemplo: considere o procedimento
fac(n) do
if ( n <= 1 ) do 1
return 1 2
else 3
return n * fac(n-1) 4
end 5
end
De acordo com as regras, obtemos:
Cfac({n:=n0})=C1,5({n:=n0})=c≤+{C2({n:=n0})C4({n:=n0}),n0≤1, else=c≤+{creturncreturn+c∗+ccall+Cfac({n:=n0−1}),n0≤1, else
Observe que desconsideramos o estado global, pois fac
claramente não acessa nenhum. Essa recorrência específica é fácil de resolver para
Cfac(ψ)=ψ(n)⋅(c≤+creturn)+(ψ(n)−1)⋅(c∗+ccall)
Nós cobrimos os recursos de idioma que você encontrará no pseudo-código típico. Cuidado com os custos ocultos ao analisar pseudo-código de alto nível; em caso de dúvida, desdobre. A notação pode parecer complicada e certamente é uma questão de gosto; os conceitos listados não podem ser ignorados. No entanto, com alguma experiência, você poderá ver imediatamente quais partes do estado são relevantes para qual medida de custo, por exemplo, "tamanho do problema" ou "número de vértices". O resto pode ser descartado - isso simplifica significativamente as coisas!
Se você acha que agora isso é muito complicado, saiba: é ! Obter custos exatos de algoritmos em qualquer modelo que seja tão próximo de máquinas reais que permita previsões de tempo de execução (mesmo as relativas) é um esforço árduo. E isso nem sequer considera cache e outros efeitos desagradáveis em máquinas reais.
Portanto, a análise de algoritmos é frequentemente simplificada a ponto de ser matematicamente tratável. Por exemplo, se você não precisar de custos exatos, poderá superestimar ou subestimar a qualquer momento (para limites superiores ou inferiores): reduzir o conjunto de constantes, livrar-se de condicionais, simplificar somas e assim por diante.
Uma nota sobre custo assintótico
O que você normalmente encontrará na literatura e nas redes é a "análise Big-Oh". O termo apropriado é análise assintótica , o que significa que, em vez de derivar custos exatos, como fizemos nos exemplos, você atribui os custos apenas a um fator constante e no limite (grosso modo, "para grandes ").n
Isso é (geralmente) justo, pois declarações abstratas têm alguns custos (geralmente desconhecidos) na realidade, dependendo da máquina, sistema operacional e outros fatores, e tempos de execução curtos podem ser dominados pelo sistema operacional que está configurando o processo em primeiro lugar e outros enfeites. Então você fica com alguma perturbação.
Aqui está como a análise assintótica se relaciona com essa abordagem.
Identifique operações dominantes (que induzem custos), ou seja, operações que ocorrem com mais frequência (até fatores constantes). No exemplo do Bubblesort, uma opção possível é a comparação na linha 5.
Como alternativa, vincule todas as constantes para operações elementares pelo respetivo máximo (de cima). mínimo (abaixo) e faça a análise usual.
- Execute a análise usando contagens de execução desta operação como custo.
- Ao simplificar, permita estimativas. Apenas permita estimativas de cima se seu objetivo for um limite superior ( ) resp. abaixo, se você quiser limites inferiores ( ).OΩ
Certifique-se de entender o significado dos símbolos Landau . Lembre-se de que esses limites existem para todos os três casos ; usar não implica uma análise do pior caso.O
Leitura adicional
Existem muitos outros desafios e truques na análise de algoritmos. Aqui estão algumas leituras recomendadas.
Existem muitas perguntas marcadas como análise de algoritmo que usam técnicas semelhantes a essa.