0% acharam este documento útil (0 voto)
5 visualizações257 páginas

Dicas e Truques de Algoritmos Profissionais

Enviado por

pietroramos2005
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
5 visualizações257 páginas

Dicas e Truques de Algoritmos Profissionais

Enviado por

pietroramos2005
Direitos autorais
© All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd

Machine Translated by Google

Algoritmos

Notas para Algoritmos Profissionais


Notas para profissionais

Mais de 200 páginas


de dicas e truques profissionais

[Link] Livros Aviso Legal


Este é um livro gratuito não oficial criado para fins educacionais e não está vinculado a
de programação grátis grupos ou empresas oficiais de Algoritmos.
Todas as marcas comerciais e marcas registradas são
propriedade de seus respectivos proprietários
Machine Translated by Google

Conteúdo
Sobre .................................................. .................................................. .................................................. ............................. 1

Capítulo 1: Introdução aos algoritmos .................................................................................................... 2

Seção 1.1: Um exemplo de problema algorítmico ............................................................................................................. 2


Seção 1.2: Introdução ao algoritmo Fizz Buzz simples em Swift .................................................. .................... 2

Capítulo 2: Complexidade do Algoritmo .........................................................................................................................


5

Seção 2.1: Notação Big-Theta Seção .................................................. .................................................. ................................ 5

2.2: Comparação das notações assintóticas Seção 2.3: Notação .................................................. ........................................... 6

Big-Omega .................................................. .................................................. ............................ 6

Capítulo 3: Notação Big-O .................................................. .................................................. .................................... 8

Seção 3.1: Um Loop Simples ............................................................................................................................................. 9


Seção 3.2: Um loop aninhado ........................................................................................................................................... 9
Seção 3.3: Tipos de algoritmos O(log n) ............................................................................................................. 10
Seção 3.4: Um exemplo O(log n) .......................................................................................................................... 12

Capítulo 4: Árvores .................................................. .................................................. .................................................. ... 14

Seção 4.1: Representação típica de árvore anária ......................................................................................................... 14


Seção 4.2: Introdução .................................................. .................................................. ........................................... 14

Seção 4.3: Para verificar se duas árvores binárias são iguais ou não .................................................. ................................... 15

Capítulo 5: Árvores de busca binárias .................................................. .................................................. ...................... 18

Seção 5.1: Árvore de busca binária - Inserção (Python) ............................................................................................... 18

Seção 5.2: Árvore de pesquisa binária - Exclusão (C++) ..................................................................................................... 20

Seção 5.3: Menor ancestral comum em um BST .................................................. .................................................. .. 21

Seção 5.4: Árvore de busca binária - Python ............................................................................................................. 22

Capítulo 6: Verifique se uma árvore é BST ou não Seção 6.1: .................................................. .................................................. ...... 24

Algoritmo para verificar se uma determinada árvore binária é BST Seção 6.2: .................................................. ................................ 24

Se uma determinada árvore de entrada segue a propriedade da árvore de busca binária ou não .................................................. ..... 25

Capítulo 7: Travessias de árvore binária Seção 7.1: .................................................. .................................................. ................. 26

Travessia de ordem de nível - Implementação ................................................................................................... 26


Seção 7.2: Percurso de pré-ordem, em ordem e pós-ordem de uma árvore binária .................................................. ........ 27

Capítulo 8: Menor ancestral comum de uma árvore binária .................................................. ....................... 29

Seção 8.1: Encontrando o menor ancestral comum .................................................. .................................................. ..... 29

Capítulo 9: Gráfico ......................................................................................................................................................... 30

Seção 9.1: Armazenando Grafos (Matriz de Adjacência) ....................................................................................................... 30


Seção 9.2: Introdução à Teoria dos Grafos .......................................................................................................... 33
Seção 9.3: Armazenando Grafos (Lista de Adjacência) ........................................................................................................... 37
Seção 9.4: Classificação topológica .................................................. .................................................. ................................. 39

Seção 9.5: Detecção de um ciclo em um grafo direcionado usando Depth First Traversal Seção 9.6: .................................................. 40

Algoritmo de Thorup ................................................................................................................................ 41

Capítulo 10: Travessias de grafos .................................................. .................................................. .......................... 43

Seção 10.1: Função de travessia de busca em profundidade .................................................. ................................................ 43

Capítulo 11: Algoritmo de Dijkstras .......................................................................................................................... 44


Seção 11.1: Algoritmo do caminho mais curto de Dijkstra ......................................................................................................... 44

Capítulo 12: A* Pathfinding ..................................................................................................................................... 49


Seção 12.1: Introdução ao A* .................................................. .................................................. ................................ 49

Seção 12.2: A* Encontrando caminho através de um labirinto sem obstáculos .................................................. ........................... 49

Seção 12.3: Resolvendo o problema de 8 quebra-cabeças usando o algoritmo A* ................................................................................... 56


Machine Translated by Google

Capítulo 13: Algoritmo de Pathfinding A* ........................................................................................................... 59

Seção 13.1: Exemplo simples de A* Pathfinding: Um labirinto sem obstáculos .................................................. ........ 59

Capítulo 14: Programação dinâmica ............................................................................................................. 66


Seção 14.1: Editar Distância .................................................. .................................................. ........................................ 66

Seção 14.2: Algoritmo de agendamento de tarefas ponderado .............................................................................................. 66


Seção 14.3: Maior subsequência comum .......................................................................................................... 70
Seção 14.4: Número de Fibonacci .................................................. .................................................. ............................. 71

Seção 14.5: Substring comum mais longa ............................................................................................................. 72

Capítulo 15: Aplicações da Programação Dinâmica ............................................................................. 73


Seção 15.1: Números de Fibonacci .................................................. .................................................. ............................ 73

Capítulo 16: Algoritmo de Kruskal .......................................................................................................................... 76

Seção 16.1: Implementação ótima baseada em conjuntos disjuntos ....................................................................................... 76


Seção 16.2: Implementação simples e mais detalhada ........................................................................................... 77
Seção 16.3: Implementação simples baseada em conjuntos disjuntos ......................................................................................... 77
Seção 16.4: Implementação simples e de alto nível ....................................................................................................... 77

Capítulo 17: Algoritmos gananciosos .............................................................................................................................79


Seção 17.1: Codificação Humana ..................................................................................................................................... 79
Seção 17.2: Problema de seleção de atividades .................................................. .................................................. ................ 82

Seção 17.3: Problema de criação de mudanças ..................................................................................................................... 84

Capítulo 18: Aplicações da técnica Greedy ............................................................................................. 86

Seção 18.1: Cache Oine .................................................................................................................................................... 86


Seção 18.2: Ticket automático .................................................. .................................................. .................................. 94

Seção 18.3: Programação de intervalos ............................................................................................................................. 97


Seção 18.4: Minimizando atrasos .................................................. .................................................. ........................ 101

Capítulo 19: Algoritmo de Prim .............................................................................................................................. 105

Seção 19.1: Introdução ao Algoritmo de Prim ....................................................................................................... 105

Capítulo 20: Algoritmo Bellman-Ford ......................................................................................................... 113

Seção 20.1: Algoritmo de caminho mais curto de fonte única (Dado que há um ciclo negativo em um gráfico) ................. 113

Seção 20.2: Detectando ciclo negativo em um gráfico ........................................................................................... 116


Seção 20.3: Por que precisamos relaxar todas as arestas no máximo (V-1) vezes .................................................. ........ 118

Capítulo 21: Algoritmo de Linha ....................................................................................................................................121


Seção 21.1: Algoritmo de desenho de linha de Bresenham ........................................... .................................................. ..... 121

Capítulo 22: Algoritmo Floyd-Warshall .......................................................................................................... 124

Seção 22.1: Algoritmo do caminho mais curto para todos os pares ......................................................................................................... 124
Capítulo 23: Algoritmo do Número Catalão ....................................................................................................... 127

Seção 23.1: Informações básicas do algoritmo numérico catalão .................................................. .............................. 127

Capítulo 24: Algoritmos multithread ......................................................................................................... 129

Seção 24.1: Multithread de multiplicação de matriz quadrada .................................................. ........................................ 129

Seção 24.2: Multithread de vetor de matriz de multiplicação .................................................. ........................................ 129


.................................................. .................................................. .................
Seção 24.3: multithread de mesclagem e ordenação 129

Capítulo 25: Algoritmo Knuth Morris Pratt (KMP) ..................................................................................... 131

Seção 25.1: Exemplo de KMP ...................................................................................................................................... 131

Capítulo 26: Algoritmo dinâmico de distância de edição .......................................................................................... 133

Seção 26.1: Edições mínimas necessárias para converter a string 1 em string 2 .................................................. ................. 133

Capítulo 27: Algoritmos online ........................................................................................................................... 136

Seção 27.1: Paginação (Cache Online) .................................................................................................................... 137

Capítulo 28: Classificação ............................................................................................................................................. 143

Seção 28.1: Estabilidade na classificação ................................................................................................................... 143


Machine Translated by Google

Capítulo 29: Classificação por bolhas


.................................................. .................................................. .................................... 144
Seção 29.1: Classificação por bolhas .................................................. .................................................. ........................................... 144

Seção 29.2: Implementação em C e C++ Seção .................................................. .................................................. ........... 144

29.3: Implementação em C# Seção 29.4: .................................................. .................................................. .................... 145

Implementação em Python ................................................................................................................... 146


Seção 29.5: Implementação em Java Seção 29.6: .................................................. .................................................. ................. 147

Implementação em Javascript ........................................................................................................... 147


Capítulo 30: Mesclar classificação .................................................. .................................................. ..................................... 149

Seção 30.1: Noções básicas de classificação .................................................. .................................................. ........................... 149

por mesclagem Seção 30.2: Implementação de classificação por .................................................. .................................................. 150

mesclagem em Go Seção 30.3: Implementação de classificação por .................................................. ........................................... 150

mesclagem em C e C# Seção 30.4: Implementação de classificação .................................................. ............................................... 152

por mesclagem em Java Seção 30.5: Implementação de classificação por mesclagem em Python ................................................................................................
153

Seção 30.6: Implementação Java Bottoms-up .................................................................................................... 154

Capítulo 31: Inserção de ordenação .................................................. .................................................. ................................. 156

Seção 31.1: Implementação Haskell ....................................................................................................................... 156

Capítulo 32: Bucket Sort Seção .................................................. .................................................. .................................... 157

32.1: Implementação em C# ............................................................................................................................. 157

Capítulo 33: Quicksort ............................................................................................................................................. 158

Seção 33.1: Noções básicas de .................................................. .................................................. .............................. 158

Quicksort Seção 33.2: Quicksort em Python ................................................................................................................................ 160


Seção 33.3: Implementação de partição Lomuto Java ............................................................................................. 160
Capítulo 34: Classificação por contagem.................................................. .................................................. ................................ 162

Seção 34.1: Informações básicas sobre classificação .................................................. .................................................. ... 162

por contagem Seção 34.2: Implementação de pseudocódigo ................................................................................................................ 162


Capítulo 35: Heap Sort .................................................. .................................................. ........................................ 164

Seção 35.1: Implementação C# ......................................................................................................................... 164


Seção 35.2: Informações básicas sobre classificação de .................................................. .................................................. ......... 164

.................................................. .................................................. ........................................


heap Capítulo 36: Classificação por ciclo 166

Seção 36.1: Implementação de pseudocódigo ............................................................................................................. 166


Capítulo 37: Classificação Ímpar-Par .................................................. .................................................. .............................. 167
Seção 37.1: Informações básicas sobre classificação ímpar-par .................................................. .................................................. .. 167

Capítulo 38: Seleção de classificação .................................................. .................................................. ................................ 170

Seção 38.1: Implementação do Elixir .......................................................................................................................... 170


Seção 38.2: Informações básicas sobre classificação por seleção .................................................. .................................................. .. 170

Seção 38.3: Implementação da classificação por seleção em .................................................. .......................................... 172

C# Capítulo 39: Pesquisando ................................................................................................................................................ 174

Seção 39.1: Busca Binária .................................................. .................................................. .................................. 174

Seção 39.2: Rabin Karp ......................................................................................................................................................... 175

Seção 39.3: Análise de busca linear (pior, médio e melhor caso) ........................................................ 176

Seção 39.4: Busca binária: em números ordenados Seção .................................................. ................................................ 178
39.5: Busca linear .................................................. .................................................. .................................. 178

Capítulo 40: Pesquisa de substring .................................................. .................................................. ....................... 180

Seção 40.1: Introdução ao Algoritmo Knuth-Morris-Pratt (KMP) ..................................................................... 180


Seção 40.2: Introdução ao Algoritmo Rabin-Karp ............................................................................................. 183
Seção 40.3: Implementação Python do algoritmo KMP ...................................................................................... 186
Seção 40.4: Algoritmo KMP em C .................................................. .................................................. ........................ 187

Capítulo 41: Busca em largura .................................................. .................................................. ................ 190


Machine Translated by Google

Seção 41.1: Encontrando o caminho mais curto da fonte para outros nós Seção .................................................. ................ 190

41.2: Encontrando o caminho mais curto da fonte em um gráfico 2D ................................................................................ 196

Seção 41.3: Componentes conectados de grafos não direcionados usando BFS Capítulo .................................................. ........... 197

42: Busca em profundidade Seção 42.1: .................................................. .................................................. .................... 202

Introdução à busca em profundidade .................................................. ................................................ 202

Capítulo 43: Funções de hash Seção 43.1: .................................................. .................................................. ............................ 207

Códigos de hash para tipos comuns em C# Seção 43.2: .................................................. ............................................. 207
Introdução às funções de hash .................................................. .................................................. ...... 208

Capítulo 44: Caixeiro Viajante Seção 44.1: Algoritmo .................................................. .................................................. ................ 210

de Força Bruta ............................................................................................................................ 210


Seção 44.2: Algoritmo de programação dinâmica .................................................................................................... 210
Capítulo 45: Problema da mochila .................................................. .................................................. .................... 212

Seção 45.1: Noções básicas sobre o problema .................................................. .................................................. .............. 212

da mochila Seção 45.2: Solução implementada em C#.................................................. .................................................. .......... 212

Capítulo 46: Resolução de equações .............................................................................................................................. 214

Seção 46.1: Equação Linear ................................................................................................................................... 214


Seção 46.2: Equação não linear .......................................................................................................................... 216

Capítulo 47: Maior Subsequência Comum ............................................................................................. 220

Seção 47.1: Explicação da maior subsequência comum ..................................................................................... 220

Capítulo 48: Maior Subsequência Crescente ......................................................................................... 225

Seção 48.1: Informações básicas sobre a maior subsequência crescente .................................................. .................... 225

Capítulo 49: Verifique se duas strings são anagramas .......................................................................................... 228

Seção 49.1: Entrada e saída de amostra .................................................................................................................... 228


Seção 49.2: Código genérico para anagramas ......................................................................................................... 229

Capítulo 50: Triângulo de Pascal ............................................................................................................................. 231

Seção 50.1: Triângulo de Pascal em C .................................................. .................................................. ......................... 231

Capítulo 51: Algoritmo: Imprima uma matriz m*n ao quadrado.................................................. ........................... 232

Seção 51.1: Exemplo de amostra .............................................................................................................................. 232


Seção 51.2: Escreva o código genérico .................................................. .................................................. ................... 232

Capítulo 52: Exponenciação de matrizes .............................................................................................................. 233

Seção 52.1: Exponenciação de matrizes para resolver problemas de exemplo .................................................. ....................... 233

Capítulo 53: Algoritmo limitado em tempo polinomial para cobertura mínima de vértices ........................ 237

Seção 53.1: Pseudocódigo do algoritmo .................................................. .................................................. .................. 237

Capítulo 54: Distorção Temporal Dinâmica .......................................................................................................... 238

Seção 54.1: Introdução à distorção dinâmica do tempo .......................................................................................... 238


Capítulo 55: Transformada rápida de Fourier
.................................................. .................................................. .......... 242
Seção 55.1: FFT de base 2 .................................................. .................................................. ........................................... 242
Seção 55.2: FFT inversa de base 2 .................................................. .................................................. ........................ 247

Apêndice A: Pseudocódigo .................................................. .................................................. ................................... 249


Seção A.1: Ações variáveis .................................................. .................................................. ........................ 249
Seção A.2: Funções .................................................. .................................................. ........................................... 249
Créditos .................................................. .................................................. .................................................. ...................... 250

Você também pode gostar.................................................. .................................................. .................................................. 252


Machine Translated by Google

Sobre

Sinta-se à vontade para compartilhar este PDF com qualquer pessoa


gratuitamente. A versão mais recente deste livro pode ser baixada em:

[Link]

Este livro Algorithms Notes for Professionals foi compilado da documentação do Stack Overflow ,
o conteúdo é escrito pelas maravilhosas pessoas do Stack Overflow.
O conteúdo do texto é liberado sob Creative Commons BY-SA, veja os créditos no final deste livro
que contribuíram para os vários capítulos. As imagens podem ser protegidas por direitos autorais de
seus respectivos proprietários, a menos que especificado de outra forma

Este é um livro gratuito não oficial criado para fins educacionais e não é afiliado a grupos ou
empresas oficiais de Algoritmos nem ao Stack Overflow. Todas as marcas comerciais e marcas
registradas são propriedade de seus respectivos
donos de empresas

As informações apresentadas neste livro não são garantidas como corretas ou precisas, use
por sua conta e risco

Por favor, envie feedback e correções para web@[Link]

Notas de algoritmos do [Link] para profissionais 1


Machine Translated by Google

Capítulo 1: Introdução aos algoritmos


Seção 1.1: Um exemplo de problema algorítmico
Um problema algorítmico é especificado descrevendo o conjunto completo de instâncias nas quais ele deve trabalhar e de sua
saída após a execução em uma dessas instâncias. Essa distinção, entre um problema e uma instância de um problema, é
fundamental. O problema algorítmico conhecido como classificação é definido como segue: [Skiena:2008:ADM:1410219]

Problema:
Classificação Entrada: Uma sequência de n chaves, um.
a_1, a_2, ..., Saída: A reordenação da sequência de entrada de modo que a'_1 <= a'_2...<= <= a'_{n-1} <= a'_n

Uma instância de classificação pode ser uma matriz de strings, como { Haskell, Emacs } ou uma sequência de números, como
{ 154, 245, 1337 }.

Seção 1.2: Introdução ao algoritmo Fizz Buzz simples em Swift

Para aqueles que são novos em programação em Swift e aqueles que vêm de diferentes bases de programação, como Python ou
Java, este artigo deve ser bem útil. Nesta postagem, discutiremos uma solução simples para implementar algoritmos swift.

Zumbido efervescente

Você pode ter visto Fizz Buzz escrito como Fizz Buzz, FizzBuzz ou Fizz-Buzz; todos eles se referem à mesma coisa. Essa "coisa" é o
principal tópico de discussão hoje. Primeiro, o que é FizzBuzz?

Essa é uma pergunta comum que surge em entrevistas de emprego.

Imagine uma série de números de 1 a 10.

1 2 3 4 5 6 7 8 9 10

Fizz e Buzz referem-se a qualquer número que seja múltiplo de 3 e 5, respectivamente. Em outras palavras, se um número for divisível por 3,
ele é substituído por fizz; se um número for divisível por 5, ele é substituído por buzz. Se um número for simultaneamente múltiplo de 3 E 5, o
número é substituído por "fizz buzz". Em essência, ele emula o famoso jogo infantil "fizz buzz".

Para resolver esse problema, abra o Xcode para criar um novo playground e inicialize um array como abaixo:

// por exemplo deixe


número = [1,2,3,4,5] // aqui 3 é fizz e
5 é buzz

Para encontrar todo o fizz e buzz, precisamos iterar pelo array e verificar quais números são fizz e quais são buzz. Para fazer
isso, crie um loop for para iterar pelo array que inicializamos:

para num em número {


// Corpo e cálculo vão aqui
}

Depois disso, podemos simplesmente usar a condição "if else" e o operador de módulo no swift ie - % para localizar o fizz e o buzz

Notas de algoritmos do [Link] para profissionais 2


Machine Translated by Google

para num em número


{ se num % 3 == 0
{ print("\(num) fizz") }
senão
{ print(num)
}
}

Ótimo! Você pode ir ao console de depuração no playground do Xcode para ver a saída. Você verá que os "fizzes" foram classificados em seu
array.

Para a parte do Buzz, usaremos a mesma técnica. Vamos tentar antes de rolar o artigo — você pode conferir seus resultados com este artigo
quando terminar de fazer isso.

para num em número


{ se num % 3 == 0
{ print("\(num) fizz") }
senão se num % 5 == 0
{ print("\(num) buzz") }
senão
{ print(num)
}
}

Verifique a saída!

É bem direto — você dividiu o número por 3, fizz e dividiu o número por 5, buzz. Agora, aumente os números na matriz

deixe o número = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

Aumentamos o intervalo de números de 1-10 para 1-15 para demonstrar o conceito de "fizz buzz". Como 15 é um múltiplo de 3 e 5, o número deve
ser substituído por "fizz buzz". Tente você mesmo e confira a resposta!

Aqui está a solução:

para num em número


{ se num % 3 == 0 && num % 5 == 0
{ print("\(num) fizz buzz")
} senão se num % 3 == 0
{ print("\(num) fizz") }
senão se num % 5 == 0
{ print("\(num) buzz") }
senão
{ print(num)
}
}

Espere... ainda não acabou! O propósito do algoritmo é personalizar o tempo de execução corretamente. Imagine se o intervalo aumentasse de
1-15 para 1-100. O compilador verificaria cada número para determinar se ele é divisível por 3 ou 5. Ele então executaria os números novamente
para verificar se eles são divisíveis por 3 e 5. O código essencialmente teria que executar cada número na matriz duas vezes — ele teria que
executar os números por 3 primeiro e depois executá-los por 5. Para acelerar o processo, podemos simplesmente dizer ao nosso código para
dividir os números por 15 diretamente.

Aqui está o código final:

para num em número {

Notas de algoritmos do [Link] para profissionais 3


Machine Translated by Google

se num % 15 == 0
{ print("\(num) zumbido efervescente")
} senão se num % 3 == 0
{ print("\(num) fizz") }
senão se num % 5 == 0 { print("\
(num) buzz") } senão
{ print(num)

}
}

Tão simples quanto isso, você pode usar qualquer idioma de sua escolha e começar

Aproveite a codificação

Notas de algoritmos do [Link] para profissionais 4


Machine Translated by Google

Capítulo 2: Complexidade do Algoritmo

Seção 2.1: Notação Big-Theta


Ao contrário da notação Big-O, que representa apenas o limite superior do tempo de execução de algum algoritmo, Big-Theta é uma notação
tight bound; limite superior e inferior. Tight bound é mais preciso, mas também mais difícil de calcular.

A notação Big-Theta é simétrica: f(x) = ÿ(g(x)) <=> g(x) = ÿ(f(x))

Uma maneira intuitiva de entender isso é que f(x) = ÿ(g(x)) significa que os gráficos de f(x) e g(x) crescem na mesma taxa, ou
que os gráficos 'se comportam' de forma semelhante para valores grandes o suficiente de x.

A expressão matemática completa da notação Big-Theta é a seguinte:


ÿ(f(x)) = {g: N0 -> R e c1, c2, n0 > 0, onde c1 < abs(g(n) / f(n)), para cada n > n0 e abs é o valor absoluto }

Um exemplo

Se o algoritmo para a entrada n leva 42n^2 + 25n + 4 operações para terminar, dizemos que é O(n^2), mas também é O(n^3)
e O(n^100). No entanto, é ÿ(n^2) e não é ÿ(n^3), ÿ(n^4) etc. Algoritmo que é ÿ(f(n)) também é O(f(n)), mas
não vice-versa!

Definição matemática formal

ÿ(g(x)) é um conjunto de funções.

ÿ(g(x)) = {f(x) tal que existem constantes positivas c1, c2, N tais que 0 <= c1*g(x) <= f(x)
<= c2*g(x) para todos os x > N}

Como ÿ(g(x)) é um conjunto, poderíamos escrever f(x) ÿ ÿ(g(x)) para indicar que f(x) é um membro de ÿ(g(x)). Em vez disso,
geralmente escreverá f(x) = ÿ(g(x)) para expressar a mesma noção - essa é a maneira comum.

Sempre que ÿ(g(x)) aparece em uma fórmula, nós o interpretamos como representando alguma função anônima que não conhecemos.
cuidado para nomear. Por exemplo, a equação T(n) = T(n/2) + ÿ(n), significa T(n) = T(n/2) + f(n) onde f(n) é um
função no conjunto ÿ(n).

Sejam f e g duas funções definidas em algum subconjunto dos números reais. Escrevemos f(x) = ÿ(g(x)) como
x->infinito se e somente se houver constantes positivas K e L e um número real x0 tal que seja válido:

K|g(x)| <= f(x) <= L|g(x)| para todo x >= x0.

A definição é igual a:

f(x) = O(g(x)) e f(x) = ÿ(g(x))

Um método que usa limites

se limit(x->infinito) f(x)/g(x) = c ÿ (0,ÿ) ou seja, o limite existe e é positivo, então f(x) = ÿ(g(x))

Classes de complexidade comuns

Notação de Nome n = 10 n = 100


Constante ÿ(1) 1 1

Logarítmico ÿ(log(n)) ÿ(n) 3 7

Linear 10 100

Notas de algoritmos do [Link] para profissionais 5


Machine Translated by Google

Linearítmico ÿ(n*log(n)) 30 700

Quadrático ÿ(n^2) 100 10 000

Exponencial ÿ(2^n) 1 024 1.267650e+ 30

Fatorial ÿ(n!) 3 628 800 9.332622e+157

Seção 2.2: Comparação das notações assintóticas


Sejam f(n) e g(n) duas funções definidas no conjunto dos números reais positivos, c, c1, c2, n0 são números reais positivos.
constantes.

Notação
f(n) = f(n) =
f(n) = O(g(n)) f(n) = ÿ(g(n)) f(n) = ÿ(g(n))
o(g(n)) ÿ(g(n))
ÿc>
ÿc> 0, ÿ
0, ÿ n0 >
n0 > 0 0:ÿ
Formal ÿ c1, c2 > 0, ÿ n0 > 0 : ÿ n ÿ n0, 0 ÿ c1 g(n) ÿ :ÿn nÿ
definição ÿ c > 0, ÿ n0 > 0 : ÿ n ÿ n0, 0 ÿ f(n) ÿ c g(n) ÿ c > 0, ÿ n0 > 0 : ÿ n ÿ n0, 0 ÿ c g(n) ÿ f(n)
f(n) ÿ c2 g(n) ÿ n0, n0, 0
0ÿ ÿc
f(n) < g(n)
c g(n) <
f(n)

Analogia
entre o
assintótico
comparação um ÿ b um ÿ b um = b um < ba > b
de f, g e
números reais
um, b
7n^2
5n^2 = =
Exemplo 7n + 10 = O(n^2 + n - 9) n^3 - 34 = ÿ(10n^2 - 7n + 1) 1/2 n^2 - 7n = ÿ(n^2)
o(n^3)
ÿ(n)

Gráfico
interpretação

As notações assintóticas podem ser representadas em um diagrama de Venn da seguinte forma:

Ligações

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introdução aos Algoritmos.

Seção 2.3: Notação Big-Omega


A notação ÿ é usada para limite inferior assintótico.

Notas de algoritmos do [Link] para profissionais 6


Machine Translated by Google

Definição formal

Sejam f(n) e g(n) duas funções definidas no conjunto dos números reais positivos. Escrevemos f(n) = ÿ(g(n)) se houver
constantes positivas c e n0 tais que:

0 ÿ c g(n) ÿ f(n) para todo n ÿ n0.

Notas

f(n) = ÿ(g(n)) significa que f(n) cresce assintoticamente não mais devagar que g(n). Também podemos dizer sobre ÿ(g(n))
quando a análise do algoritmo não é suficiente para a declaração sobre ÿ(g(n)) ou / e O(g(n)).

Das definições das notações segue o teorema:

Para duas funções quaisquer f(n) e g(n), temos f(n) = ÿ(g(n)) se e somente se f(n) = O(g(n)) e f(n) = ÿ(g(n)).

Graficamente, a notação ÿ pode ser representada da seguinte forma:

Por exemplo, vamos ter f(n) = 3n^2 + 5n - 4. Então f(n) = ÿ(n^2). Também é correto f(n) = ÿ(n), ou mesmo f(n) = ÿ(1).

Outro exemplo para resolver o algoritmo de correspondência perfeita: se o número de vértices for ímpar, a saída será "Sem correspondência
perfeita". Caso contrário, tente todas as correspondências possíveis.

Gostaríamos de dizer que o algoritmo requer tempo exponencial, mas na verdade você não pode provar um limite inferior
ÿ(n^2) usando a definição usual de ÿ, já que o algoritmo roda em tempo linear para n ímpares. Em vez disso,
deveríamos definir f(n)=ÿ(g(n)) dizendo para alguma constante c>0, f(n)ÿ c g(n) para infinitamente muitos n.
Isso dá uma boa correspondência entre limites superior e inferior: f(n)=ÿ(g(n)) iff f(n) != o(g(n)).

Referências

A definição formal e o teorema foram retirados do livro "Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introduction to
Algorithms".

Notas de algoritmos do [Link] para profissionais 7


Machine Translated by Google

Capítulo 3: Notação Big-O


Definição

A notação Big-O é, em sua essência, uma notação matemática usada para comparar a taxa de convergência de funções.
Sejam n -> f(n) e n -> g(n) funções definidas sobre os números naturais. Então dizemos que f = O(g) se e somente se f(n)/g(n) é
limitado quando n se aproxima do infinito. Em outras palavras, f = O(g) se e somente se existe uma constante A, tal que para todo
n, f(n)/g(n) <= A.

Na verdade, o escopo da notação Big-O é um pouco mais amplo em matemática, mas para simplificar, eu o reduzi ao que é usado na análise de
complexidade de algoritmos: funções definidas em naturais, que têm valores diferentes de zero, e o caso de n crescendo até o infinito.

O que isso significa ?

Vamos pegar o caso de f(n) = 100n^2 + 10n + 1 e g(n) = n^2. É bem claro que ambas as funções tendem ao infinito assim como n
tende ao infinito. Mas às vezes saber o limite não é suficiente, e também queremos saber a velocidade na qual as funções se aproximam
de seu limite. Noções como Big-O ajudam a comparar e classificar funções por sua velocidade de
convergência.

Vamos descobrir se f = O(g) aplicando a definição. Temos f(n)/g(n) = 100 + 10/n + 1/n^2. Como 10/n é 10 quando n é 1 e está
diminuindo, e como 1/n^2 é 1 quando n é 1 e também está diminuindo, temos ÿf(n)/g(n) <= 100 + 10 + 1 = 111. A definição é satisfeita
porque encontramos um limite de f(n)/g(n) (111) e então f = O(g) (dizemos que f é um Big-O de n^2).

Isso significa que f tende ao infinito aproximadamente na mesma velocidade que g. Agora, isso pode parecer uma coisa estranha de se dizer,
porque o que descobrimos é que f é no máximo 111 vezes maior que g, ou em outras palavras, quando g cresce em 1, f cresce no máximo 111.
Pode parecer que crescer 111 vezes mais rápido não é "aproximadamente a mesma velocidade". E, de fato, a notação Big-O não é uma
maneira muito precisa de classificar a velocidade de convergência de funções, e é por isso que em matemática usamos a relação de
equivalência quando queremos uma estimativa precisa de velocidade. Mas para os propósitos de separar algoritmos em grandes
classes de velocidade, Big-O é o suficiente. Não precisamos separar funções que crescem um número fixo de vezes mais rápido que umas das
outras, mas apenas funções que crescem infinitamente mais rápido que umas das outras.
Por exemplo, se tomarmos h(n) = n^2*log(n), vemos que h(n)/g(n) = log(n), que tende ao infinito com n, então h não é O(n^2), porque h cresce
infinitamente mais rápido que n^2.

Agora preciso fazer uma observação lateral: você deve ter notado que se f = O(g) e g = O(h), então f = O(h). Por exemplo, em
nosso caso, temos f = O(n^3), e f = O(n^4)... Na análise de complexidade de algoritmos, frequentemente dizemos f = O(g) para significar
que f = O(g) e g = O(f), o que pode ser entendido como "g é o menor Big-O para f". Em matemática, dizemos que tais
funções são Big-Thetas umas das outras.

Como é usado?

Ao comparar o desempenho do algoritmo, estamos interessados no número de operações que um algoritmo realiza. Isso é chamado de
complexidade de tempo. Neste modelo, consideramos que cada operação básica (adição, multiplicação, comparação, atribuição,
etc.) leva um tempo fixo, e contamos o número dessas operações. Normalmente podemos expressar esse número como uma função do
tamanho da entrada, que chamamos de n. E, infelizmente, esse número geralmente cresce até o infinito com n (se não crescer, dizemos que
o algoritmo é O(1)). Separamos nossos algoritmos em grandes classes de velocidade definidas por Big-O: quando falamos sobre um
"algoritmo O(n^2)", queremos dizer que o número de operações que ele realiza, expresso como uma função de n, é um O(n^2). Isso diz que
nosso algoritmo é aproximadamente tão rápido quanto um algoritmo que faria um número de operações igual ao quadrado do tamanho
de sua entrada, ou mais rápido. A parte "ou mais rápido" está lá porque usei Big-O em vez de Big-Theta, mas normalmente as pessoas dizem
Big-O para significar Big-Theta.

Notas de algoritmos do [Link] para profissionais 8


Machine Translated by Google

Ao contar operações, geralmente consideramos o pior caso: por exemplo, se temos um loop que pode ser executado no máximo n vezes
e que contém 5 operações, o número de operações que contamos é 5n. Também é possível considerar a complexidade média do
caso.

Nota rápida: um algoritmo rápido é aquele que executa poucas operações, então se o número de operações cresce até o infinito mais
rápido, então o algoritmo é mais lento: O(n) é melhor que O(n^2).

Às vezes também estamos interessados na complexidade de espaço do nosso algoritmo. Para isso, consideramos o número de
bytes na memória ocupados pelo algoritmo como uma função do tamanho da entrada, e usamos Big-O da mesma forma.

Seção 3.1: Um Loop Simples


A função a seguir encontra o elemento máximo em uma matriz:

int find_max(const int *array, tamanho_t len) { int max =


INT_MIN; para
(tamanho_t i = 0; i < len; i++) { se (max < array[i])
{ max = array[i];

} retornar máximo;
}

O tamanho da entrada é o tamanho do array, que chamei de len no código.

Vamos contar as operações.

int max = INT_MIN;


tamanho_t i = 0;

Essas duas atribuições são feitas apenas uma vez, então são 2 operações. As operações que são colocadas em loop são:

se (máx. < array[i]) i++;

máximo = array[i]

Como há 3 operações no loop, e o loop é feito n vezes, adicionamos 3n às nossas 2 operações já existentes para obter 3n + 2.
Então nossa função leva 3n + 2 operações para encontrar o máximo (sua complexidade é 3n + 2). Este é um polinômio onde o termo de
crescimento mais rápido é um fator de n, então é O(n).

Você provavelmente notou que "operação" não é muito bem definida. Por exemplo, eu disse que if (max < array[i]) era uma operação, mas
dependendo da arquitetura esta declaração pode compilar para, por exemplo, três instruções: uma leitura de memória, uma comparação e
uma ramificação. Eu também considerei todas as operações como as mesmas, embora, por exemplo, as operações de memória
sejam mais lentas do que as outras, e seu desempenho varie muito devido, por exemplo, aos efeitos de cache. Eu também ignorei
completamente a declaração return, o fato de que um quadro será criado para a função, etc. No final, não importa para a análise de
complexidade, porque qualquer maneira que eu escolher para contar as operações, isso mudará apenas o coeficiente do fator n e a
constante, então o resultado ainda será O(n).
A complexidade mostra como o algoritmo é dimensionado com o tamanho da entrada, mas não é o único aspecto do desempenho!

Seção 3.2: Um loop aninhado


A função a seguir verifica se uma matriz possui duplicatas pegando cada elemento e, em seguida, iterando em toda a matriz para ver
se o elemento está lá

Notas de algoritmos do [Link] para profissionais 9


Machine Translated by Google

_Bool contém_duplicatas(const int *array, tamanho_t len) {


para (int i = 0; i < len - 1; i++) { para (int j = 0; j <
len; j++) { se (i != j && array[i] == array[j])
{ retornar 1;

}
}

} retornar 0;
}

O loop interno realiza em cada iteração um número de operações que é constante com n. O loop externo também realiza algumas operações
constantes e executa o loop interno n vezes. O loop externo em si é executado n vezes. Então as operações dentro do loop interno são
executadas n^2 vezes, as operações no loop externo são executadas n vezes e a atribuição a i é feita uma vez. Assim, a complexidade será
algo como an^2 + bn + c, e como o termo mais alto é n^2, a notação O é O(n^2).

Como você deve ter notado, podemos melhorar o algoritmo evitando fazer as mesmas comparações várias vezes.
Podemos começar com i + 1 no loop interno, porque todos os elementos anteriores a ele já terão sido verificados em relação a todos
os elementos do array, incluindo aquele no índice i + 1. Isso nos permite descartar a verificação i == j .

_Bool faster_contains_duplicates(const int *array, size_t len) {


para (int i = 0; i < len - 1; i++) {
para (int j = i + 1; j < len; j++) { se (matriz[i] ==
matriz[j]) { retornar 1;

}
}

} retornar 0;
}

Obviamente, esta segunda versão faz menos operações e, portanto, é mais eficiente. Como isso se traduz na notação Big-O? Bem,
...
agora o corpo do loop interno é executado 1 + 2 + + n - 1 = n(n-1)/2 vezes. Este ainda é um polinômio de segundo grau e, portanto, ainda é
apenas O(n^2). Nós claramente diminuímos a complexidade, já que dividimos aproximadamente por 2 o número de operações que estamos
fazendo, mas ainda estamos na mesma classe de complexidade definida pelo Big-O. Para diminuir a complexidade para uma classe menor,
precisaríamos dividir o número de operações por algo que tende ao infinito com n.

Seção 3.3: Tipos de algoritmos O(log n)


Digamos que temos um problema de tamanho n. Agora, para cada passo do nosso algoritmo (que precisamos escrever), nosso
problema original se torna metade do seu tamanho anterior (n/2).

Então, a cada passo, nosso problema se torna metade.

Problema de Etapa
1 n/2

2 n/4
3 n/8
4 n/16

Quando o espaço do problema é reduzido (ou seja, resolvido completamente), ele não pode ser reduzido mais (n se torna igual a 1)
após sair da condição de verificação.

Notas de algoritmos do [Link] para profissionais 10


Machine Translated by Google

1. Digamos que no k-ésimo passo ou número de operações:

tamanho do problema = 1

2. Mas sabemos que no passo k, o tamanho do nosso problema deve ser:

tamanho do problema = n/2k

3. De 1 e 2:

n/2k = 1 ou

n = 2k

4. Pegue o log em ambos os lados

logaritmo n = k logaritmo2

ou

k = logaritmo n / logaritmo 2

5. Usando a fórmula logx m / logx n = logn m

k = log2 n

ou simplesmente k = log n

Agora sabemos que nosso algoritmo pode ser executado no máximo até log n, portanto a complexidade de tempo surge como

O(log n)

Um exemplo muito simples em código para dar suporte ao texto acima é:

para(int i=1; i<=n; i=i*2) {

// executar alguma operação


}

Então, agora, se alguém lhe perguntar se n é 256, quantas etapas esse loop (ou qualquer outro algoritmo que reduza o tamanho do problema pela metade)

executará, você pode calcular facilmente.

k = log2 256

k = log2 2 8 ( => logaa = 1)

k=8

Outro exemplo muito bom de caso semelhante é o Algoritmo de Busca Binária.

Notas de algoritmos do [Link] para profissionais 11


Machine Translated by Google

int bSearch(int arr[],int tamanho,int item){


int baixo=0;
int alto=tamanho-1;

enquanto(baixo<=alto)
{ médio=baixo+(alto-baixo)/
2; se(arr[médio]==item)
retornar
médio; senão

se(arr[médio]<item)
baixo=médio+1; senão alto=médio-1; }
return –1;// Resultado sem sucesso
}

Seção 3.4: Um exemplo de O(log n)


Introdução

Considere o seguinte problema:

L é uma lista ordenada contendo n inteiros assinados ( sendo n grande o suficiente), por exemplo [-5, -2, -1, 0, 1, 2, 4] (aqui, n
tem um valor de 7). Se L é conhecido por conter o inteiro 0, como você pode encontrar o índice de 0?

Abordagem ingênua

A primeira coisa que vem à mente é apenas ler cada índice até que 0 seja encontrado. No pior caso, o número de
operações é n, então a complexidade é O(n).

Isso funciona bem para valores pequenos de n, mas existe uma maneira mais eficiente?

Dicotomia

Considere o seguinte algoritmo (Python3):

a=0
b = n-1
enquanto Verdadeiro:

h = (a+b)//2 ## // é a divisão inteira, então h é um inteiro


se L[h] == 0:
retornar h
elif L[h] > 0:
b=h
elif L[h] < 0:
um = h

a e b são os índices entre os quais 0 deve ser encontrado. Cada vez que entramos no loop, usamos um índice entre a
e b e usá-lo para restringir a área a ser pesquisada.

No pior caso, temos que esperar até que a e b sejam iguais. Mas quantas operações isso leva? Não n, porque
cada vez que entramos no loop, dividimos a distância entre a e b por cerca de dois. Em vez disso, a complexidade é O(log
e).

Explicação

Nota: Quando escrevemos "log", queremos dizer o logaritmo binário, ou log base 2 (que escreveremos "log_2"). Como O(log_2 n) = O(log
n) (você pode fazer as contas) usaremos "log" em vez de "log_2".

Notas de algoritmos do [Link] para profissionais 12


Machine Translated by Google

Vamos chamar x de número de operações: sabemos que 1 = n / (2^x).

Então 2^x = n, então x = log n

Conclusão

Ao se deparar com divisões sucessivas (seja por dois ou por qualquer número), lembre-se de que a complexidade é logarítmica.

Notas de algoritmos do [Link] para profissionais 13


Machine Translated by Google

Capítulo 4: Árvores

Seção 4.1: Representação típica de árvore anária


Normalmente representamos uma árvore anária (uma com filhos potencialmente ilimitados por nó) como uma árvore binária (uma com
exatamente dois filhos por nó). O "próximo" filho é considerado um irmão. Note que se uma árvore for binária, essa
representação cria nós extras.

Então iteramos sobre os irmãos e recursivamente descemos pelos filhos. Como a maioria das árvores é relativamente rasa - muitos
filhos, mas apenas alguns níveis de hierarquia, isso dá origem a um código eficiente. Observe que as genealogias humanas são
uma exceção (muitos níveis de ancestrais, apenas alguns filhos por nível).

Se necessário, ponteiros de volta podem ser mantidos para permitir que a árvore seja escalada. Esses são mais difíceis de manter.

Observe que é comum ter uma função para chamar na raiz e uma função recursiva com parâmetros extras, neste caso, profundidade da
árvore.

estrutura nó {

struct nó *próximo; struct nó


*filho; std::string dados;

void printtree_r(struct nó *nó, int profundidade) {

int eu;

enquanto(nó)
{
se(nó->filho) {

para(i=0;i<profundidade*3;i++)
printf(" ");
printf("{\n"):
printtree_r(nó->filho, profundidade +1);
para(i=0;i<profundidade*3;i++)
printf(" "); printf("{\n"):

para(i=0;i<profundidade*3;i++)
printf(" ");
printf("%s\n", nó->data.c_str());

nó = nó->próximo;
}
}
}

vazio printtree(nó *raiz) {

printree_r(raiz, 0);
}

Seção 4.2: Introdução

Árvores são um subtipo da estrutura de dados do gráfico nó-aresta mais geral.

Notas de algoritmos do [Link] para profissionais 14


Machine Translated by Google

Para ser uma árvore, um gráfico deve satisfazer dois requisitos:

É acíclico. Não contém ciclos (ou "loops").


Está conectado. Para qualquer nó dado no gráfico, cada nó é alcançável. Todos os nós são alcançáveis por meio de um caminho no
gráfico.

A estrutura de dados em árvore é bem comum na ciência da computação. Árvores são usadas para modelar muitas estruturas de dados algorítmicas diferentes, como

árvores binárias comuns, árvores rubro-negras, árvores B, árvores AB, árvores 23, Heap e tries.

é comum se referir a uma árvore como uma árvore enraizada por:

escolhendo 1 célula para ser chamada de


`Raiz` pintando a `Raiz` no topo criando
uma camada inferior para cada célula no gráfico dependendo da distância da raiz - quanto maior a distância, mais baixas as
células (exemplo acima)

símbolo comum para árvores: T

Seção 4.3: Para verificar se duas árvores binárias são iguais ou não
1. Por exemplo, se as entradas forem:

Exemplo:1

um)

Notas de algoritmos do [Link] para profissionais 15


Machine Translated by Google

b)

A saída deve ser verdadeira.

Exemplo:2

Se as entradas forem:

um)

b)

A saída deve ser falsa.

Pseudocódigo para o mesmo:

boolean sameTree(nó raiz1, nó raiz2){

Notas de algoritmos do [Link] para profissionais 16


Machine Translated by Google

se(root1 == NULL && root2 == NULL) retornar


verdadeiro;

se(raiz1 == NULL || raiz2 == NULL) retornar falso;

se(raiz1->dados == raiz2->dados &&


mesmaÁrvore(raiz1->esquerda,raiz2->esquerda)
&& sameTree(raiz1->direita, raiz2->direita)) retorna
verdadeiro;

Notas de algoritmos do [Link] para profissionais 17


Machine Translated by Google

Capítulo 5: Árvores de busca binárias


Árvore binária é uma árvore em que cada nó nela tem no máximo dois filhos. Árvore de busca binária (BST) é uma árvore binária cujos
elementos são posicionados em ordem especial. Em cada BST, todos os valores (ou seja, chave) na subárvore esquerda são menores que os
valores na subárvore direita.

Seção 5.1: Árvore de busca binária - Inserção (Python)


Esta é uma implementação simples de Inserção de Árvore de Busca Binária usando Python.

Um exemplo é mostrado abaixo:

Após o trecho de código, cada imagem mostra a visualização da execução, o que facilita a visualização de como esse código funciona.

classe Nó:
def __init__(self, val): self.l_child =
Nenhum self.r_child = Nenhum
[Link] = val

def insert(root, node): se root for


Nenhum:
raiz = nó senão:
se
[Link] > nó.dados:
se root.l_child for Nenhum: root.l_child
= nó
outro:
inserir(root.l_child, nó)
outro:
se root.r_child for Nenhum:

Notas de algoritmos do [Link] para profissionais 18


Machine Translated by Google

raiz.r_child = nó
outro:
inserir(root.r_child, nó)

def in_order_print(root): se não for root:

retornar

in_order_print(root.l_child) imprimir [Link]


in_order_print(root.r_child)

def pre_order_print(root): se não for root:

retornar

impressão [Link]
pre_order_print(root.l_child) pre_order_print(root.r_child)

Notas de algoritmos do [Link] para profissionais 19


Machine Translated by Google

Seção 5.2: Árvore de pesquisa binária - Exclusão (C++)

Antes de começar com a exclusão, eu só quero colocar algumas luzes sobre o que é uma árvore de busca binária (BST). Cada nó em
uma BST pode ter no máximo dois nós (filho esquerdo e direito). A subárvore esquerda de um nó tem uma chave menor ou igual à
chave do nó pai. A subárvore direita de um nó tem uma chave maior que a chave do nó pai.

Excluindo um nó em uma árvore, mantendo sua propriedade Árvore de pesquisa binária.

Há três casos a serem considerados ao excluir um nó.

Caso 1: O nó a ser excluído é o nó folha. (Nó com valor 22).


Caso 2: O nó a ser excluído tem um filho. (Nó com valor 26).
Caso 3: O nó a ser excluído tem ambos os filhos. (Nó com valor 49).

Explicação dos casos:

1. Quando o nó a ser excluído for um nó folha, basta excluir o nó e passar nullptr para seu nó pai.
2. Quando um nó a ser excluído tiver apenas um filho, copie o valor do filho para o valor do nó e exclua o filho (convertido para
o caso 1)
3. Quando um nó a ser excluído tem dois filhos, o mínimo de sua subárvore direita pode ser copiado para o nó e, em seguida, o
valor mínimo pode ser excluído da subárvore direita do nó (convertido para o caso 2)

Observação: o mínimo na subárvore direita pode ter no máximo um filho, e o filho direito, se tiver o filho esquerdo, significa que não é
o valor mínimo ou não segue a propriedade BST.

Notas de algoritmos do [Link] para profissionais 20


Machine Translated by Google

A estrutura de um nó em uma árvore e o código para exclusão:

estrutura nó {

int dados; nó
*esquerda, *direita;
};

nó* delete_node(nó *raiz, int dados) {

if(root == nullptr) return root; else if(dados < root-


>data) root->left = delete_node(root->left, data); else if(dados > root->data) root->right = delete_node(root->right,
data);

outro
{
if(root->left == nullptr && root->right == nullptr) // Caso 1 {

livre(raiz); raiz =
nullptr;

} senão se(raiz->esquerda == nullptr) { // Caso 2

nó* temp = raiz; raiz= raiz-


>direita; livre(temp);

} senão se(raiz->direita == nullptr) { // Caso 2

nó* temp = raiz; raiz = raiz-


>esquerda; livre(temp);

} // Caso 3
outro {
nó* temp = raiz->direita;

enquanto(temp->esquerda != nullptr) temp = temp->esquerda;

raiz->dados = temp->dados; raiz-


>direita = delete_node(raiz->direita, temp->dados);
}

} retornar raiz;
}

A complexidade de tempo do código acima é O(h), onde h é a altura da árvore.

Seção 5.3: Menor ancestral comum em um BST

Considere o BST:

Notas de algoritmos do [Link] para profissionais 21


Machine Translated by Google

O menor ancestral comum de 22 e 26 é 24

O menor ancestral comum de 26 e 49 é 46

O menor ancestral comum de 22 e 24 é 24

A propriedade da árvore de pesquisa binária pode ser usada para encontrar nós ancestrais mais baixos

Pseudocódigo:

lowerCommonAncestor(raiz,nó1, nó2){

se(raiz == NULL)
retornar NULL;

senão se(nó1->dados == raiz->dados || nó2->dados== raiz->dados)


retornar raiz;

senão se((nó1->dados <= raiz->dados && nó2->dados > raiz->dados)


|| (nó2->dados <= raiz->dados && nó1->dados > raiz->dados)){

retornar raiz;
}

senão se(raiz->dados > max(nó1->dados,nó2->dados)){


retornar lowerCommonAncestor(raiz->esquerda, nó1, nó2);
}

senão
{ return lowestCommonAncestor(raiz->direita, nó1, nó2);
}
}

Seção 5.4: Árvore de busca binária - Python


classe Node(objeto): def
__init__(self, val): self.l_child =
Nenhum self.r_child = Nenhum
[Link] = val

classe BinarySearchTree(objeto):
def insert(self, raiz, nó):

Notas de algoritmos do [Link] para profissionais 22


Machine Translated by Google
se a raiz for Nenhuma:
nó de retorno

se [Link] < nó.val:


root.r_child = [Link](root.r_child, nó) senão: root.l_child =

[Link](root.l_child, nó)

retornar raiz

def in_order_place(self, root): se não for root:

retornar Nenhum
outro:
self.in_order_place(root.l_child) imprimir [Link]

self.in_order_place(root.r_child)

def pre_order_place(self, root): se não for root: retornar


Nenhum senão:

imprimir [Link]
self.pre_order_place(root.l_child)
self.pre_order_place(root.r_child)

def post_order_place(self, root): se não for root:

retornar Nenhum
outro:
self.post_order_place(root.l_child)
self.post_order_place(root.r_child) imprimir [Link]

""" Crie um nó diferente e insira dados nele"""

r = Nó(3) nó =
BinarySearchTree() lista de nós = [1, 8,
5, 12, 14, 6, 15, 7, 16, 8]

para nd em nodeList:
nó.inserir(r, Nó(nd))

imprimir "------Em ordem ---------" imprimir


(node.in_order_place(r)) imprimir "------Pré-
encomenda ---------" imprimir (node.pre_order_place(r))
imprimir "------Pós-encomenda ---------" imprimir
(node.post_order_place(r))

Notas de algoritmos do [Link] para profissionais 23


Machine Translated by Google

Capítulo 6: Verifique se uma árvore é BST ou não

Seção 6.1: Algoritmo para verificar se uma determinada árvore binária é BST
Uma árvore binária é BST se satisfaz qualquer uma das seguintes condições:

1. Está vazio 2.
Não tem subárvores

3. Para cada nó x na árvore, todas as chaves (se houver) na subárvore esquerda devem ser menores que a chave(x) e todas as chaves
(se houver) na subárvore direita devem ser maiores que a chave(x).

Então, um algoritmo recursivo simples seria:

is_BST(raiz): se raiz
== NULL:
retornar verdadeiro

// Verifica valores na subárvore esquerda se root-


>left != NULL:
max_key_in_left = find_max_key(root->left) se max_key_in_left >
root->key: retornar falso

// Verificar valores na subárvore direita se root-


>right != NULL: min_key_in_right =
find_min_key(root->right) se min_key_in_right < root->key: retornar
falso

retornar is_BST(raiz->esquerda) && is_BST(raiz->direita)

O algoritmo recursivo acima está correto, mas é ineficiente, porque percorre cada nó várias vezes.

Outra abordagem para minimizar as múltiplas visitas de cada nó é lembrar os valores mínimos e máximos possíveis das chaves na subárvore
que estamos visitando. Seja o valor mínimo possível de qualquer chave K_MIN e o valor máximo K_MAX. Quando começamos da raiz da
árvore, o intervalo de valores na árvore é [K_MIN,K_MAX]. Seja a chave do nó raiz x. Então o intervalo de valores na subárvore esquerda é
[K_MIN,x) e o intervalo de valores na subárvore direita é (x,K_MAX]. Usaremos essa ideia para desenvolver um algoritmo mais
eficiente.

is_BST(raiz, min, max): se raiz ==


NULL:
retornar verdadeiro

// a chave do nó atual está fora do intervalo? se root->key < min


|| root->key > max: retornar falso

// verifique se a subárvore esquerda e direita é BST return


is_BST(root->left,min,root->key-1) && is_BST(root->right,root->key+1,max)

Inicialmente será chamado de:

is_BST(minha_raiz_da_árvore,CHAVE_MIN,CHAVE_MAX)

Outra abordagem será fazer a travessia inorder da árvore binária. Se a travessia inorder produzir uma sequência ordenada de chaves,
então a árvore dada é uma BST. Para verificar se a sequência inorder está ordenada, lembre-se do valor de

Notas de algoritmos do [Link] para profissionais 24


Machine Translated by Google

nó visitado anteriormente e compará-lo com o nó atual.

Seção 6.2: Se uma determinada árvore de entrada segue a propriedade da árvore


de pesquisa binária ou não

Por exemplo

se a entrada for:

A saída deve ser falsa:

Como 4 na subárvore esquerda é maior que o valor raiz (3)

Se a entrada for:

A saída deve ser verdadeira

Notas de algoritmos do [Link] para profissionais 25


Machine Translated by Google

Capítulo 7: Travessias de Árvore Binária


Visitar um nó de uma árvore binária em alguma ordem específica é chamado de travessia.

Seção 7.1: Travessia de ordem de nível - Implementação


Por exemplo, se a árvore fornecida for:

A travessia da ordem de nível será

1234567

Imprimindo dados do nó nível por nível.

Código:

#incluir<iostream>
#incluir<fila>
#incluir<malloc.h>

usando namespace std;

estrutura nó{

int dados;
nó *esquerda;
nó *direita;
};

void levelOrder(struct nó *raiz){

se(raiz == NULL) retornar;

fila<nó *> Q;
[Link](raiz);

enquanto(![Link]()){
estrutura nó* curr = [Link](); cout<<
curr->data <<" "; if(curr->left !=
NULL) [Link](curr-> left);
se(atual->direita != NULL) [Link](atual-> direita);

[Link]();

Notas de algoritmos do [Link] para profissionais 26


Machine Translated by Google

} struct node* newNode(int dados) {

struct nó* nó = (struct nó*) malloc(sizeof(struct


nó));
nó->dados = dados; nó-
>esquerda = NULL; nó-
>direita = NULL;

retornar(nó);
}

int principal(){

struct node *root = newNode(1); root->left =


newNode(2); root->right = newNode(3); root->left-
>left = newNode(4); root->left->right = newNode(5);
root->right->left = newNode(6); root->right->right
= newNode(7);

printf(" A ordem de nível da travessia da árvore binária é \n");


levelOrder(root);

retornar 0;

A estrutura de dados da fila é usada para atingir o objetivo acima.

Seção 7.2: Percurso de pré-ordem, em ordem e pós-ordem de uma


árvore binária
Considere a árvore binária:

A travessia de pré-ordem (raiz) consiste em percorrer o nó, depois a subárvore esquerda do nó e depois a subárvore direita do nó.

Portanto, a travessia de pré-ordem da árvore acima será:

1245367

A travessia em ordem (raiz) consiste em percorrer a subárvore esquerda do nó, depois o nó e depois a subárvore direita do nó.

Notas de algoritmos do [Link] para profissionais 27


Machine Translated by Google

nó.

Portanto, a travessia em ordem da árvore acima será:

4251637

A travessia pós-ordem (raiz) consiste em percorrer a subárvore esquerda do nó, depois a subárvore direita e, por fim, o nó.

Portanto, a travessia pós-ordem da árvore acima será:

4526731

Notas de algoritmos do [Link] para profissionais 28


Machine Translated by Google

Capítulo 8: Menor ancestral comum de um


Árvore binária
O ancestral comum mais baixo entre dois nós n1 e n2 é definido como o nó mais baixo na árvore que possui n1
e n2 como descendentes.

Seção 8.1: Encontrando o menor ancestral comum


Considere a árvore:

O menor ancestral comum dos nós com valor 1 e 4 é 2

O menor ancestral comum dos nós com valor 1 e 5 é 3

O menor ancestral comum dos nós com valor 2 e 4 é 4

O menor ancestral comum dos nós com valor 1 e 2 é 2

Notas de algoritmos do [Link] para profissionais 29


Machine Translated by Google

Capítulo 9: Gráfico
Um gráfico é uma coleção de pontos e linhas conectando algum subconjunto (possivelmente vazio) deles. Os pontos de um gráfico são
chamados de vértices do gráfico, "nós" ou simplesmente "pontos". Similarmente, as linhas conectando os vértices de um gráfico são chamadas
de arestas do gráfico, "arcos" ou "linhas".

Um grafo G pode ser definido como um par (V,E), onde V é um conjunto de vértices e E é um conjunto de arestas entre os vértices E ÿ {(u,v) |
u, v ÿ V}.

Seção 9.1: Armazenando Grafos (Matriz de Adjacência)


Para armazenar um gráfico, dois métodos são comuns:

Matriz de Adjacência
Lista de Adjacências

Uma matriz de adjacência é uma matriz quadrada usada para representar um grafo finito. Os elementos da matriz indicam se pares
de vértices são adjacentes ou não no grafo.

Adjacente significa 'próximo ou adjacente a outra coisa' ou estar ao lado de algo. Por exemplo, seus vizinhos são adjacentes a você. Na
teoria dos grafos, se pudermos ir do nó A para o nó B, podemos dizer que o nó B é adjacente ao nó A. Agora aprenderemos como
armazenar quais nós são adjacentes a qual via Matriz de Adjacência. Isso significa que representaremos quais nós compartilham arestas
entre eles. Aqui, matriz significa matriz 2D.

Aqui você pode ver uma tabela ao lado do gráfico, esta é nossa matriz de adjacência. Aqui Matrix[i][j] = 1 representa que há uma aresta
entre i e j. Se não houver aresta, simplesmente colocamos Matrix[i][j] = 0.

Essas arestas podem ser ponderadas, como se pudessem representar a distância entre duas cidades. Então colocaremos o valor em
Matrix[i][j] em vez de colocar 1.

O gráfico descrito acima é Bidirecional ou Não Direcionado, ou seja, se podemos ir do nó 2 para o nó 1, também podemos ir do nó 1 para
o nó 2. Se o gráfico fosse Direcionado, então haveria um sinal de seta em um lado do gráfico. Mesmo assim, poderíamos representá-lo
usando matriz de adjacência.

Notas de algoritmos do [Link] para profissionais 30


Machine Translated by Google

Representamos os nós que não compartilham aresta por infinito. Uma coisa a ser notada é que, se o gráfico for não direcionado, a matriz se torna
simétrica.

O pseudocódigo para criar a matriz:

Procedimento AdjacencyMatrix(N): // N representa o número de nós


Matriz[N][N] para i
de 1 a N para j de 1 a N

Pegue a entrada -> Matriz[i][j] fim para


fim para

Também podemos preencher a Matrix usando esta maneira comum:

Procedimento AdjacencyMatrix(N, E): // N -> número de nós


Matriz[N][E] para i // E -> número de arestas
de 1 a E
entrada -> n1, n2, custo
Matriz[n1][n2] = custo
Matriz[n2][n1] = custo fim para

Para gráficos direcionados, podemos remover Matrix[n2][n1] = linha de custo.

As desvantagens de usar a Matriz de Adjacência:

Memória é um problema enorme. Não importa quantas arestas existam, sempre precisaremos de uma matriz de tamanho N * N, onde N é o
número de nós. Se houver 10000 nós, o tamanho da matriz será 4 * 10000 * 10000, cerca de 381 megabytes.
Isso é um enorme desperdício de memória se considerarmos gráficos que têm poucas arestas.

Suponha que queremos descobrir para qual nó podemos ir de um nó u. Precisaremos verificar toda a linha de u, o que leva muito tempo.

O único benefício é que podemos encontrar facilmente a conexão entre os nós UV e seus custos usando a Matriz de Adjacência.

Código Java implementado usando o pseudocódigo acima:

importar [Link];

classe pública Represent_Graph_Adjacency_Matrix {

vértices finais privados int ;

Notas de algoritmos do [Link] para profissionais 31


Machine Translated by Google

privado int[][] matriz_de_adjacência;

público Representar_Grafo_Matriz_de_Adjacência(int v) {

vértices = v;
matriz_de_adjacência = novo int[vértices + 1][vértices + 1];
}

público vazio makeEdge(int para, int de, int borda) {

tentar

adjacency_matrix[para][de] = aresta;

} catch (ArrayIndexOutOfBoundsException índice) {

[Link]("Os vértices não existem");


}
}

público int getEdge(int para, int de) {

tentar

retornar adjacency_matrix[para][de];

} catch (ArrayIndexOutOfBoundsException índice) {

[Link]("Os vértices não existem");

} retornar -1;
}

público estático vazio main(String args[]) {

int v, e, contagem = 1, para = 0, de = 0; Scanner sc =


novo Scanner([Link]);
Representar_Grafo_Matriz_de_Adjacências gráfico; tentar {

[Link]("Digite o número de vértices: "); v = [Link]();


[Link]("Digite
o número de arestas: "); e = [Link]();

gráfico = novo Represent_Graph_Adjacency_Matrix(v);

[Link]("Digite as arestas: <para> <de>"); while (count <= e) {

para = [Link](); de =
[Link]();

[Link](para, de, 1); contagem+


+;
}

[Link]("A matriz de adjacência para o grafo dado é: "); [Link](" "); for (int i = 1; i <=
v; i++)

[Link](i + " ");


[Link]();

Notas de algoritmos do [Link] para profissionais 32


Machine Translated by Google

para (int i = 1; i <= v; i++) {

[Link](i + " "); para (int


j = 1; j <= v; j++)
[Link](grá[Link](i, j) + " ");
[Link]();
}

} catch (Exceção E) {

[Link]("Algo deu errado");


}

[Link]();
}
}

Executando o código: Salve o arquivo e compile usando javac Represent_Graph_Adjacency_Matrix.java

Exemplo:

$ java Representar_Grafo_Matriz_de_Adjacência
Insira o número de vértices:
4
Digite o número de arestas: 6

Insira as arestas: 1
13
42
31
42
41
2
A matriz de adjacência para o gráfico dado é: 1 2 3 4 1 1 1 0 1 2 0 0 1 1

30001
40000

Seção 9.2: Introdução à Teoria dos Grafos


Teoria dos Grafos é o estudo de gráficos, que são estruturas matemáticas usadas para modelar relações de pares entre objetos.

Você sabia que quase todos os problemas do planeta Terra podem ser convertidos em problemas de Estradas e Cidades, e
resolvidos? A Teoria dos Grafos foi inventada há muitos anos, antes mesmo da invenção do computador. Leonhard Euler escreveu
um artigo sobre as Sete Pontes de Königsberg que é considerado o primeiro artigo da Teoria dos Grafos. Desde então, as
pessoas perceberam que se pudermos converter qualquer problema para este problema Cidade-Estrada, podemos resolvê-lo
facilmente pela Teoria dos Grafos.

A Teoria dos Grafos tem muitas aplicações. Uma das aplicações mais comuns é encontrar a menor distância entre uma cidade e
outra. Todos nós sabemos que para chegar ao seu PC, esta página da web teve que viajar por muitos roteadores do servidor.
A Teoria dos Grafos ajuda a descobrir os roteadores que precisam ser cruzados. Durante a guerra, qual rua precisa ser
bombardeada para desconectar a capital das outras, isso também pode ser descoberto usando a Teoria dos Grafos.

Notas de algoritmos do [Link] para profissionais 33


Machine Translated by Google

Vamos primeiro aprender algumas definições básicas sobre Teoria dos Grafos.

Gráfico:

Digamos que temos 6 cidades. Nós as marcamos como 1, 2, 3, 4, 5, 6. Agora conectamos as cidades que têm estradas entre si.

Este é um gráfico simples onde algumas cidades são mostradas com as estradas que as conectam. Na Teoria dos Grafos, chamamos cada uma dessas

cidades de Nó ou Vértice e as estradas são chamadas de Aresta. O gráfico é simplesmente uma conexão desses nós e arestas.

Um nó pode representar muitas coisas. Em alguns gráficos, nós representam cidades, alguns representam aeroportos, alguns representam um

quadrado em um tabuleiro de xadrez. A aresta representa a relação entre cada nó. Essa relação pode ser o tempo para ir de um aeroporto para outro, os

movimentos de um cavalo de um quadrado para todos os outros quadrados, etc.

Notas de algoritmos do [Link] para profissionais 34


Machine Translated by Google

Caminho do Cavalo em um Tabuleiro de Xadrez

Em palavras simples, um Nó representa qualquer objeto e uma Aresta representa a relação entre dois objetos.

Nó adjacente:

Se um nó A compartilha uma aresta com o nó B, então B é considerado adjacente a A. Em outras palavras, se dois nós estão
diretamente conectados, eles são chamados de nós adjacentes. Um nó pode ter vários nós adjacentes.

Grafo direcionado e não direcionado:

Em grafos direcionados, as arestas têm sinais de direção em um lado, o que significa que as arestas são unidirecionais. Por
outro lado, as arestas de grafos não direcionados têm sinais de direção em ambos os lados, o que significa que são bidirecionais.
Normalmente, gráficos não direcionados são representados sem sinais em ambos os lados das arestas.

Vamos supor que haja uma festa acontecendo. As pessoas na festa são representadas por nós e há uma aresta entre duas
pessoas se elas apertam as mãos. Então este gráfico é não direcionado porque qualquer pessoa A aperta a mão da pessoa B se
e somente se B também aperta a mão de A. Em contraste, se as arestas de uma pessoa A para outra pessoa B correspondem à
admiração de A por B, então este gráfico é direcionado, porque a admiração não é necessariamente recíproca. O primeiro tipo de
gráfico é chamado de gráfico não direcionado e as arestas são chamadas de arestas não direcionadas , enquanto o último tipo de
gráfico é chamado de gráfico direcionado e as arestas são chamadas de arestas direcionadas.

Gráfico ponderado e não ponderado:

Um grafo ponderado é um grafo no qual um número (o peso) é atribuído a cada aresta. Tais pesos podem representar, por exemplo,
custos, comprimentos ou capacidades, dependendo do problema em questão.

Notas de algoritmos do [Link] para profissionais 35


Machine Translated by Google

Um gráfico não ponderado é simplesmente o oposto. Assumimos que o peso de todas as arestas é o mesmo (presumivelmente 1).

Caminho:

Um caminho representa uma maneira de ir de um nó para outro. Ele consiste em uma sequência de arestas. Pode haver vários

caminhos entre dois nós.

No exemplo acima, há dois caminhos de A para D. A->B, B->C, C->D é um caminho. O custo desse caminho é 3 + 4 + 2 = 9. Novamente,
há outro caminho A->D. O custo desse caminho é 10. O caminho que custa menos é chamado de caminho mais curto.

Grau:

O grau de um vértice é o número de arestas que estão conectadas a ele. Se houver alguma aresta que se conecte ao vértice em
ambas as extremidades (um loop) é contado duas vezes.

Notas de algoritmos do [Link] para profissionais 36


Machine Translated by Google

Em grafos direcionados, os nós têm dois tipos de graus:

Grau de entrada: O número de arestas que apontam para o nó.


Grau de saída: O número de arestas que apontam do nó para outros nós.

Para gráficos não direcionados, eles são simplesmente chamados de grau.

Alguns Algoritmos Relacionados à Teoria dos Grafos

Algoritmo Bellman–Ford
Algoritmo de Dijkstra
Algoritmo Ford–Fulkerson
Algoritmo de Kruskal
Algoritmo do vizinho mais próximo
Algoritmo de Prim
Busca em profundidade
Busca em largura

Seção 9.3: Armazenando Grafos (Lista de Adjacência)


Lista de adjacências é uma coleção de listas não ordenadas usadas para representar um grafo finito. Cada lista descreve o conjunto de vizinhos
de um vértice em um grafo. Ocupa menos memória para armazenar grafos.

Vamos ver um gráfico e sua matriz de adjacência:

Agora criamos uma lista usando esses valores.

Notas de algoritmos do [Link] para profissionais 37


Machine Translated by Google

Isso é chamado de lista de adjacência. Ela mostra quais nós estão conectados a quais nós. Podemos armazenar essas informações
usando uma matriz 2D. Mas vai nos custar a mesma memória que a Matriz de Adjacência. Em vez disso, vamos usar memória
alocada dinamicamente para armazenar esta.

Muitas linguagens suportam Vector ou List , que podemos usar para armazenar listas de adjacência. Para estas, não precisamos
especificar o tamanho da List. Precisamos apenas especificar o número máximo de nós.

O pseudocódigo será:

Procedimento Adjacency-List(maxN, E): edge[maxN] // maxN denota o número máximo de nós


= Vector() para i de 1 a E // E denota o número de arestas

entrada -> x, y // Aqui x, y denota que há uma aresta entre x, y


aresta[x].push(y)
aresta[y].push(x)
fim para
Borda de retorno

Como este é um gráfico não direcionado, se há uma aresta de x para y, também há uma aresta de y para x. Se fosse um gráfico
direcionado, omitiríamos o segundo. Para gráficos ponderados, precisamos armazenar o custo também. Criaremos outro vetor ou lista
chamada cost[] para armazená-los. O pseudocódigo:

Procedimento Adjacency-List(maxN, E): edge[maxN]


= Vector() cost[maxN] = Vector()
para i de 1 a E

entrada -> x, y, w
edge[x].push(y)
cost[x].push(w) fim para
Retornar
aresta, custo

A partir daí, podemos descobrir facilmente o número total de nós conectados a qualquer nó e quais são esses nós.

Notas de algoritmos do [Link] para profissionais 38


Machine Translated by Google

Leva menos tempo do que a Matriz de Adjacência. Mas se precisássemos descobrir se há uma aresta entre u e v, seria mais fácil se mantivéssemos
uma matriz de adjacência.

Seção 9.4: Classificação topológica


Uma ordenação topológica, ou uma classificação topológica, ordena os vértices em um grafo acíclico direcionado em uma linha, ou seja, em uma
lista, de modo que todas as arestas direcionadas vão da esquerda para a direita. Tal ordenação não pode existir se o grafo contiver um ciclo direcionado,
porque não há como você continuar indo para a direita em uma linha e ainda retornar para onde começou.

Formalmente, em um gráfico G = (V, E), então uma ordenação linear de todos os seus vértices é tal que se G contém uma aresta
(u, v) ÿ E do vértice u ao vértice v então u precede v na ordenação.

É importante observar que cada DAG tem pelo menos uma classificação topológica.

Existem algoritmos conhecidos para construir uma ordenação topológica de qualquer DAG em tempo linear, um exemplo é:

1. Chame depth_first_search(G) para calcular os tempos de conclusão vf para cada vértice v 2. À medida
que cada vértice é concluído, insira-o na frente de uma lista encadeada 3. a lista
encadeada de vértices, como está agora classificada.

Uma classificação topológica pode ser realizada em (V + E) de tempo, já que o algoritmo de busca em profundidade leva (V + E) de
tempo e leva ÿ(1) (tempo constante) para inserir cada um dos vértices |V| na frente de uma lista encadeada.

Muitas aplicações usam grafos acíclicos direcionados para indicar precedências entre eventos. Usamos ordenação topológica para obter uma ordenação
para processar cada vértice antes de qualquer um de seus sucessores.

Os vértices em um gráfico podem representar tarefas a serem executadas e as arestas podem representar restrições de que uma tarefa deve ser
executada antes de outra; uma ordenação topológica é uma sequência válida para executar o conjunto de tarefas descrito em V.

Instância do problema e sua solução

Deixe um vértice v descrever uma Tarefa(horas_para_concluir: int), ou seja , Tarefa(4) descreve uma Tarefa que leva 4 horas para ser
concluída, e uma aresta e descreve um Tempo de Resfriamento(horas: int) tal que Tempo de Resfriamento(3) descreve uma duração de
tempo para esfriar após uma tarefa concluída.

Seja nosso grafo chamado dag (já que é um grafo acíclico direcionado) e contenha 5 vértices:

A <- dag.add_vertex(Tarefa(4)); B <-


dag.add_vertex(Tarefa(5)); C <-
dag.add_vertex(Tarefa(3)); D <-
dag.add_vertex(Tarefa(2)); E <-
dag.add_vertex(Tarefa(7));

onde conectamos os vértices com arestas direcionadas de modo que o grafo seja acíclico,

// A ---> C ----+
// | // | |
v // B você você

---> D --> E dag.add_edge(A,


B, Tempo de Recarga(2)); dag.add_edge(A, C,
Tempo de Recarga(2)); dag.add_edge(B, D, Tempo
de Recarga(1)); dag.add_edge(C, D, Tempo de
Recarga(1)); dag.add_edge(C, E, Tempo de
Recarga(1)); dag.add_edge(D, E, Tempo de
Recarga(3));

Notas de algoritmos do [Link] para profissionais 39


Machine Translated by Google

então há três possíveis ordenações topológicas entre A e E,

1. A -> B -> D -> E


2. Lá -> Dó -> Ré -> Mi
3. Lá -> Dó -> Mi

Seção 9.5: Detectando um ciclo em um gráfico direcionado usando Depth First


Traversal

Um ciclo em um grafo direcionado existe se houver uma aresta posterior descoberta durante um DFS. Uma aresta posterior é uma aresta de um
nó para si mesmo ou um dos ancestrais em uma árvore DFS. Para um grafo desconectado, obtemos uma floresta DFS, então você tem que
iterar por todos os vértices no grafo para encontrar árvores DFS disjuntas.

Implementação C++:

#incluir <iostream>
#incluir <lista>

usando namespace std;

#define NUM_V 4

bool helper(lista<int> *gráfico, int u, bool* visitado, bool* recStack) {

visitou[u]=true;
recStack[u]=true;
lista<int>::iterador i; para(i =
gráfico[u].begin();i!=gráfico[u].end();++i) {

if(recStack[*i]) //se o vértice v for encontrado na pilha de recursão desta travessia DFS
retornar
verdadeiro; senão if(*i==u) //se houver uma aresta do vértice para ele mesmo retornar
verdadeiro; senão
if(!visited[*i]) { if(helper(graph, *i,
visited, recStack))
retornar verdadeiro;
}

} recStack[u]=falso; retornar
falso;

}/
* / A função wrapper chama a função auxiliar em cada vértice que não foi visitado. Auxiliar
a função retorna verdadeiro se detectar uma aresta posterior no subgrafo (árvore) ou falso. */

bool é cíclico(lista<int> *gráfico, int V) {

bool visited[V]; //matriz para rastrear vértices já visitados bool recStack[V]; //matriz para
rastrear vértices na pilha de recursão da travessia.

for(int i = 0;i<V;i++) visited[i]=false,


recStack[i]=false; //inicializar todos os vértices como não visitados e não
recursivo

for(int u = 0; u < V; u++) //Verifica iterativamente se todos os vértices foram visitados { if(visited[u]==false) { if(helper(graph, u,
visited, recStack)) //verifica se a árvore
DFS do vértice
contém um ciclo
retornar verdadeiro;

Notas de algoritmos do [Link] para profissionais 40


Machine Translated by Google

} retornar falso;

} /*
Função do motorista
*/
int principal() {

lista<int>* gráfico = nova lista<int>[NUM_V];


gráfico[0].push_back(1); gráfico
[0].push_back(2);
gráfico[1].push_back(2);
gráfico[2].push_back(0);
gráfico[2].push_back(3);
gráfico[3].push_back(3); bool res =
isCyclic(gráfico, NUM_V); cout<<res<<endl;

Resultado: Conforme mostrado abaixo, há três arestas traseiras no gráfico. Uma entre o vértice 0 e 2; entre o vértice 0, 1 e 2; e o vértice 3. A
complexidade de tempo da busca é O(V+E), onde V é o número de vértices e E é o número de arestas.

Seção 9.6: Algoritmo de Thorup


O algoritmo de Thorup para o caminho mais curto de fonte única para grafos não direcionados tem complexidade de tempo O(m), menor que a
de Dijkstra.

As ideias básicas são as seguintes. (Desculpe, ainda não tentei implementar, então posso perder alguns detalhes menores. E o artigo original
é pago, então tentei reconstruí-lo a partir de outras fontes que o referenciam. Por favor, remova este comentário se puder verificar.)

Existem maneiras de encontrar a árvore de abrangência em O(m) (não descritas aqui). Você precisa "crescer" a árvore de abrangência
da aresta mais curta para a mais longa, e seria uma floresta com vários componentes conectados antes

Notas de algoritmos do [Link] para profissionais 41


Machine Translated by Google

totalmente crescido.

Selecione um inteiro b (b>=2) e considere apenas as florestas de abrangência com limite de comprimento b^k.
Mescle os componentes que são exatamente os mesmos, mas com k diferente, e chame o k mínimo de nível do
componente. Então, logicamente, faça os componentes em uma árvore. u é o pai de v iff u é o menor componente distinto
de v que contém v completamente. A raiz é o gráfico inteiro e as folhas são vértices únicos no gráfico original (com o
nível de infinito negativo). A árvore ainda tem apenas O(n) nós.
Mantenha a distância de cada componente para a fonte (como no algoritmo de Dijkstra). A distância de um
componente com mais de um vértice é a distância mínima de seus filhos não expandidos. Defina a distância do
vértice de origem para 0 e atualize os ancestrais de acordo.
Considere as distâncias na base b. Ao visitar um nó no nível k pela primeira vez, coloque seus filhos em buckets
compartilhados por todos os nós do nível k (como em bucket sort, substituindo o heap no algoritmo de Dijkstra) pelo dígito
k e maior de sua distância. Cada vez que visitar um nó, considere apenas seus primeiros b buckets, visite e remova
cada um deles, atualize a distância do nó atual e revincule o nó atual ao seu próprio pai usando a nova distância e aguarde
a próxima visita para os buckets seguintes.
Quando uma folha é visitada, a distância atual é a distância final do vértice. Expanda todas as arestas dela no gráfico
original e atualize as distâncias de acordo.
Visite o nó raiz (gráfico inteiro) repetidamente até que o destino seja alcançado.

Baseia-se no fato de que não há uma aresta com comprimento menor que l entre dois componentes conectados da floresta de
abrangência com limitação de comprimento l, então, começando na distância x, você pode focar apenas em um
componente conectado até atingir a distância x + l. Você visitará alguns vértices antes que os vértices com distância menor sejam
todos visitados, mas isso não importa porque é sabido que não haverá um caminho mais curto para cá a partir desses vértices. Outras
partes funcionam como a ordenação por balde / ordenação por radix MSD e, claro, requer a árvore de abrangência O(m).

Notas de algoritmos do [Link] para profissionais 42


Machine Translated by Google

Capítulo 10: Travessias de grafos


Seção 10.1: Função de travessia de busca em profundidade
A função recebe o argumento do índice do nó atual, a lista de adjacências (armazenada no vetor de vetores neste
exemplo) e o vetor de booleanos para rastrear qual nó foi visitado.

void dfs(int nó, vetor<vetor<int>>* gráfico, vetor<bool>* visitado) {


// verifique se o nó foi visitado antes if((*visited)[node])

retornar;

// definido como visitado para evitar visitar o mesmo nó duas vezes (*visited)
[node] = true;

// execute alguma ação aqui cout <<


node;

// percorrer para os nós adjacentes de maneira em profundidade for(int i = 0; i <


(*graph)[node].size(); ++i) dfs((*graph)[node][i], graph, visited);

Notas de algoritmos do [Link] para profissionais 43


Machine Translated by Google

Capítulo 11: Algoritmo de Dijkstras


Seção 11.1: Algoritmo do caminho mais curto de Dijkstra
Antes de prosseguir, é recomendável ter uma breve ideia sobre Matriz de Adjacência e BFS

Algoritmo de Dijkstra é conhecido como algoritmo de caminho mais curto de fonte única. É usado para encontrar os caminhos mais curtos
entre nós em um gráfico, que pode representar, por exemplo, redes rodoviárias. Foi concebido por Edsger W.
Dijkstra em 1956 e publicado três anos depois.

Podemos encontrar o caminho mais curto usando o algoritmo de busca Breadth First Search (BFS). Esse algoritmo funciona bem, mas o
problema é que ele assume que o custo de percorrer cada caminho é o mesmo, o que significa que o custo de cada aresta é o mesmo. O
algoritmo de Dijkstra nos ajuda a encontrar o caminho mais curto onde o custo de cada caminho não é o mesmo.

Primeiro, veremos como modificar o BFS para escrever o algoritmo de Dijkstra e, depois, adicionaremos uma fila de prioridades para torná-
lo um algoritmo de Dijkstra completo.

Digamos que a distância de cada nó da fonte é mantida no array d[] . Como em, d[3] representa que d[3] tempo é levado para alcançar o nó 3
da fonte. Se não soubermos a distância, armazenaremos infinito em d[3]. Além disso, deixe cost[u][v] representar o custo de uv.
Isso significa que leva cost[u][v] para ir do nó u para o nó v .

Precisamos entender o Relaxamento de Borda. Digamos que, da sua casa, que é a fonte, leva 10 minutos para ir ao local A. E leva 25
minutos para ir ao local B. Temos,

d[A] = 10
d[B] = 25

Agora digamos que leva 7 minutos para ir do ponto A ao ponto B, isso significa:

custo[A][B] = 7

Então podemos ir para o lugar B da fonte indo para o lugar A da fonte e então do lugar A, indo para o lugar B, o que levará 10 + 7 = 17
minutos, em vez de 25 minutos. Então,

d[A] + custo[A][B] < d[B]

Então atualizamos,

d[B] = d[A] + custo[A][B]

Isso é chamado de relaxamento. Iremos do nó u para o nó v e se d[u] + cost[u][v] < d[v] então atualizaremos d[v] = d[u] + cost[u][v].

No BFS, não precisamos visitar nenhum nó duas vezes. Só verificamos se um nó é visitado ou não. Se não foi visitado, colocamos o nó na
fila, marcamos como visitado e incrementamos a distância em 1. No Dijkstra, podemos empurrar um nó

Notas de algoritmos do [Link] para profissionais 44


Machine Translated by Google

na fila e em vez de atualizá-lo com o visitado, relaxamos ou atualizamos a nova aresta. Vejamos um exemplo:

Vamos supor que o Nó 1 seja a Fonte. Então,

d[1] = 0 d[2]
= d[3] = d[4] = infinito (ou um valor grande)

Definimos d[2], d[3] e d[4] para infinito porque ainda não sabemos a distância. E a distância da fonte é, claro , 0. Agora, vamos para outros nós da

fonte e, se pudermos atualizá-los, então os colocaremos na fila.

Digamos, por exemplo, que atravessaremos a aresta 1-2. Como d[1] + 2 < d[2], o que fará com que d[2] = 2. Da mesma forma, atravessaremos a aresta
1-3 , o que fará com que d[3] = 5.

Podemos ver claramente que 5 não é a menor distância que podemos cruzar para ir ao nó 3. Então, atravessar um nó apenas uma vez, como BFS, não

funciona aqui. Se formos do nó 2 ao nó 3 usando a aresta 2-3, podemos atualizar d[3] = d[2] + 1 = 3. Então podemos ver que um nó pode ser atualizado
muitas vezes. Quantas vezes você pergunta? O número máximo de vezes que um nó pode ser atualizado é o número de graus de entrada de um nó.

Vamos ver o pseudocódigo para visitar qualquer nó várias vezes. Simplesmente modificaremos o BFS:

procedimento BFSmodified(G, fonte): Q = fila()


distância[] =
infinito [Link](fonte)
distância[fonte]=0 enquanto
Q não estiver vazio u <-
[Link]() para todas as arestas
de u a v em
[Link](v) faça se distância[u] + custo[u][v] < distância[v] distância[v] =
distância[u] + custo[u][v] fim se fim para

fim enquanto
Distância de retorno

Notas de algoritmos do [Link] para profissionais 45


Machine Translated by Google

Isso pode ser usado para encontrar o caminho mais curto de todos os nós da fonte. A complexidade desse código não é tão boa.
Aqui está o porquê,

No BFS, quando vamos do nó 1 para todos os outros nós, seguimos o método primeiro a chegar, primeiro a ser atendido . Por exemplo, fomos para o nó
3 da fonte antes de processar o nó 2. Se formos para o nó 3 da fonte, atualizamos o nó 4 como 5 + 3 = 8.
Quando atualizamos novamente o nó 3 do nó 2, precisamos atualizar o nó 4 como 3 + 3 = 6 novamente! Então o nó 4 é atualizado
duas vezes.

Dijkstra propôs, em vez de ir para o método Primeiro a chegar, primeiro a ser atendido , se atualizarmos os nós mais próximos primeiro, então serão
necessárias menos atualizações. Se processássemos o nó 2 antes, então o nó 3 teria sido atualizado antes, e depois de atualizar o nó 4
adequadamente, obteríamos facilmente a distância mais curta! A ideia é escolher da fila, o nó, que está mais próximo da fonte. Então usaremos a Fila de
Prioridade aqui para que, quando estourarmos a fila, ela nos traga o nó mais próximo u da fonte. Como ela fará isso? Ela verificará o valor de d[u] com
ela.

Vamos ver o pseudocódigo:

procedimento dijkstra(G, fonte): Q =


priority_queue() distância[] =
infinito [Link](fonte)
distância[fonte] = 0
enquanto Q não estiver vazio u
<- nós em Q com distância
mínima[] remova u do Q para todas as arestas de u a v em
[Link](v) faça se
distância[u] + custo[u][v] < distância[v] distância[v] = distância[u] + custo[u][v]

[Link](v) fim
se fim
para fim
enquanto
Distância de retorno

O pseudocódigo retorna a distância de todos os outros nós da fonte. Se quisermos saber a distância de um único nó v, podemos simplesmente retornar o
valor quando v for retirado da fila.

Agora, o Algoritmo de Dijkstra funciona quando há uma borda negativa? Se houver um ciclo negativo, então o loop infinito ocorrerá, pois continuará
reduzindo o custo a cada vez. Mesmo se houver uma borda negativa, Dijkstra não funcionará, a menos que retornemos logo após o alvo ser estourado.
Mas então, não será um algoritmo de Dijkstra. Precisaremos do algoritmo Bellman–Ford para processar a borda/ciclo negativo.

Complexidade:

A complexidade do BFS é O(log(V+E)) onde V é o número de nós e E é o número de arestas. Para Dijkstra, a complexidade é similar, mas a ordenação
da Priority Queue leva O(logV). Então a complexidade total é: O(Vlog(V)+E)

Abaixo está um exemplo Java para resolver o Algoritmo do Caminho Mais Curto de Dijkstra usando Matriz de Adjacência

importar [Link].*; importar


[Link].*; importar [Link].*;

classe ShortestPath {

estático final int V=9; int


minDistance(int dist[], Boolean sptSet[]) {

Notas de algoritmos do [Link] para profissionais 46


Machine Translated by Google

int min = Inteiro.MAX_VALUE, min_index=-1;

for (int v = 0; v < V; v++) if (sptSet[v]


== false && dist[v] <= min) {

min = dist[v];
índice_min = v;
}

retornar min_index;
}

vazio printSolution(int dist[], int n) {

[Link](" Distância do vértice da fonte"); for (int i = 0; i <


V; i++)
[Link](i+" \t\t "+dist[i]);
}

vazio dijkstra(int gráfico[][], int src) {

Booleano sptSet[] = novo Booleano[V];

para (int i = 0; i < V; i++) {

dist[i] = Inteiro.MAX_VALUE; sptSet[i]


= falso;
}

dist[fonte] = 0;

para (int contagem = 0; contagem < V-1; contagem+


+) {
int u = minDistance(dist, sptSet);

sptSet[u] = verdadeiro;

para (int v = 0; v < V; v++)

se (!sptSet[v] && gráfico[u][v]!=0 && dist[u] !=


Integer.MAX_VALUE && dist[u]+gráfico[u]
[v] < dist[v])
dist[v] = dist[u] + gráfico[u][v];
}

imprimirSolução(dist, V);
}

público estático vazio principal (String[] args) { int

gráfico[][] = novo int[][]{{0, 4, 0 , 0, 0, 0, 0, 8, 0}, {4, 0, 8, 0, 0, 0, 0, 11, 0},


{0, 8, 0, 7, 0, 4, 0, 0, 2}, {0, 0, 7, 0, 9,
14, 0, 0, 0}, {0, 0, 0, 9, 0, 10, 0, 0, 0},
{0, 0, 4, 14, 10, 0, 2, 0, 0}, {0, 0, 0, 0,
0, 2, 0, 1, 6}, {8, 11, 0, 0, 0, 0, 1, 0, 7},
{0, 0, 2, 0, 0, 0, 6, 7, 0} };

CaminhoMaisCurto t = novo CaminhoMaisCurto();

Notas de algoritmos do [Link] para profissionais 47


Machine Translated by Google

[Link](gráfico, 0);
}
}

A saída esperada do programa é

Distância do vértice da fonte


0 0
4
12 12
3 19
4 21
5 11
6 9
7 8
8 14

Notas de algoritmos do [Link] para profissionais 48


Machine Translated by Google

Capítulo 12: A* Pathfinding


Seção 12.1: Introdução ao A*

A* (A estrela) é um algoritmo de busca que é usado para encontrar o caminho de um nó para outro. Então ele pode ser comparado com
Breadth First Search, ou algoritmo de Dijkstra, ou Depth First Search, ou Best First Search. O algoritmo A* é amplamente utilizado
na busca por gráficos para melhorar a eficiência e a precisão, onde o pré-processamento de gráficos não é uma opção.

A* é uma especialização de Best First Search , em que a função de avaliação f é definida de uma maneira particular.

f(n) = g(n) + h(n) é o custo mínimo desde o nó inicial até os objetivos condicionados a passar pelo nó n.

g(n) é o custo mínimo do nó inicial até n.

h(n) é o custo mínimo de n para o objetivo mais próximo de n

A* é um algoritmo de busca informado e sempre garante encontrar o menor caminho (caminho com custo mínimo) em
o menor tempo possível (se usar heurística admissível). Então é completo e ótimo. A animação a seguir
demonstra pesquisa A*-

Seção 12.2: A* Encontrando caminhos em um labirinto sem obstáculos


Digamos que temos a seguinte grade 4 por 4:

Notas de algoritmos do [Link] para profissionais 49


Machine Translated by Google

Vamos supor que este seja um labirinto. Não há paredes/obstáculos, no entanto. Temos apenas um ponto de partida (o
quadrado verde) e um ponto final (o quadrado vermelho). Vamos também supor que, para ir do verde ao vermelho, não podemos
nos mover na diagonal. Então, começando pelo quadrado verde, vamos ver para quais quadrados podemos nos mover e destacá-los em
azul:

Notas de algoritmos do [Link] para profissionais 50


Machine Translated by Google

Para escolher para qual quadrado mover em seguida, precisamos levar em conta duas heurísticas:

1. O valor "g" - Esta é a distância que este nó está do quadrado verde.


2. O valor "h" - Esta é a distância que este nó está do quadrado vermelho.
3. O valor "f" - Esta é a soma do valor "g" e do valor "h". Este é o número final que nos diz qual
nó para onde mover.

Para calcular essas heurísticas, esta é a fórmula que usaremos: distância = abs(de.x - para.x) + abs(de.y - para.y)

Isto é conhecido como "Distância de Manhattan" fórmula.

Vamos calcular o valor "g" para o quadrado azul imediatamente à esquerda do quadrado verde: abs(3 - 2) + abs(2 - 2) = 1

Ótimo! Temos o valor: 1. Agora, vamos tentar calcular o valor "h": abs(2 - 0) + abs(2 - 0) = 4

Perfeito. Agora, vamos pegar o valor "f": 1 + 4 = 5

Então, o valor final para este nó é "5".

Vamos fazer o mesmo para todos os outros quadrados azuis. O número grande no centro de cada quadrado é o valor "f", enquanto o número
no canto superior esquerdo é o valor "g", e o número no canto superior direito é o valor "h":

Notas de algoritmos do [Link] para profissionais 51


Machine Translated by Google

Calculamos os valores g, h e f para todos os nós azuis. Agora, qual escolhemos?

Aquele que tiver o menor valor f.

Entretanto, neste caso, temos 2 nós com o mesmo valor f, 5. Como escolher entre eles?

Simplesmente, escolha um aleatoriamente ou tenha uma prioridade definida. Eu geralmente prefiro ter uma prioridade assim: "Direita > Cima >
Baixo > Esquerda"

Um dos nós com o valor f de 5 nos leva na direção "Down", e o outro nos leva "Left". Como Down tem prioridade maior que Left, escolhemos
o quadrado que nos leva "Down".

Agora, marco os nós para os quais calculamos a heurística, mas não movemos, como laranja, e o nó que escolhemos como ciano:

Notas de algoritmos do [Link] para profissionais 52


Machine Translated by Google

Tudo bem, agora vamos calcular a mesma heurística para os nós ao redor do nó ciano:

Novamente, escolhemos o nó que desce do nó ciano, pois todas as opções têm o mesmo valor f:

Notas de algoritmos do [Link] para profissionais 53


Machine Translated by Google

Vamos calcular a heurística para o único vizinho que o nó ciano tem:

Tudo bem, já que seguiremos o mesmo padrão que temos seguido:

Notas de algoritmos do [Link] para profissionais 54


Machine Translated by Google

Mais uma vez, vamos calcular a heurística para o vizinho do nó:

Vamos lá:

Notas de algoritmos do [Link] para profissionais 55


Machine Translated by Google

Por fim, podemos ver que temos um quadrado vencedor ao nosso lado, então nos movemos para lá e pronto.

Seção 12.3: Resolução do problema de 8 quebra-cabeças usando o algoritmo A*

Definição do problema:

Um quebra-cabeça 8 é um jogo simples que consiste em uma grade 3 x 3 (contendo 9 quadrados). Um dos quadrados está vazio. O objetivo
é mover os quadrados em diferentes posições e ter os números exibidos no "estado objetivo".

Dado um estado inicial de jogo de 8 quebra-cabeças e um estado final a ser alcançado, encontre o caminho mais econômico para atingir o
estado final a partir do estado inicial.

Estado inicial:

_ 13
425
786

Notas de algoritmos do [Link] para profissionais 56


Machine Translated by Google

Estado final:

123
456
78 _

Heurística a ser assumida:

Vamos considerar a distância de Manhattan entre o estado atual e o estado final como a heurística para este problema
declaração.

h(n) = | x - p | + | y - q |
onde x e y são coordenadas da célula no estado atual
p e q são coordenadas da célula no estado final

Função de custo total:

Portanto, a função de custo total f(n) é dada por,

f(n) = g(n) + h(n), onde g(n) é o custo necessário para atingir o estado atual a partir do estado inicial dado
estado

Solução para o problema de exemplo:

Primeiro, encontramos o valor heurístico necessário para atingir o estado final a partir do estado inicial. A função de custo, g(n) = 0, como
estão no estado inicial

h(n) = 8

O valor acima é obtido, pois 1 no estado atual está a 1 distância horizontal de distância do que 1 no estado final. O mesmo
vale para 2, 5, 6. _ é 2 distância horizontal e 2 distância vertical. Então o valor total para h(n) é 1 + 1 + 1 + 1 +

2 + 2 = 8. A função de custo total f(n) é igual a 8 + 0 = 8.

Agora, os estados possíveis que podem ser alcançados a partir do estado inicial são encontrados e acontece que podemos mover para a _

direita ou para baixo.

Então os estados obtidos após realizar esses movimentos são:

1 3_4 413
25 _ 25
786 786
(1) (2)

Novamente a função de custo total é calculada para esses estados usando o método descrito acima e acaba sendo
6 e 7 respectivamente. Escolhemos o estado com custo mínimo que é o estado (1). Os próximos movimentos possíveis podem ser Esquerda,
Direita ou Baixo. Não vamos mover para a Esquerda como estávamos anteriormente naquele estado. Então, podemos mover para a Direita ou Baixo.

Novamente encontramos os estados obtidos de (1).

13 _ 123

Notas de algoritmos do [Link] para profissionais 57


Machine Translated by Google

425 4 _ 5
7 8 6 (3) 786
(4)

(3) leva a uma função de custo igual a 6 e (4) leva a 4. Também consideraremos (2) obtido antes que tem custo
função igual a 7. Escolher o mínimo deles leva a (4). Os próximos movimentos possíveis podem ser Esquerda ou Direita ou Baixo.
Obtemos estados:

123 123 123


_ 45 45 _ 485
7 8 6 (5) 7 8 6 (6) 76_

(7)

Obtemos custos iguais a 5, 2 e 4 para (5), (6) e (7) respectivamente. Além disso, temos estados anteriores (3) e (2) com 6 e 7
respectivamente. Escolhemos o estado de custo mínimo que é (6). Os próximos movimentos possíveis são para cima, para baixo e claramente para baixo
nos levará ao estado final que leva ao valor da função heurística igual a 0.

Notas de algoritmos do [Link] para profissionais 58


Machine Translated by Google

Capítulo 13: Algoritmo de Pathfinding A*


Este tópico se concentrará no algoritmo A* Pathfinding, como ele é usado e por que ele funciona.

Nota para futuros contribuidores: adicionei um exemplo para A* Pathfinding sem obstáculos, em uma grade 4x4. Um exemplo
com obstáculos ainda é necessário.

Seção 13.1: Exemplo simples de A* Pathfinding: Um labirinto sem obstáculos

Digamos que temos a seguinte grade 4 por 4:

Vamos supor que este seja um labirinto. Não há paredes/obstáculos, no entanto. Temos apenas um ponto de partida (o
quadrado verde) e um ponto final (o quadrado vermelho). Vamos também supor que, para ir do verde ao vermelho, não podemos

Notas de algoritmos do [Link] para profissionais 59


Machine Translated by Google

mover diagonalmente. Então, começando pelo quadrado verde, vamos ver para quais quadrados podemos nos mover, e destacá-los em azul:

Para escolher para qual quadrado mover em seguida, precisamos levar em conta duas heurísticas:

1. O valor "g" - Esta é a distância que este nó está do quadrado verde.


2. O valor "h" - Esta é a distância que este nó está do quadrado vermelho.
3. O valor "f" - Esta é a soma do valor "g" e do valor "h". Este é o número final que nos diz qual
nó para onde mover.

Para calcular essas heurísticas, esta é a fórmula que usaremos: distância = abs(de.x - para.x) + abs(de.y - para.y)

Isto é conhecido como "Distância de Manhattan" fórmula.

Vamos calcular o valor "g" para o quadrado azul imediatamente à esquerda do quadrado verde: abs(3 - 2) + abs(2 - 2) = 1

Ótimo! Temos o valor: 1. Agora, vamos tentar calcular o valor "h": abs(2 - 0) + abs(2 - 0) = 4

Perfeito. Agora, vamos pegar o valor "f": 1 + 4 = 5

Então, o valor final para este nó é "5".

Vamos fazer o mesmo para todos os outros quadrados azuis. O número grande no centro de cada quadrado é o valor "f", enquanto o número no
canto superior esquerdo é o valor "g", e o número no canto superior direito é o valor "h":

Notas de algoritmos do [Link] para profissionais 60


Machine Translated by Google

Calculamos os valores g, h e f para todos os nós azuis. Agora, qual escolhemos?

Aquele que tiver o menor valor f.

Entretanto, neste caso, temos 2 nós com o mesmo valor f, 5. Como escolher entre eles?

Simplesmente, escolha um aleatoriamente ou tenha uma prioridade definida. Eu geralmente prefiro ter uma prioridade assim: "Direita > Cima >
Baixo > Esquerda"

Um dos nós com o valor f de 5 nos leva na direção "Down", e o outro nos leva "Left". Como Down tem prioridade maior que Left, escolhemos
o quadrado que nos leva "Down".

Agora, marco os nós para os quais calculamos a heurística, mas não movemos, como laranja, e o nó que escolhemos como ciano:

Notas de algoritmos do [Link] para profissionais 61


Machine Translated by Google

Tudo bem, agora vamos calcular a mesma heurística para os nós ao redor do nó ciano:

Novamente, escolhemos o nó que desce do nó ciano, pois todas as opções têm o mesmo valor f:

Notas de algoritmos do [Link] para profissionais 62


Machine Translated by Google

Vamos calcular a heurística para o único vizinho que o nó ciano tem:

Tudo bem, já que seguiremos o mesmo padrão que temos seguido:

Notas de algoritmos do [Link] para profissionais 63


Machine Translated by Google

Mais uma vez, vamos calcular a heurística para o vizinho do nó:

Vamos lá:

Notas de algoritmos do [Link] para profissionais 64


Machine Translated by Google

Por fim, podemos ver que temos um quadrado vencedor ao nosso lado, então nos movemos para lá e pronto.

Notas de algoritmos do [Link] para profissionais 65


Machine Translated by Google

Capítulo 14: Programação dinâmica


A programação dinâmica é um conceito amplamente usado e frequentemente usado para otimização. Ela se refere à
simplificação de um problema complicado, dividindo-o em subproblemas mais simples de forma recursiva, geralmente uma
abordagem de baixo para cima. Existem dois atributos principais que um problema deve ter para que a programação
dinâmica seja aplicável "Subestrutura ótima" e "Subproblemas sobrepostos". Para atingir sua otimização, a programação
dinâmica usa um conceito chamado memorização

Seção 14.1: Editar Distância

A declaração do problema é como se recebêssemos duas strings str1 e str2, então quantas operações mínimas podem ser
executadas na str1 para que ela seja convertida em str2?

Implementação em Java

classe pública EditarDistância {

público estático vazio main(String[] args) {


// TODO Stub de método gerado automaticamente
String str1 = "marcha";
String str2 = "carrinho";

EditarDistância ed = new EditarDistância();


[Link]([Link](str1, str2));
}

público int getMinConversions(String str1, String str2){


int dp[][] = novo int[[Link]()+1][[Link]()+1]; para(int
i=0;i<=[Link]();i++){
para(int j=0;j<=[Link]();j++){ se(i==0) dp[i]
[j] = j;
senão se(j==0)
dp[i][j] = i; senão
se([Link](i-1)
== [Link](j-1)) dp[i][j] = dp[i-1][j-1]; senão{ dp[i][j] = 1 +
[Link](dp[i-1][j], [Link](dp[i]
[j-1],
dp[i-1][j-1]));
}
}

} retornar dp[[Link]()][[Link]()];
}

Saída

Seção 14.2: Algoritmo de agendamento de tarefas ponderado


O Algoritmo de Agendamento de Tarefas Ponderado também pode ser denotado como Algoritmo de Seleção de Atividades Ponderado.

O problema é que, dados certos trabalhos com seus horários de início e término, e um lucro que você obtém ao terminar o trabalho,
qual é o lucro máximo que você pode obter, já que não é possível executar dois trabalhos em paralelo?

Notas de algoritmos do [Link] para profissionais 66


Machine Translated by Google

Este parece Activity Selection usando Greedy Algorithm, mas há uma reviravolta adicional. Ou seja, em vez de
maximizando o número de trabalhos concluídos, focamos em obter o lucro máximo. O número de trabalhos realizados
não importa aqui.

Vejamos um exemplo:

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | UM
| B | C | E | E | F |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (2,5) | (6,7) | (7,9) | (1,3) | (5,8) | (4,6) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 6 | 4 | 2 | 5 | 11 | 5 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Os trabalhos são denotados com um nome, seu horário de início e término e lucro. Após algumas iterações, podemos descobrir se
realizamos o Trabalho-A e o Trabalho-E, podemos obter o lucro máximo de 17. Agora, como descobrir isso usando um algoritmo?

A primeira coisa que fazemos é classificar os trabalhos pelo tempo de conclusão em ordem não decrescente. Por que fazemos isso? É porque
se selecionarmos um trabalho que leve menos tempo para terminar, então deixaremos mais tempo para escolher outros trabalhos. Nós
ter:

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Teremos um array temporário adicional Acc_Prof de tamanho n (aqui, n denota o número total de trabalhos). Isso irá
contém o lucro máximo acumulado da execução dos trabalhos. Não entendeu? Espere e observe. Vamos inicializar o
valores do array com o lucro de cada trabalho. Isso significa que Acc_Prof[i] irá inicialmente conter o lucro da execução do i-ésimo
trabalho.

+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Agora vamos denotar a posição 2 com i, e a posição 1 será denotada com j. Nossa estratégia será iterar j de 1 para
i-1 e após cada iteração, incrementaremos i em 1, até que i se torne n+1.

eu
eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Notas de algoritmos do [Link] para profissionais 67


Machine Translated by Google

Verificamos se Job[i] e Job[j] se sobrepõem, ou seja, se o tempo de término de Job[j] for maior que o tempo de início de Job[i] , então estes
dois trabalhos não podem ser feitos juntos. No entanto, se eles não se sobrepõem, verificaremos se Acc_Prof[j] + Profit[i] > Acc_Prof[i]. Se
este é o caso, atualizaremos Acc_Prof[i] = Acc_Prof[j] + Profit[i]. Ou seja:

se Job[j].tempo_de_término <= Job[i].tempo_de_início


se Acc_Prof[j] + Lucro[i] > Acc_Prof[i]
Acc_Prof[i] = Acc_Prof[j] + Lucro[i]
fim se
fim se

Aqui Acc_Prof[j] + Profit[i] representa o lucro acumulado de fazer esses dois trabalhos juntos. Vamos verificar isso
nosso exemplo:

Aqui Job[j] se sobrepõe a Job[i]. Então estes dois não podem ser feitos juntos. Como nosso j é igual a i-1, incrementamos o
valor de i para i+1 que é 3. E fazemos j = 1.

eu
eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Agora Job[j] e Job[i] não se sobrepõem. O valor total de lucro que podemos obter escolhendo esses dois jobs é: Acc_Prof[j]
+ Profit[i] = 5 + 5 = 10 que é maior que Acc_Prof[i]. Então atualizamos Acc_Prof[i] = 10. Também incrementamos j em 1.
Nós conseguimos,

eu
eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 10 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Aqui, Job[j] se sobrepõe a Job[i] e j também é igual a i-1. Então incrementamos i em 1, e fazemos j = 1. Obtemos,

eu
eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 10 | 4 | 11 | 2 |

Notas de algoritmos do [Link] para profissionais 68


Machine Translated by Google
+--------------+---------+---------+--- ------+---------+---------+------------+

Agora, Job[j] e Job[i] não se sobrepõem, obtemos o lucro acumulado 5 + 4 = 9, que é maior que Acc_Prof[i]. Nós
atualize Acc_Prof[i] = 9 e incremente j em 1.

eu
eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 10 | 9 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Novamente Job[j] e Job[i] não se sobrepõem. O lucro acumulado é: 6 + 4 = 10, que é maior que Acc_Prof[i]. Nós
novamente atualizamos Acc_Prof[i] = 10. Incrementamos j em 1. Obtemos:

eu
eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 10 | 10 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Se continuarmos esse processo, depois de iterar por toda a tabela usando i, nossa tabela finalmente ficará assim:

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | E | UM
| F | B | E | C |
+--------------+---------+---------+--- ------+---------+---------+------------+
|(Hora de início, hora de término)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Lucro | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acess_Prof | 5 | 6 | 10 | 14 | 17 | 8 |
+--------------+---------+---------+--- ------+---------+---------+------------+

* Algumas etapas foram ignoradas para tornar o documento mais curto.

Se iterarmos pelo array Acc_Prof, podemos descobrir que o lucro máximo é 17! O pseudocódigo:

Procedimento WeightedJobScheduling(Trabalho)
classificar o trabalho de acordo com o tempo de conclusão em ordem não decrescente
para i -> 2 para n
para j -> 1 para i-1
se Job[j].tempo_de_término <= Job[i].tempo_de_início
se Acc_Prof[j] + Lucro[i] > Acc_Prof[i]
Acc_Prof[i] = Acc_Prof[j] + Lucro[i]

Notas de algoritmos do [Link] para profissionais 69


Machine Translated by Google
fim se
fimse
fimpara
fimpara

maxProfit = 0 para i
-> 1 para n se maxProfit
< Acc_Prof[i] maxProfit = Acc_Prof[i]
retornar maxProfit

A complexidade de popularização do array Acc_Prof é O(n2). A travessia do array leva O(n). Então a complexidade total desse algoritmo é O(n2).

Agora, se quisermos descobrir quais trabalhos foram realizados para obter o lucro máximo, precisamos percorrer o array na ordem inversa e se o
Acc_Prof corresponder ao maxProfit, colocaremos o nome do trabalho em uma pilha e subtrairemos o Profit desse trabalho do maxProfit.
Faremos isso até que nosso maxProfit > 0 ou atinjamos o ponto inicial do array Acc_Prof . O pseudocódigo ficará assim:

Procedimento FindingPerformedJobs(Job, Acc_Prof, maxProfit):


S = stack() para
i -> n até 0 e maxProfit > 0
se maxProfit for igual a Acc_Prof[i]
[Link](Job[i].name maxProfit
= maxProfit - Job[i].profit fimse fimpara

A complexidade deste procedimento é: O(n).

Uma coisa a lembrar: se houver várias programações de trabalho que podem nos dar o lucro máximo, só poderemos encontrar uma programação
de trabalho por meio deste procedimento.

Seção 14.3: Maior Subsequência Comum


Se nos forem dadas duas sequências, temos que encontrar a maior subsequência comum presente em ambas.

Exemplo

LCS para sequências de entrada “ABCDGH” e “AEDFHR” é “ADH” de comprimento 3.

LCS para sequências de entrada “AGGTAB” e “GXTXAYB” é “GTAB” de comprimento 4.

Implementação em Java

classe pública LCS {

public static void main(String[] args) { // TODO Stub de método


gerado automaticamente String str1 = "AGGTAB";
String str2 = "GXTXAYB"; LCS obj
= new LCS();
[Link]([Link](str1,
str2, [Link](), [Link]())); [Link](obj.lcs2(str1, str2));

// Função recursiva public int


lcs(String str1, String str2, int m, int n){

Notas de algoritmos do [Link] para profissionais 70


Machine Translated by Google

se(m==0 || n==0)
retornar 0;
se([Link](m-1) == [Link](n-1))
retornar 1 + lcs(str1, str2, m-1, n-1);
outro
retornar [Link](lcs(str1, str2, m-1, n), lcs(str1, str2, m, n-1));
}

// Função iterativa
público int lcs2(String str1, String str2){
int lcs[][] = novo int[[Link]()+1][[Link]()+1];

para(int i=0;i<=[Link]();i++){
para(int j=0;j<=[Link]();j++){
se(i==0 || j== 0){
lcs[i][j] = 0;
}
senão se([Link](i-1) == [Link](j-1)){
lcs[i][j] = 1 + lcs[i-1][j-1];
}outro{
lcs[i][j] = [Link](lcs[i-1][j], lcs[i][j-1]);
}
}
}

retornar lcs[[Link]()][[Link]()];
}

Saída

Seção 14.4: Número de Fibonacci

Abordagem de baixo para cima para imprimir o enésimo número de Fibonacci usando Programação Dinâmica.

Árvore recursiva

mentira(5)
\
/ fib(4) \ fib(3) / \

/ fib(3) / \ fib(2) / fib(2) / \ mentira(1)


\
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
mentira(1) mentira(0)

Subproblemas sobrepostos

Aqui fib(0),fib(1) e fib(3) são os subproblemas [Link](0) está sendo repetido 3 vezes, fib(1) está sendo
repetido 5 vezes e fib(3) está sendo repetido 2 vezes.

Implementação

público int fib(int n){


int f[] = novo int[n+1];

Notas de algoritmos do [Link] para profissionais 71


Machine Translated by Google

f[0]=0;f[1]=1; para(int
i=2;i<=n;i++){ f[i]=f[i-1]+f[i-2];

} retornar f[n];
}

Complexidade de tempo

Sobre)

Seção 14.5: Substring comum mais longa


Dadas 2 strings str1 e str2, temos que encontrar o comprimento da maior substring comum entre elas.

Exemplos

Entrada: X = "abcdxyz", y = "xyzabcd" Saída: 4

A maior substring comum é "abcd" e tem comprimento 4.

Entrada: X = "zxabcdezy", y = "yzabcdezx" Saída: 6

A maior substring comum é "abcdez" e tem comprimento 6.

Implementação em Java

público int obterLongestCommonSubstring(String str1,String str2){


int arr[][] = novo int[[Link]()+1][[Link]()+1]; int max = Inteiro.VALOR_MIN; para(int
i=1;i<=[Link]();i++){

para(int j=1;j<=[Link]();j++){ se([Link](j-1) ==


[Link](i-1)){ arr[i][j] = arr[i-1][j-1]+1; se(arr[i][j]>máx.) máximo =
arr[i][j];

}
senão arr[i][j] = 0;
}

} retornar máximo;
}

Complexidade de tempo

O(m*n)

Notas de algoritmos do [Link] para profissionais 72


Machine Translated by Google

Capítulo 15: Aplicações da Dinâmica


Programação
A ideia básica por trás da programação dinâmica é dividir um problema complexo em vários problemas pequenos e simples que são repetidos. Se você

puder identificar um subproblema simples que é calculado repetidamente, as chances são de que haja uma abordagem de programação dinâmica para o

problema.

Como este tópico é intitulado Aplicações da Programação Dinâmica, ele se concentrará mais nas aplicações do que no processo de criação de algoritmos de

programação dinâmica.

Seção 15.1: Números de Fibonacci

Números de Fibonacci são um assunto primordial para programação dinâmica, pois a abordagem recursiva tradicional faz muitos cálculos repetidos. Nestes
exemplos, usarei o caso base de f(0) = f(1) = 1.

Aqui está um exemplo de árvore recursiva para fibonacci(4), observe os cálculos repetidos:

Programação não dinâmica O(2^n) Complexidade de tempo de execução, O(n) Complexidade de pilha

def fibonacci(n): se n < 2:

retornar 1
retornar fibonacci(n-1) + fibonacci(n-2)

Esta é a maneira mais intuitiva de escrever o problema. No máximo, o espaço da pilha será O(n) conforme você desce o primeiro ramo recursivo fazendo

chamadas para fibonacci(n-1) até atingir o caso base n < 2.

A prova da complexidade de tempo de execução O(2^n) que pode ser vista aqui: Complexidade computacional da sequência de Fibonacci. O ponto principal a

ser observado é que o tempo de execução é exponencial, o que significa que o tempo de execução para isso dobrará para cada termo subsequente,

fibonacci(15) levará o dobro do tempo que fibonacci(14).

Complexidade de tempo de execução O(n) memorizada , complexidade de espaço O(n) , complexidade de pilha O(n)

memorando
= [] [Link](1) # f(1) =
1 [Link](1) # f(2) = 1

def fibonacci(n): if
len(memo) > n: return
memo[n]

Notas de algoritmos do [Link] para profissionais 73


Machine Translated by Google

resultado = fibonacci(n-1) + fibonacci(n-2) [Link](result)


# f(n) = f(n-1) + f(n-2) retornar resultado

Com a abordagem memoizada, introduzimos um array que pode ser pensado como todas as chamadas de função anteriores. O memo[n]
de localização é o resultado da chamada de função fibonacci(n). Isso nos permite negociar a complexidade de espaço de O(n) para um tempo
de execução O(n), pois não precisamos mais calcular chamadas de função duplicadas.

Programação dinâmica iterativa O(n) Complexidade de tempo de execução, O(n) Complexidade de espaço, Sem pilha recursiva

def fibonacci(n):
memorando = [1,1] # f(0) = 1, f(1) = 1

para i no intervalo(2, n+1):


[Link](memo[i-1] + memo[i-2])

memorando de retorno [n]

Se dividirmos o problema em seus elementos principais, você notará que, para calcular fibonacci(n), precisamos de fibonacci(n-1) e
fibonacci(n-2). Também podemos notar que nosso caso base aparecerá no final disso
árvore recursiva como visto acima.

Com essas informações, agora faz sentido calcular a solução de trás para frente, começando nos casos base e trabalhando para cima.
Agora, para calcular fibonacci(n), primeiro calculamos todos os números de fibonacci até e através de n.

O principal benefício aqui é que agora eliminamos a pilha recursiva, mantendo o tempo de execução O(n) .
Infelizmente, ainda temos uma complexidade de espaço O(n), mas isso também pode ser alterado.

Programação dinâmica iterativa avançada O(n) Complexidade de tempo de execução, O(1) Complexidade de espaço, Sem pilha recursiva

def fibonacci(n):
memorando = [1,1] # f(1) = 1, f(2) = 1

para i no intervalo (2, n):


memo[i%2] = memo[0] + memo[1]

memorando de retorno [n%2]

Conforme observado acima, a abordagem de programação dinâmica iterativa começa nos casos base e trabalha até o resultado final. A
observação chave a ser feita para chegar à complexidade espacial para O(1) (constante) é a mesma observação que fizemos
para a pilha recursiva - precisamos apenas de fibonacci(n-1) e fibonacci(n-2) para construir fibonacci(n). Isso significa que
precisamos apenas salvar os resultados para fibonacci(n-1) e fibonacci(n-2) em qualquer ponto da nossa iteração.

Para armazenar esses 2 últimos resultados, uso uma matriz de tamanho 2 e simplesmente inverto o índice que estou atribuindo usando
i % 2, que alternará assim: 0, 1, 0, 1, 0, 1, ..., eu % 2.

Eu adiciono ambos os índices do array juntos porque sabemos que a adição é comutativa (5 + 6 = 11 e 6 + 5 == 11). O resultado é então
atribuído ao mais antigo dos dois pontos (denotado por i % 2). O resultado final é então armazenado na posição n%2

Notas

É importante observar que às vezes pode ser melhor criar uma solução iterativa memorizada para

Notas de algoritmos do [Link] para profissionais 74


Machine Translated by Google

funções que realizam grandes cálculos repetidamente, pois você criará um cache da resposta para a pergunta
chamadas de função e chamadas subsequentes podem ser O(1) se já tiverem sido computadas.

Notas de algoritmos do [Link] para profissionais 75


Machine Translated by Google

Capítulo 16: Algoritmo de Kruskal


Seção 16.1: Implementação ótima baseada em conjuntos disjuntos
Podemos fazer duas coisas para melhorar os subalgoritmos de conjuntos disjuntos simples e subótimos:

1. Heurística de compressão de caminho: findSet nunca precisa manipular uma árvore com altura maior que 2. Se ele acabar
iterando tal árvore, ele pode vincular os nós inferiores diretamente à raiz, otimizando travessias futuras;

subalgo findSet (v: um nó): se [Link] ! =


v [Link] = findSet
([Link]) retornar [Link]

2. Heurística de mesclagem baseada em altura: para cada nó, armazene a altura de sua subárvore. Ao mesclar, faça o
árvore mais alta é a mãe da menor, não aumentando assim a altura de ninguém.

subalgo unionSet(u, v: nós): vRoot = findSet(v)


uRoot = findSet(u)

se vRoot == uRoot:
retornar

se [Link] < [Link]:


[Link] = uRoot senão se
[Link] > [Link]: [Link] = vRoot senão:

[Link] = vRoot [Link]


= [Link] + 1

Isso leva a um tempo O(alpha(n)) para cada operação, onde alfa é o inverso da função de Ackermann de crescimento rápido,
portanto, é de crescimento muito lento e pode ser considerado O(1) para fins práticos.

Isso torna todo o algoritmo de Kruskal O(m log m + m) = O(m log m), por causa da classificação inicial.

Observação

A compressão de caminho pode reduzir a altura da árvore, portanto, comparar alturas das árvores durante a operação de união pode
não ser uma tarefa trivial. Portanto, para evitar a complexidade de armazenar e calcular a altura das árvores, o pai resultante
pode ser escolhido aleatoriamente:

subalgo unionSet(u, v: nós): vRoot = findSet(v)


uRoot = findSet(u)

se vRoot == uRoot:
retornar
se aleatório() % 2 == 0:
[Link] = uRoot senão:

[Link] = vRoot

Na prática, este algoritmo aleatório juntamente com a compressão de caminho para a operação findSet resultará em

Notas de algoritmos do [Link] para profissionais 76


Machine Translated by Google

desempenho comparável, mas muito mais simples de implementar.

Seção 16.2: Implementação simples e mais detalhada


Para lidar eficientemente com a detecção de ciclos, consideramos cada nó como parte de uma árvore. Ao adicionar uma aresta,
verificamos se seus dois nós componentes são parte de árvores distintas. Inicialmente, cada nó compõe uma árvore de um nó.

algoritmo kruskalMST(G: um grafo) ordena as arestas


de Gs pelo seu valor MST = uma floresta de
árvores, inicialmente cada árvore é um nó no grafo para cada aresta e em G: se a raiz da árvore à qual [Link]
pertence não for a mesma que a
raiz da árvore à qual [Link] pertence: conecte uma das raízes à outra, fundindo assim duas árvores

retornar MST, que agora é uma floresta de árvore única

Seção 16.3: Implementação simples baseada em conjuntos disjuntos


A metodologia florestal acima é, na verdade, uma estrutura de dados de conjuntos disjuntos, que envolve três operações principais:

subalgo makeSet(v: a node): <- cria uma


nova árvore com raiz em v [Link] = v

subalgo findSet (v: um nó): se [Link] == v:


retornar v

retornar findSet([Link])

subalgo unionSet(v, u: nós): vRoot = findSet(v)


uRoot = findSet(u)

[Link] = vRoot

algoritmo kruskalMST(G: um grafo): ordena as arestas


de Gs pelo seu valor para cada nó n em G:

makeSet(n)
para cada aresta e em G: se
findSet([Link]) != findSet([Link]): unionSet([Link], [Link])

Essa implementação ingênua leva a um tempo O(n log n) para gerenciar a estrutura de dados do conjunto disjunto, levando a um tempo O(m*n log n) para todo o

algoritmo de Kruskal.

Seção 16.4: Implementação simples e de alto nível


Classifique as arestas por valor e adicione cada uma ao MST em ordem classificada, caso isso não crie um ciclo.

algoritmo kruskalMST(G: um grafo) ordena as arestas


Gs pelo seu valor
MST = um grafo vazio para cada
aresta e em G: se adicionar e a
MST não criar um ciclo:
adicionar e ao MST

Notas de algoritmos do [Link] para profissionais 77


Machine Translated by Google

retornar MST

Notas de algoritmos do [Link] para profissionais 78


Machine Translated by Google

Capítulo 17: Algoritmos gananciosos


Seção 17.1: Codificação Humana
Código Huffman é um tipo particular de código de prefixo ótimo que é comumente usado para compressão de dados sem perdas. Ele
comprime dados de forma muito eficaz, economizando de 20% a 90% de memória, dependendo das características dos dados que
estão sendo comprimidos. Consideramos os dados como uma sequência de caracteres. O algoritmo guloso de Huffman usa uma tabela
que fornece a frequência com que cada caractere ocorre (ou seja, sua frequência) para construir uma maneira ótima de
representar cada caractere como uma sequência binária. O código de Huffman foi proposto por David A. Huffman em 1951.

Suponha que temos um arquivo de dados de 100.000 caracteres que desejamos armazenar de forma compacta. Assumimos que há
apenas 6 caracteres diferentes naquele arquivo. A frequência dos caracteres é dada por:

+-------------+-----+-----+-----+-----+ -----+-----+
| Personagem | um | b | c | d | e | f |
+-------------+-----+-----+-----+-----+ -----+-----+
|Frequência (em milhares)| 45 | 13 | 12 | 16 | 9 | 5 |
+-------------+-----+-----+-----+-----+ -----+-----+

Temos muitas opções de como representar tal arquivo de informações. Aqui, consideramos o problema de projetar um Código de Caracteres
Binários em que cada caractere é representado por uma string binária única, que chamamos de palavra-código.

A árvore construída nos fornecerá:

+-------------+-----+-----+-----+-----+ -----+-----+
| Personagem | um | b | c | d | e | f |
+-------------+-----+-----+-----+-----+ -----+-----+
| Palavra-código de comprimento fixo | 000 | 001 | 010 | 011 | 100 | 101 |
+-------------+-----+-----+-----+-----+ -----+-----+
|Palavra-código de comprimento variável| 0 | 101 | 100 | 111 | 1101| 1100|
+-------------+-----+-----+-----+-----+ -----+-----+

Se usarmos um código de comprimento fixo, precisamos de três bits para representar 6 caracteres. Este método requer 300.000 bits
para codificar o arquivo inteiro. Agora a questão é: podemos fazer melhor?

Notas de algoritmos do [Link] para profissionais 79


Machine Translated by Google

Um código de comprimento variável pode ter um desempenho consideravelmente melhor do que um código de comprimento fixo, ao dar aos
caracteres frequentes palavras-código curtas e aos caracteres pouco frequentes palavras-código longas. Este código requer: (45 X 1 + 13 X 3 + 12 X 3
+ 16 X 3 + 9 X 4 + 5 X 4) X 1000 = 224000 bits para representar o arquivo, o que economiza aproximadamente 25% de memória.

Uma coisa a lembrar, consideramos aqui apenas códigos nos quais nenhuma palavra-código também é um prefixo de alguma outra
palavra-código. Esses são chamados códigos de prefixo. Para codificação de comprimento variável, codificamos o arquivo de 3 caracteres abc
como 0.101.100 = 0101100, onde "." denota a concatenação.

Códigos de prefixo são desejáveis porque simplificam a decodificação. Como nenhuma palavra-código é um prefixo de qualquer outra,
a palavra-código que inicia um arquivo codificado é inequívoca. Podemos simplesmente identificar a palavra-código inicial, traduzi-la de volta para o
caractere original e repetir o processo de decodificação no restante do arquivo codificado. Por exemplo, 001011101 analisa exclusivamente
como 0.0.101.1101, que decodifica para aabe. Em suma, todas as combinações de representações binárias são únicas. Digamos, por
exemplo, que se uma letra for denotada por 110, nenhuma outra letra será denotada por 1101 ou 1100. Isso ocorre porque você pode enfrentar
confusão sobre selecionar 110 ou continuar concatenando o próximo bit e selecionar aquele.

Técnica de compressão:

A técnica funciona criando uma árvore binária de nós. Eles podem ser armazenados em uma matriz regular, cujo tamanho depende do
número de símbolos, n. Um nó pode ser um nó folha ou um nó interno. Inicialmente, todos os nós são nós folha, que contêm o símbolo em si,
sua frequência e, opcionalmente, um link para seus nós filhos. Como convenção, o bit '0' representa o filho esquerdo e o bit '1' representa o
filho direito. A fila de prioridade é usada para armazenar os nós, o que fornece ao nó a menor frequência quando estourado. O processo é descrito
abaixo:

1. Crie um nó folha para cada símbolo e adicione-o à fila de prioridades.


2. Quando houver mais de um nó na fila: 1. Remova os dois nós
de maior prioridade da fila.
2. Crie um novo nó interno com esses dois nós como filhos e com frequência igual à soma de
a frequência dos dois nós.
3. Adicione o novo nó à fila.
3. O nó restante é o nó raiz e a árvore de Huffman está completa.

Para o nosso exemplo:

Notas de algoritmos do [Link] para profissionais 80


Machine Translated by Google

O pseudocódigo se parece com:

Procedimento Huffman(C): // C é o conjunto de n caracteres e informações relacionadas


n = [Link]
Q = priority_queue() para i = 1 a
nn = node(C[i])

[Link](n)
fim
enquanto [Link]() não for igual a 1 Z = new node()

[Link] = x = [Link]
[Link] = y = [Link]
[Link]ência = [Link]ência + [Link]ência [Link](Z) fim
enquanto

Retornar Q

Embora o tempo linear forneça uma entrada ordenada, em casos gerais de entrada arbitrária, usar esse algoritmo requer pré-
ordenação. Assim, como a ordenação leva tempo O(nlogn) em casos gerais, ambos os métodos têm a mesma complexidade.

Como n aqui é o número de símbolos no alfabeto, que normalmente é um número muito pequeno (comparado ao comprimento da
mensagem a ser codificada), a complexidade de tempo não é muito importante na escolha deste algoritmo.

Técnica de descompressão:

O processo de descompressão é simplesmente uma questão de traduzir o fluxo de códigos de prefixo para valor de byte individual, geralmente
atravessando a árvore de Huffman nó por nó, à medida que cada bit é lido do fluxo de entrada. Alcançar um nó folha necessariamente
encerra a busca por aquele valor de byte específico. O valor folha representa o desejado

Notas de algoritmos do [Link] para profissionais 81


Machine Translated by Google

personagem. Normalmente a Árvore de Huffman é construída usando dados estatisticamente ajustados em cada ciclo de compressão,
portanto a reconstrução é bastante simples. Caso contrário, as informações para reconstruir a árvore devem ser enviadas separadamente.
O pseudocódigo:

Procedimento HuffmanDecompression(root, S): // root representa a raiz da Árvore de Huffman n := [Link] // S refere-se ao fluxo de bits a ser descompactado para
i := 1 a n

atual = raiz
enquanto [Link] != NULL e [Link] != NULL se S[i] for igual a '0'

atual := [Link]
senão
atual := [Link] fimse

i := i+1
fimenquanto
imprimir [Link] fimpara

Explicação gananciosa:
A codificação de Huffman analisa a ocorrência de cada caractere e a armazena como uma sequência binária de forma
otimizada. A ideia é atribuir códigos de comprimento variável aos caracteres de entrada, o comprimento dos códigos
atribuídos é baseado nas frequências dos caracteres correspondentes. Criamos uma árvore binária e operamos nela de forma
ascendente para que os dois caracteres menos frequentes estejam o mais longe possível da raiz. Dessa forma, o caractere mais
frequente obtém o menor código e o caractere menos frequente obtém o maior código.

Referências:

Introdução aos Algoritmos - Charles E. Leiserson, Clifford Stein, Ronald Rivest e Thomas H. Cormen Huffman
Codificação - Wikipédia
Matemática Discreta e suas Aplicações - Kenneth H. Rosen

Seção 17.2: Problema de seleção de atividade


O Problema

Você tem um conjunto de coisas para fazer (atividades). Cada atividade tem um horário de início e um horário de término. Você não
tem permissão para executar mais de uma atividade por vez. Sua tarefa é encontrar uma maneira de executar o número máximo de atividades.

Por exemplo, suponha que você tenha uma seleção de aulas para escolher.

Atividade No. hora de início hora


de término 1 10h20 11h00
2 10h30 11h30
3 11h00 12h00
4 10h00 11h30
5 9h00 11h00

Lembre-se, você não pode fazer duas aulas ao mesmo tempo. Isso significa que você não pode fazer a aula 1 e 2 porque elas compartilham
um horário comum das 10h30 às 11h00. No entanto, você pode fazer a aula 1 e 3 porque elas não compartilham um horário comum. Então
sua tarefa é fazer o máximo de aulas possível sem nenhuma sobreposição. Como você pode fazer isso?

Análise

Notas de algoritmos do [Link] para profissionais 82


Machine Translated by Google

Vamos pensar na solução por meio de uma abordagem gananciosa. Primeiro, escolhemos aleatoriamente alguma abordagem e verificamos se ela irá
trabalhar ou não.

classificar a atividade por hora de início, o que significa que a atividade que começa primeiro, nós a pegaremos primeiro. então pegue primeiro para

último da lista classificada e verifique se ele irá cruzar com a atividade anterior ou não. Se a atividade atual não for
intersectar com a atividade realizada anteriormente, realizaremos a atividade, caso contrário, não a realizaremos.
a abordagem funcionará para alguns casos como

Atividade No. hora de início hora de término


1 11h00 - 13h30
2 11h30 - 12h00
3 13h30 14h00
4 10h00 11h00

a ordem de classificação será 4-->1-->2-->3. A atividade 4--> 1--> 3 será executada e a atividade 2 será pulada.
o máximo de 3 atividades será executado. Funciona para esse tipo de caso. mas falhará em alguns casos. Vamos aplicar
esta abordagem para o caso

Atividade No. hora de início hora de término


1 11h00 13h30
2 11h30 - 12h00
3 13h30 14h00
4 10h00 15h00

A ordem de classificação será 4-->1-->2-->3 e apenas a atividade 4 será executada, mas a resposta pode ser atividade 1-->3 ou 2-
->3 será executado. Então nossa abordagem não funcionará para o caso acima. Vamos tentar outra abordagem

Classifique a atividade por duração de tempo , o que significa executar a atividade mais curta primeiro. que pode resolver o anterior
problema . Embora o problema não esteja completamente resolvido. Ainda há alguns casos que podem falhar a solução.
aplique esta abordagem no caso abaixo.

Atividade No. hora de início hora de término


1 6h00 11h40
2 11h30 - 12h00
3 23h40 14h00

se classificarmos a atividade por duração de tempo, a ordem de classificação será 2--> . e se realizarmos a atividade nº 2 primeiro então
3 --->1, nenhuma outra atividade pode ser realizada. Mas a resposta será executar atividade 1, então executar 3, . Então podemos executar
no máximo 2 atividades. Então, isso não pode ser uma solução para esse problema. Devemos tentar uma abordagem diferente.

A solução

Classifique a atividade por hora de término, o que significa que a atividade termina primeiro. O algoritmo é fornecido
abaixo

1. Classifique as atividades por horário de término.


2. Se a atividade a ser realizada não compartilhar um tempo comum com as atividades que já foram realizadas
realizada, executar a atividade.

Vamos analisar o primeiro exemplo

Notas de algoritmos do [Link] para profissionais 83


Machine Translated by Google

Atividade No. hora de início hora de


término 10h20 11h00 1
2 10h30 11h30
3 11h00 12h00
4 10h00 11h30
5 9h00 11h00

classifique a atividade pelos horários de , Então a ordem de classificação será 1-->5-->2-->4-->3.. a resposta é 1-->3 essas duas atividades
término em que será realizada. Essa é a resposta. Aqui está o código sudo.

1. classificar: atividades

2. execute a primeira atividade da lista ordenada de atividades.


3. Definir: Current_activity := primeira atividade
4. definir: end_time := end_time da atividade atual 5. ir
para a próxima atividade se existir, se não existir encerrar 6. .
se start_time da atividade atual <= end_time: executar a atividade e ir para 4 7. senão: ir para 5.

veja aqui para ajuda de codificação [Link]

Seção 17.3: Problema de mudança


Dado um sistema monetário, é possível fornecer uma quantidade de moedas e como encontrar um conjunto mínimo de
moedas correspondente a essa quantidade?

Sistemas monetários canônicos. Para alguns sistemas monetários, como os que usamos na vida real, a solução "intuitiva" funciona
perfeitamente. Por exemplo, se as diferentes moedas e notas de euro (excluindo centavos) forem 1€, 2€, 5€, 10€, dar a moeda ou nota
mais alta até atingirmos o valor e repetir esse procedimento levará ao conjunto mínimo de moedas.

Podemos fazer isso recursivamente com OCaml:

(* assumindo que o sistema monetário é classificado em ordem decrescente *) let change_make


money_system amount = let rec loop given amount = if
amount = 0 then given else (* encontramos
o primeiro valor menor ou igual ao valor

restante *) let coin = [Link] ((>=) amount) money_system in loop (coin::given) (amount - coin) in loop [] amount

Esses sistemas são feitos para que a troca seja fácil. O problema fica mais difícil quando se trata de sistema monetário arbitrário.

Caso geral. Como dar 99€ com moedas de 10€, 7€ e 5€? Aqui, dar moedas de 10€ até que restem 9€ leva obviamente a nenhuma
solução. Pior do que isso, uma solução pode não existir. Este problema é de fato np-difícil, mas existem soluções aceitáveis misturando
ganância e memorização . A ideia é explorar todas as possibilidades e escolher aquela com
o número mínimo de moedas.

Para dar uma quantia X > 0, escolhemos uma peça P no sistema monetário e então resolvemos o subproblema correspondente a XP.
Tentamos isso para todas as peças do sistema. A solução, se existir, é então o menor caminho que levou a 0.

Aqui está uma função recursiva OCaml correspondente a este método. Ela retorna None, se nenhuma solução existir.

Notas de algoritmos do [Link] para profissionais 84


Machine Translated by Google

(* opção utilitários *) deixe optmin


xy = corresponder x,y
com
| Nenhum,a | a,Nenhum -> a
| Alguns x, Alguns y-> Alguns (min x y)

deixe optsucc = função


| Alguns x -> Alguns (x+1)
| Nenhum -> Nenhum

(* Problema de troco *) let change_make


money_system amount = let rec loop n = let onepiece acc
piece = match n - piece
with | 0 -> (*problema resolvido com
uma moeda*)

Alguns 1

| x -> se x < 0 então (*não


chegamos a 0, descartamos esta solução*)

Nenhum outro (*nós procuramos o menor caminho diferente de Nenhum com as peças restantes*) optmin (optsucc (loop x)) acc

em

(*chamamos de onepiece para todas as peças*)


List.fold_left peça única Nenhum sistema monetário
em quantidade de loop

Nota: Podemos observar que esse procedimento pode computar várias vezes o conjunto de alterações para o mesmo valor. Na
prática, usar memoização para evitar essas repetições leva a resultados mais rápidos (muito mais rápidos).

Notas de algoritmos do [Link] para profissionais 85


Machine Translated by Google

Capítulo 18: Aplicações da técnica Greedy

Seção 18.1: Cache Oine


O problema de cache surge da limitação de espaço finito. Vamos supor que nosso cache C tenha k páginas. Agora queremos
processar uma sequência de m requisições de itens que devem ter sido colocadas no cache antes de serem processadas.
Claro que se m<=k então colocamos todos os elementos no cache e funcionará, mas geralmente é m>>k.

Dizemos que uma solicitação é um cache hit, quando o item já está no cache, caso contrário, é chamado de cache miss. Nesse caso,
devemos trazer o item solicitado para o cache e despejar outro, assumindo que o cache esteja cheio. O objetivo é um cronograma de despejo
que minimize o número de despejos.

Existem inúmeras estratégias gananciosas para esse problema, vamos ver algumas:

1. Primeiro a entrar, primeiro a sair (FIFO): A página mais antiga


é removida. 2. Último a entrar, primeiro a sair (LIFO): A página
mais nova é removida. 3. Último a sair (LRU): Remove a página cujo acesso mais recente
foi o mais antigo. 4. Menos frequentemente solicitada (LFU): Remove a página que foi solicitada
com menos frequência. 5. Maior distância de avanço (LFD): Remove a página no cache que não é solicitada até o momento mais distante no

Atenção: Nos exemplos a seguir, removemos a página com o menor índice, caso mais de uma página possa ser removida.

Exemplo (FIFO)

Seja o tamanho do cache k=3 o cache inicial a,b,c e a solicitação a,a,d,e,b,b,a,c,f,d,e,a,f,b,e,c:

Solicitar aadebbacfdeafbec cache 1


aaddddaaadddfffc
cache 2 bbbeeeeccceeebbb
cache 3 ccccbbbbfffaaaee
falta de cache xxxxxxxxxxxxxxx

Treze falhas de cache por dezesseis solicitações não parece muito ideal. Vamos tentar o mesmo exemplo com outra
estratégia:

Exemplo (LFD)

Seja o tamanho do cache k=3 o cache inicial a,b,c e a solicitação a,a,d,e,b,b,a,c,f,d,e,a,f,b,e,c:

Solicitar aadebbacfdeafbec cache 1


aadeeeeeeeeeeeec
cache 2 bbbbbbaaaaaaffff
esconderijo 3 ccccccccfddddbbb
falta de cache xx xxx xxx

Oito erros de cache são muito melhores.

Autoteste: faça o exemplo para LIFO, LFU, RFU e veja o que aconteceu.

O programa de exemplo a seguir (escrito em C++) consiste em duas partes:

Notas de algoritmos do [Link] para profissionais 86


Machine Translated by Google

O esqueleto é uma aplicação que resolve o problema dependendo da estratégia gananciosa escolhida:

#incluir <iostream>
#incluir <memória>

usando namespace std;

const int tamanho do cache = 3;


const int comprimento da solicitação = 16;

const char request[] char = {'a','a','d','e','b','b','a','c','f','d','e','a','f','b','e','c' } ;


cache[]

// para redefinir
char originalCache[] = {'a','b','c'};

estratégia de classe {

público:
Estratégia(std::string nome) : strategyName(nome) {} virtual ~Strategy() =
default;

// calcula qual local de cache deve ser usado virtual int apply(int
requestIndex) = 0;

// atualiza as informações que a estratégia precisa virtual void


update(int cachePlace, int requestIndex, bool cacheMiss) = 0;

const std::string estratégiaNome;


};

bool updateCache(int requestIndex, Estratégia* estratégia) {

// calcula onde colocar a solicitação int cachePlace


= strategy->apply(requestIndex);

// prova se é um acerto de cache ou uma falha de cache bool isMiss =


request[requestIndex] != cache[cachePlace];

// atualizar estratégia (por exemplo, recontar distâncias) strategy-


>update(cachePlace, requestIndex, isMiss);

// escrever no cache cache


[cachePlace] = request[requestIndex];

retornar éMiss;
}

int principal()
{
Estratégia* selectedStrategy[] = { novo FIFO, novo LIFO, novo LRU, novo LFU, novo LFD };

for (int estrato=0; estrato < 5; ++ estrato) {

// redefinir cache para


(int i=0; i < cacheSize; ++i) cache[i] = originalCache[i];

<< selectedStrategy[strat]->strategyName << endl; cout <<"\nEstratégia: "

cout << "\nCache inicial: ("; para (int i=0; i <


cacheSize-1; ++i) cout << cache[i] << ",";

Notas de algoritmos do [Link] para profissionais 87


Machine Translated by Google

cout << cache[tamanhodocache-1] << ")\n\n";

cout << "Request_t"; para


(int i=0; i < cacheSize; ++i) cout << "cache " cout << "cache faltando" << << eu << "\t";
endl;

int cntPerdas = 0;

para(int i=0; i<comprimento da solicitação; ++i)


{
bool isMiss = updateCache(i, selectedStrategy[strat]); se (isMiss) ++cntMisses;

" "
corte << << solicitação[i] << "\t";
" "
para (int l=0; l < cacheSize; ++l) cout << cout << (isMiss ? << cache[l] << "\t";
"x" : "") << endl;
}

"
cout<< "\nTotal de falhas de cache: << cntPerde << endl;
}

para(int i=0; i<5; ++i) excluir estratégia selecionada[i];


}

A ideia básica é simples: para cada solicitação, tenho duas chamadas, minha estratégia:

1. apply: A estratégia tem que dizer ao chamador qual página usar 2. update: Depois

que o chamador usa o lugar, ele diz à estratégia se foi um erro ou não. Então a estratégia pode atualizar seus dados internos. A estratégia LFU, por exemplo,

tem que atualizar a frequência de acertos para as páginas de cache, enquanto a estratégia LFD tem que recalcular as distâncias para as páginas de

cache.

Agora vamos dar uma olhada em exemplos de implementações para nossas cinco estratégias:

FIFO

classe FIFO : estratégia pública { público:

FIFO() : Estratégia("FIFO") {

para (int i=0; i<cacheSize; ++i) idade[i] = 0;


}

int apply(int requestIndex) substituir {

int mais antigo = 0;

para(int i=0; i<tamanhodocache; ++i) {

se(cache[i] == solicitação[índicesolicitação]) retornar


i;

senão se(idade[i] > idade[mais velho])


mais velho = i;
}

retornar mais antigo;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituir {

// nada mudou, não precisamos atualizar as idades

Notas de algoritmos do [Link] para profissionais 88


Machine Translated by Google

se(!cacheMiss)
retornar;

// todas as páginas antigas ficam mais velhas, as novas ficam 0


para(int i=0; i<tamanhocache; ++i)
{
se(i != cachePos)
idade[i]++;

outro
idade[i] = 0;
}
}

privado:
int idade[tamanhodocache];
};

O FIFO só precisa da informação de quanto tempo uma página fica no cache (e, claro, apenas em relação às outras páginas). Então
a única coisa a fazer é esperar por uma falha e então fazer as páginas, que não foram despejadas mais antigas. Para o nosso exemplo
acima a solução do programa é:

Estratégia: FIFO

Cache inicial: (a,b,c)

Solicitar esconderijo 0 esconderijo 1 esconderijo 2 falta de cache


um um c
um um c
e e bbb c x
e e c x
e x
bb ddd e bb
um um e x
c um c x
e um c x
e c bff x
e e e x
um e um x
e ddddf e um x
b ff um x
e e x
c c bbb e x

Total de falhas de cache: 13

Essa é exatamente a solução acima.

UEPS

classe LIFO : estratégia pública {


público:
LIFO() : Estratégia("LIFO")
{
para (int i=0; i<cacheSize; ++i) idade[i] = 0;
}

int apply(int requestIndex) substituir


{
int mais novo = 0;

Notas de algoritmos do [Link] para profissionais 89


Machine Translated by Google

para(int i=0; i<tamanhocache; ++i)


{
se(cache[i] == solicitação[índicesolicitação])
retornar i;

senão se(idade[i] < idade[mais recente])


mais novo = i;
}

retornar mais recente;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituir


{
// nada mudou, não precisamos atualizar as idades
se(!cacheMiss)
retornar;

// todas as páginas antigas ficam mais velhas, as novas ficam 0


para(int i=0; i<tamanhocache; ++i)
{
se(i != cachePos)
idade[i]++;

outro
idade[i] = 0;
}
}

privado:
int idade[tamanhodocache];
};

A implementação do LIFO é mais ou menos a mesma que a do FIFO , mas nós despejamos a página mais nova, não a mais velha.
os resultados do programa são:

Estratégia: LIFO

Cache inicial: (a,b,c)

Solicitar esconderijo 0 esconderijo 1 esconderijo 2 falta de cache


um um b c
um um c
e e c x
e e bbb c x
e c
bb e c
um um bbb c x
c um c
c x
fd fd bbb c x
e e b c x
um um c x
ff c x
Facebook bbb c
e e c x
c e bb c

Total de erros de cache: 9

LRU

Notas de algoritmos do [Link] para profissionais 90


Machine Translated by Google

classe LRU : estratégia pública {


público:
LRU() : Estratégia("LRU")
{
para (int i=0; i<cacheSize; ++i) idade[i] = 0;
}

// aqui mais antigo significa não usado há mais tempo


int apply(int requestIndex) substituir
{
int mais antigo = 0;

para(int i=0; i<tamanhocache; ++i)


{
se(cache[i] == solicitação[índicesolicitação])
retornar i;

senão se(idade[i] > idade[mais velho])


mais antigo = i;
}

retornar mais antigo;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituir


{
// todas as páginas antigas ficam mais velhas, as usadas ficam 0
para(int i=0; i<tamanhocache; ++i)
{
se(i != cachePos)
idade[i]++;

outro
idade[i] = 0;
}
}

privado:
int idade[tamanhodocache];
};

No caso de LRU a estratégia é independente do que está na página de cache, seu único interesse é o último uso.

os resultados do programa são:

Estratégia: LRU

Cache inicial: (a,b,c)

Solicitar esconderijo 0 esconderijo 1 esconderijo 2 falta de cache


um um c
um um c
e um BBD-BBD c x
e um e x
e x
bb bb ddd e
um b um e x
c um c x
um c x
fd melhor amiga e c x
e e e x
um um dd e x

Notas de algoritmos do [Link] para profissionais 91


Machine Translated by Google
e um e e x
b um ff x
e e x
c e c bbb x

Total de falhas de cache: 13

LFU

classe LFU : estratégia pública {


público:
LFU() : Estratégia("LFU")
{
para (int i=0; i<cacheSize; ++i) requestFrequency[i] = 0;
}

int apply(int requestIndex) substituir


{
int menos = 0;

para(int i=0; i<tamanhocache; ++i)


{
se(cache[i] == solicitação[índicesolicitação])
retornar i;

senão se(frequênciasolicitada[i] < frequênciasolicitada[menor])


menos = i;
}

retornar menos;
}

void update(int cachePos, int requestIndex, bool cacheMiss) substituir


{
se(cacheMiss)
solicitaçãoFrequência[cachePos] = 1;

outro
++requestFrequency[cachePos];
}

privado:

// com que frequência a página foi usada


int requestFrequency[tamanhodocache];
};

O LFU despeja a página que menos usa. Então a estratégia de atualização é apenas contar cada acesso. Claro que depois de uma falha,
contagem reinicia. Os resultados do programa são:

Estratégia: LFU

Cache inicial: (a,b,c)

Solicitar esconderijo 0 esconderijo 1 esconderijo 2 falta de cache


um um c
um um bb c
e um e c x
e um e x
um e x
bb um dbb e
um um b e

Notas de algoritmos do [Link] para profissionais 92


Machine Translated by Google
c um b c x
um x
fd um fd x
e um bbb e x
um um e
um ff x
Facebook um bbb
e um e x
c um bb c x

Total de falhas de cache: 10

LFD
classe LFD : estratégia pública {
público:
LFD() : Estratégia("LFD")
{
// pré-calcula o próximo uso antes de começar a atender às solicitações
para (int i=0; i<tamanhodocache; ++i) nextUse[i] = calcNextUse(-1, cache[i]);
}

int apply(int requestIndex) substituir


{
int mais recente = 0;

para(int i=0; i<tamanhocache; ++i)


{
se(cache[i] == solicitação[índicesolicitação])
retornar i;

senão se(nextUse[i] > nextUse[mais recente])


mais recente = i;
}

retornar o mais recente;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituir


{
nextUse[cachePos] = calcNextUse(requestIndex, cache[cachePos]);
}

privado:

int calcNextUse(int requestPosition, char pageItem)


{
para(int i = requestPosition+1; i < requestLength; ++i)
{
se (request[i] == pageItem)
retornar i;
}

retornar requestLength + 1;
}

// próximo uso da página


int nextUse[tamanhodocache];
};

A estratégia LFD é diferente de todas as anteriores. É a única estratégia que usa as solicitações futuras para seu
decisão de quem despejar. A implementação usa a função calcNextUse para obter a página cujo próximo uso é

Notas de algoritmos do [Link] para profissionais 93


Machine Translated by Google

mais distante no futuro. A solução do programa é igual à solução à mão de cima:

Estratégia: LFD

Cache inicial: (a,b,c)

Solicitar esconderijo 0 esconderijo esconderijo 2 falta de cache


um um 1 c
um um bb c
e um e x
e um e x
b um bbb e
b um b e
um um b e
c um c e x
e um e e x
e um e x
e um e
um um ddd e
e x
Facebook e x
e fbb ddd e
c c e e x

Total de falhas de cache: 8

A estratégia gananciosa LFD é de fato a única estratégia ótima das cinco apresentadas. A prova é bastante longa e pode
ser encontrado aqui ou no livro de Jon Kleinberg e Eva Tardos (veja as fontes nos comentários abaixo).

Algoritmo vs Realidade

A estratégia LFD é ótima, mas há um grande problema. É uma solução offline ótima. Na prática, o cache é geralmente
um problema online , o que significa que a estratégia é inútil porque não podemos saber a próxima vez que precisarmos de um determinado
item. As outras quatro estratégias também são estratégias online . Para problemas online, precisamos de uma estratégia geral diferente
abordagem.

Seção 18.2: Ticket automático

Primeiro exemplo simples:

Você tem um bilhete automático que dá troca em moedas com valores 1, 2, 5, 10 e 20. A dispensação do
a troca pode ser vista como uma série de quedas de moedas até que o valor correto seja dispensado. Dizemos que uma dispensação é ótima
quando sua contagem de moedas é mínima para seu valor.

Seja M em [1,50] o preço do bilhete T e P em [1,50] o dinheiro que alguém pagou por T, com P >= M. Seja D=PM.
Definimos o benefício de um passo como a diferença entre D e Dc com c a moeda que o automático dispensa neste
etapa.

A Técnica Greedy para a troca é a seguinte abordagem pseudo algorítmica:

Passo 1: enquanto D > 20 dispensar uma moeda de 20 e definir D = D - 20

Passo 2: enquanto D > 10 dispensar uma moeda de 10 e definir D = D - 10

Passo 3: enquanto D > 5 dispensar uma moeda de 5 e definir D = D - 5

Passo 4: enquanto D > 2 dispensar uma moeda 2 e definir D = D - 2

Passo 5: enquanto D > 1 dispensar uma moeda de 1 e definir D = D - 1

Notas de algoritmos do [Link] para profissionais 94


Machine Translated by Google

Depois, a soma de todas as moedas é claramente igual a D. É um algoritmo ganancioso porque, após cada passo e após cada
repetição de um passo, o benefício é maximizado. Não podemos dispensar outra moeda com um benefício maior.

Agora o ticket automat como programa (em C++):

#incluir <iostream> #incluir


<vetor>
#incluir <string> #incluir
<algoritmo>

usando namespace std;

// leia alguns valores de moedas, classifique-os em ordem decrescente, //


limpe as cópias e garanta que a única moeda esteja nela std::vector<unsigned
int> readInCoinValues();

int principal() {

std::vector<unsigned int> coinValues; // Matriz de valores de moedas ascendente int ticketPrice;


// M no exemplo int
paidMoney; // P no exemplo

// gerar valores de moedas coinValues


= readInCoinValues();

cout << "preço do ingresso: "; cin >>


ticketPrice;

cout << "dinheiro pago: "; cin >>


paidMoney;

if(pagamentoDinheiro <= preçodoingresso)


{
cout << "Nenhum dinheiro para troca" << endl; return 1;

int diffValue = paidMoney - preçodoingresso;

// Aqui começa a ganância

// economizamos quantas moedas temos para distribuir std::vector<unsigned


int> coinCount;

para(auto coinValue = [Link](); coinValue !=


[Link](); ++coinValue)
{
int contagemMoedas = 0;

enquanto (diffValue >= *coinValue) {

diffValue -= *coinValue; contarMoedas+


+;
}

coinCount.push_back(contagemMoedas);
}

// imprime o resultado cout <<


"a diferença " é paga com: " << paidMoney - preço do ingresso << fim;
"
<<

Notas de algoritmos do [Link] para profissionais 95


Machine Translated by Google

para(unsigned int i=0; i < [Link](); ++i) {

if(coinCount[i] > 0) cout <<


"
coinCount[i] << moedas com valor " << coinValues[i] << endl;

retornar 0;
}

std::vector<unsigned int> readInCoinValues() {

// valores de moedas
std::vector<unsigned int> coinValues;

// certifique-se de que 1 esteja no vetor


coinValues.push_back(1);

// ler valores de moedas (atenção: o tratamento de erros é omitido) while(true) {

int valordamoeda;

cout << "Valor da moeda (<1 para parar): "; cin >>
coinValue;

se(valordamoeda > 0)
valoresdamoeda.push_back(valordamoeda);

senão
quebrar;
}

// classificar valores
sort([Link](), [Link](), std::greater<int>());

// apaga cópias do mesmo valor auto last =


std::unique([Link](), [Link]()); [Link](last, [Link]());

// imprimir array cout


<< "Valores de moedas: ";

para(auto i : coinValues) " ";


cout << eu <<

cout << fim;

retornar coinValues;
}

Esteja ciente de que agora há uma verificação de entrada para manter o exemplo simples. Um exemplo de saída:

Valor da moeda (<1 para parar): 2


Valor da moeda (<1 para parar): 4
Valor da moeda (<1 para parar): 7
Valor da moeda (<1 para parar): 9
Valor da moeda (<1 para parar): 14
Valor da moeda (<1 para parar): 4
Valor da moeda (<1 para parar): 0

Notas de algoritmos do [Link] para profissionais 96


Machine Translated by Google
Valores das moedas: 14 9 7 4 2 1
preço do bilhete: 34
dinheiro pago: 67 a
diferença 33 é paga com: 2 moedas com valor 14 1
moedas com valor 4 1 moedas
com valor 1

Enquanto 1 estiver nos valores da moeda, sabemos que o algoritmo será encerrado, porque:

D diminui rigorosamente a cada passo


D nunca é >0 e menor que a menor moeda 1 ao mesmo tempo

Mas o algoritmo tem duas armadilhas:

1. Seja C o maior valor da moeda. O tempo de execução é polinomial apenas enquanto D/C for polinomial, porque o
a representação de D usa apenas bits de log D e o tempo de execução é pelo menos linear em D/C.
2. Em cada passo nosso algoritmo escolhe o ótimo local. Mas isso não é suficiente para dizer que o algoritmo encontra a solução ótima
global (veja mais informações aqui ou no Livro de Korte e Vygen).

Um simples contraexemplo: as moedas são 1,3,4 e D=6. A solução ótima é claramente duas moedas de valor 3, mas o ganancioso
escolhe 4 no primeiro passo, então ele tem que escolher 1 no passo dois e três. Então ele não dá uma solução ótima. Um possível
Algoritmo ótimo para este exemplo é baseado em programação dinâmica.

Seção 18.3: Programação de intervalos


Temos um conjunto de jobs J={a,b,c,d,e,f,g}. Seja j em J um job que começa em sj e termina em fj. Dois jobs são compatíveis se não
se sobrepõem. Uma imagem como exemplo:

O objetivo é encontrar o subconjunto máximo de jobs mutuamente compatíveis. Existem várias abordagens gananciosas para esse
problema:

1. Horário de início mais cedo: considere os trabalhos em ordem


crescente de sj 2. Horário de término mais cedo: considere os trabalhos
em ordem crescente de fj 3. Intervalo mais curto: considere os trabalhos em
ordem crescente de fj-sj 4. Menos conflitos: para cada trabalho j, conte o número de trabalhos conflitantes cj

A questão agora é qual abordagem é realmente bem-sucedida. Início antecipado definitivamente não, aqui está um contra-exemplo

Notas de algoritmos do [Link] para profissionais 97


Machine Translated by Google

O intervalo mais curto também não é o ideal

e menos conflitos pode realmente parecer ótimo, mas aqui está um caso problemático para esta abordagem:

O que nos deixa com o tempo de término mais cedo. O pseudocódigo é bem simples:

1. Classifique os trabalhos por hora de término de modo que


f1<=f2<=...<=fn 2. Seja A um

conjunto vazio 3. para j=1 a n se j for compatível com todos os trabalhos em A, defina A=A+{j}

4. A é um subconjunto máximo de empregos mutuamente compatíveis

Ou como programa C++:

#include <iostream> #include


<utilitário> #include <tupla>
#include <vetor> #include
<algoritmo>

const int jobCnt = 10;

// Horários de início do
trabalho const int startTimes[] = { 2, 3, 1, 4, 3, 2, 6, 7, 8, 9};

// Horários de término do
trabalho const int endTimes[] = { 4, 4, 3, 5, 5, 5, 8, 9, 9, 10};

usando namespace std;

int principal()
{
empregos vector<par<int,int>> ;

para(int i=0; i<jobCnt; ++i)


jobs.push_back(make_pair(horáriosinício[i], horáriosfim[i]));

// passo 1: classificar
sort([Link](), [Link](),[](pair<int,int> p1, pair<int,int> p2) { return [Link] < [Link]; });

Notas de algoritmos do [Link] para profissionais 98


Machine Translated by Google

// passo 2: conjunto vazio A


vector<int> A;

// passo 3:
para(int i=0; i<jobCnt; ++i) {

trabalho automático =
jobs[i]; bool isCompatible = true;

para(auto jobIndex : A) {

// teste se o trabalho real e o trabalho de A são incompatíveis if([Link] >= jobs[jobIndex].first


&& [Link] <= jobs[jobIndex].second)

{
éCompatível = falso; quebrar;

}
}

se(éCompatível)
A.push_back(i);
}

//passo 4: imprimir A
cout << "Compatível: ";

para(auto i : A) cout
<< "(" << empregos[i].primeiro << "," << empregos[i].segundo << ") "; cout << fim;

retornar 0;
}

A saída para este exemplo é: Compatível: (1,3) (4,5) (6,8) (9,10)

A implementação do algoritmo está claramente em ÿ(n^2). Há uma implementação ÿ(n log n) e o leitor interessado pode continuar
lendo abaixo (Exemplo Java).

Agora temos um algoritmo guloso para o problema de agendamento de intervalos, mas ele é ótimo?

Proposição: O tempo de término mais cedo do algoritmo guloso é o ideal.

Prova: (por contradição)

Suponha que ganancioso não seja ótimo e i1,i2,...,ik denote o conjunto de trabalhos selecionados por ganancioso. Seja j1,j2,...,jm
denote o conjunto de trabalhos em uma solução ótima com i1=j1,i2=j2,...,ir=jr para o maior valor possível de r.

O trabalho i(r+1) existe e termina antes de j(r+1) (término mais cedo). Mas então j1,j2,...,jr,i(r+1),j(r+2),...,jm também é uma solução
ótima e para todo k em [1,(r+1)] é jk=ik. isso é uma contradição à maximalidade de r. Isso conclui a prova.

Este segundo exemplo demonstra que geralmente há muitas estratégias gananciosas possíveis, mas apenas algumas ou mesmo
nenhuma podem encontrar a solução ideal em todos os casos.

Abaixo está um programa Java que roda em ÿ(n log n)

importar [Link];

Notas de algoritmos do [Link] para profissionais 99


Machine Translated by Google

importar [Link];

classe Trabalho

{
int início, fim, lucro;

Trabalho(int início, int fim, int lucro) {

[Link] = início; [Link] =


fim; [Link] = lucro;

}
}

classe JobComparator implementa Comparator<Job> {

público int compare(Trabalho a, Trabalho


b) {
retornar [Link] < [Link] ? -1 : [Link] == [Link] ? 0 : 1;
}
}

classe pública WeightedIntervalScheduling {

estático público int binarySearch(Job jobs[], int index) {

int lo = 0, hi = índice - 1;

enquanto (lo <= hi) {

int médio = (baixo + alto) / 2; se


(trabalhos[médio].fim <= trabalhos[índice].início) {

se (empregos[meio + 1].fim <= empregos[índice].início) lo = meio + 1; senão


retorne meio;

}
senão hi = mid - 1;
}

retornar -1;
}

estático público int agendamento( Trabalho empregos[])


{
[Link](empregos, novo JobComparator());

int n = [Link];
int tabela[] = novo int[n]; tabela[0] =
empregos[0].lucro;

para (int i=1; i<n; i++) {

int inclProf = empregos[i].lucro; int l =


binarySearch(empregos, i); se (l != -1) inclProf
+= tabela[l];

tabela[i] = [Link](inclProf, tabela[i-1]);

Notas de algoritmos do [Link] para profissionais 100


Machine Translated by Google

retornar tabela[n-1];
}

público estático vazio main(String[] args) {

Trabalho jobs[] = {novo Trabalho(1, 2, 50), novo Trabalho(3, 5, 20),


novo Trabalho(6, 19, 100), novo Trabalho(2, 100, 200)};

"
[Link](" O lucro ideal é + agendar(trabalhos));
}
}

E a saída esperada é:

O lucro ideal é 250

Seção 18.4: Minimizando atrasos


Existem vários problemas minimizando o atraso, aqui temos um único recurso que pode processar apenas um trabalho por vez. O trabalho j
requer tj unidades de tempo de processamento e vence no tempo dj. se j começar no tempo sj, ele terminará no tempo fj=sj+tj. Definimos o
atraso L=max{0,fj-dh} para todos os j. O objetivo é minimizar o atraso máximo L.

123456
tj 3 2 1 4 3 2
DJ 6 8 9 9 10 11

Trabalho 3 2 2 5 5 5 4 4 4 4 1 1 1 6 6
Tempo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Nível -8 -5 -4 1 7 4

A solução L=7 obviamente não é ótima. Vamos dar uma olhada em algumas estratégias gananciosas:

1. Menor tempo de processamento primeiro: agende os trabalhos em ordem crescente de tempo de


processamento j` 2. Prazo mais curto primeiro: agende os trabalhos em ordem crescente
de prazo dj 3. Menor folga: agende os trabalhos em ordem crescente de folga dj-tj

É fácil ver que o menor tempo de processamento primeiro não é o ideal. Um bom contra-exemplo é

12
tj 1 5
DJ 10 5

a menor solução de pilha tem problemas semelhantes

12
tj 1 5
DJ 3 5

a última estratégia parece válida, então começamos com algum pseudocódigo:

1. Classifique n trabalhos por tempo de vencimento para que d1<=d2<=...<=dn


2. Defina t=0

3. para j=1 a n
Atribuir tarefa j ao intervalo [t,t+tj]

Notas de algoritmos do [Link] para profissionais 101


Machine Translated by Google

defina sj=t e fj=t+tj


defina t=t+tj
4. intervalos de retorno [s1,f1],[s2,f2],...,[sn,fn]

E como implementação em C++:

#include <iostream> #include


<utilitário> #include <tupla>
#include <vetor> #include
<algoritmo>

const int jobCnt = 10;

// Horários de início do
trabalho const int processTimes[] = { 2, 3, 1, 4, 3, 2, 3, 5, 2, 1};

// Horários de término
do trabalho const int dueTimes[] = { 4, 7, 9, 13, 8, 17, 9, 11, 22, 25};

usando namespace std;

int principal()
{
empregos vector<par<int,int>> ;

para(int i=0; i<jobCnt; ++i)


jobs.push_back(make_pair(processTimes[i], dueTimes[i]));

// passo 1: classificar
sort([Link](), [Link](),[](pair<int,int> p1, pair<int,int> p2) { return [Link] < [Link]; });

// passo 2: definir t=0 int t =


0;

// etapa 3:
vector<pair<int,int>> jobIntervals;

para(int i=0; i<jobCnt; ++i) {

jobIntervals.push_back(make_pair(t,t+jobs[i].first)); t += jobs[i].first;

//passo 4: imprimir intervalos cout <<


"Intervalos:\n" << endl;

int atraso = 0;

para(int i=0; i<jobCnt; ++i) {

par automático = jobIntervals[i];

atraso = max(atraso, [Link]-jobs[i].second);

cout << "(" << [Link] << "," << [Link] << ") "
<< "Atraso: " << [Link]-empregos[i].segundo << std::endl;
}

"
cout << "\natraso máximo é << atraso << fiml;

Notas de algoritmos do [Link] para profissionais 102


Machine Translated by Google

retornar 0;
}

E a saída para este programa é:

Intervalos:

(0,2) Atraso:-2
(2,5) Atraso:-2
(5,8) Atraso: 0
(8,9) Atraso: 0
(9,12) Atraso: 3
(12,17) Atraso: 6
(17,21) Atraso: 8
(21,23) Atraso: 6
(23,25) Atraso: 3
(25,26) Atraso: 1

atraso máximo é 8

O tempo de execução do algoritmo é obviamente ÿ(n log n) porque a classificação é a operação dominante deste algoritmo.
Agora precisamos mostrar que é ótimo. Claramente, um cronograma ótimo não tem tempo ocioso. o prazo mais cedo primeiro
a programação também não tem tempo ocioso.

Vamos supor que os trabalhos sejam numerados de modo que d1<=d2<=...<=dn. Dizemos que uma inversão de um cronograma é um par de
trabalhos i e j de modo que i<j, mas j é agendado antes de i. Devido à sua definição, o primeiro cronograma com prazo mais cedo não tem
inversões. Claro que se um cronograma tem uma inversão, ele tem um com um par de trabalhos invertidos agendados consecutivamente.

Proposição: Trocar dois trabalhos adjacentes e invertidos reduz o número de inversões em um e não aumenta o atraso máximo.

Prova: Seja L o atraso antes da troca e M o atraso depois. Como trocar dois jobs adjacentes não move os outros jobs de suas
posições, é Lk=Mk para todo k != i,j.

Claramente é Mi<=Li, já que o trabalho i foi agendado mais cedo. Se o trabalho j estiver atrasado, segue a definição:

Mj = fi-dj <= fi- (definição) (já


di que i e j são trocados)
<= Eu

Isso significa que o atraso após a troca é menor ou igual ao anterior. Isso conclui a prova.

Proposição: O prazo mais próximo do primeiro cronograma S é o ideal.

Prova: (por contradição)

Vamos supor que S* seja um cronograma ótimo com o menor número possível de inversões. Podemos supor que S* não tem tempo ocioso. Se
S* não tem inversões, então S=S* e estamos prontos. Se S* tem uma inversão, então tem uma inversão adjacente. A última Proposição
afirma que podemos trocar a inversão adjacente sem aumentar o atraso, mas diminuindo o número de inversões. Isso contradiz a definição de S*.

O problema de minimização de atraso e seu problema de makespan mínimo quase relacionado , onde a questão para um cronograma
mínimo é feita, têm muitas aplicações no mundo real. Mas geralmente você não tem apenas uma máquina, mas muitas, e elas lidam com a
mesma tarefa em taxas diferentes. Esses problemas ficam NP-completos muito rápido.

Notas de algoritmos do [Link] para profissionais 103


Machine Translated by Google

Outra questão interessante surge se não olharmos para o problema offline , onde temos todas as tarefas e dados em
mão, mas na variante online , onde as tarefas aparecem durante a execução.

Notas de algoritmos do [Link] para profissionais 104


Machine Translated by Google

Capítulo 19: Algoritmo de Prim


Seção 19.1: Introdução ao Algoritmo de Prim
Digamos que temos 8 casas. Queremos instalar linhas telefônicas entre essas casas. A borda entre as casas representa o custo de
instalação de linha entre duas casas.

Nossa tarefa é configurar linhas de tal forma que todas as casas estejam conectadas e o custo de configuração de toda a conexão
seja mínimo. Agora, como descobrimos isso? Podemos usar o Algoritmo de Prim.

Algoritmo de Prim é um algoritmo guloso que encontra uma árvore geradora mínima para um grafo não direcionado ponderado. Isso
significa que ele encontra um subconjunto das arestas que formam uma árvore que inclui todos os nós, onde o peso total de todas
as arestas na árvore é minimizado. O algoritmo foi desenvolvido em 1930 pelo matemático tcheco Vojtÿch Jarník e mais tarde
redescoberto e republicado pelo cientista da computação Robert Clay Prim em 1957 e Edsger Wybe Dijkstra em 1959. Também é
conhecido como algoritmo DJP, algoritmo de Jarnik, algoritmo Prim-Jarnik ou algoritmo Prim-Dijsktra.

Agora, vamos olhar primeiro para os termos técnicos. Se criarmos um grafo, S usando alguns nós e arestas de um grafo não
direcionado G, então S é chamado de subgrafo do grafo G. Agora S será chamado de Spanning Tree se e somente se:

Ele contém todos os nós de G.


É uma árvore, o que significa que não há ciclo e todos os nós estão conectados.
Existem (n-1) arestas na árvore, onde n é o número de nós em G.

Pode haver muitas Spanning Tree's de um grafo. A Minimum Spanning Tree de um grafo não direcionado ponderado é uma árvore,
tal que a soma do peso das arestas é mínima. Agora usaremos o algoritmo de Prim para descobrir a spanning tree mínima,
que é como configurar as linhas telefônicas em nosso grafo de exemplo de tal forma que o custo de configuração seja mínimo.

Primeiro, selecionaremos um nó de origem . Digamos que o nó-1 seja nossa origem. Agora, adicionaremos a aresta do nó-1 que tem o
custo mínimo ao nosso subgrafo. Aqui, marcamos as arestas que estão no subgrafo usando a cor azul. Aqui, 1-5 é

Notas de algoritmos do [Link] para profissionais 105


Machine Translated by Google

nossa vantagem desejada.

Agora consideramos todas as arestas do nó-1 e nó-5 e pegamos o mínimo. Como 1-5 já está marcado, nós

pegue 1-2.

Desta vez, consideramos o nó-1, o nó-2 e o nó-5 e tomamos a aresta mínima que é 5-4.

Notas de algoritmos do [Link] para profissionais 106


Machine Translated by Google

O próximo passo é importante. Do nó-1, nó-2, nó-5 e nó-4, a aresta mínima é 2-4. Mas se selecionarmos
esse, ele criará um ciclo em nosso subgrafo. Isso ocorre porque o nó-2 e o nó-4 já estão em nosso subgrafo. Então
tomar a aresta 2-4 não nos beneficia. Selecionaremos as arestas de tal forma que adicione um novo nó em nosso subgrafo. Então nós

selecione a aresta 4-8.

Se continuarmos dessa forma, selecionaremos as arestas 8-6, 6-7 e 4-3. Nosso subgrafo ficará assim:

Notas de algoritmos do [Link] para profissionais 107


Machine Translated by Google

Este é o nosso subgrafo desejado, que nos dará a árvore de abrangência mínima. Se removermos as arestas que não

selecione, obteremos:

Esta é nossa árvore de abrangência mínima (MST). Então o custo de configuração das conexões telefônicas é: 4 + 2 + 5 + 11 + 9
+ 2 + 1 = 34. E o conjunto de casas e suas conexões são mostrados no gráfico. Pode haver vários MST de um
gráfico. Depende do nó de origem que escolhermos.

O pseudocódigo do algoritmo é fornecido abaixo:

Procedimento PrimsMST(Gráfico): // aqui Graph é um grafo ponderado conectado não vazio


Vnovo[] = {x} // Novo subgrafo Vnew com nó de origem x

Notas de algoritmos do [Link] para profissionais 108


Machine Translated by Google

Enew[] = {}
enquanto Vnew não for igual a V u -> um nó
de Vnew v -> um nó que não está
em Vnew tal que a aresta uv tenha o custo mínimo
// se dois nós tiverem o mesmo peso, escolha qualquer um deles
adicione v a Vnew
adicione aresta (u, v) a Enew fim
enquanto
Retornar Vnew e Enew

Complexidade:

A complexidade de tempo da abordagem ingênua acima é O(V²). Ela usa matriz de adjacência. Podemos reduzir a complexidade usando fila
de prioridade. Quando adicionamos um novo nó a Vnew, podemos adicionar suas arestas adjacentes na fila de prioridade. Então, retire a aresta
ponderada mínima dele. Então a complexidade será: O(ElogE), onde E é o número de arestas.
Novamente, um heap binário pode ser construído para reduzir a complexidade para O(ElogV).

O pseudocódigo usando Priority Queue é fornecido abaixo:

Procedimento MSTPrim(Gráfico, fonte): para cada u


em V chave[u] := inf
pai[u] := NULL fim
para chave[fonte] := 0 Q =

Priority_Queue()

Q=V
enquanto Q não estiver vazio u
-> [Link] para
cada v adjacente a i se v pertencer a Q
e Edge(u,v) < key[v] // aqui Edge(u, v) representa // custo de edge(u,
v)
parent[v] := u key[v] :=
Edge(u, v) fim se fim para fim
enquanto

Aqui key[] armazena o custo mínimo de travessia do nó-v. parent[] é usado para armazenar o nó pai. É útil para travessia e impressão da árvore.

Abaixo está um programa simples em Java:

importar [Link].*;

classe pública Gráfico {

privado estático int infinito = 9999999; int[][] LinkCost;

Inteiro Nós N;
Gráfico(int[][] mat) {

int i, j; NNodes
= comprimento da esteira;
LinkCost = novo int[NNodes][NNodes]; para ( i=0; i <
NNodes; i++) {

para ( j=0; j < NNodes; j++) {

Notas de algoritmos do [Link] para profissionais 109


Machine Translated by Google

CustoDoLink [ i ] [ j ] = mat [ i ] [ j ] ; se
( CustoDoLink [ i ] [ j ] == 0 )
LinkCost [ i ] [ j ] = infinito ;
}

} para ( i = 0 ; i < NNodes ; i++ ) {

para ( j = 0 ; j < NNodes ; j++ ) se


( LinkCost [ i ] [ j ] < infinito )
Sistema .out .print ( " " senão + LinkCost [ i ] [ j ] + "");

Sistema .out .print (" * " ) ;


Sistema .out .println ( ) ;
}

} público int não alcançado (booleano [ ] r ) {

boolean done = verdadeiro ;


para ( int i = 0 ; i < [Link] ; i++) se ( r [ i ] == falso )
retornar i ; retornar - 1 ;

} público void Prim ( ) {

int i, j, k, x, y ; boolean
[ ] Alcançado = novo boolean [ NNodes ] ; int [ ] predNode
= novo int [NNodes ] ;
Atingido [ 0 ] =
verdadeiro ; para ( k = 1 ; k < NNodes ; k+
+){
Atingido [ k ] = falso ;

} predNode [ 0 ] = 0 ;
printReachSet ( Alcançado ) ; para
(k = 1 ; k < NNodes ; k++ ) {

x=y=0;
para ( i = 0 ; i < NNodes ; i++ ) para ( j =
0;{ j < NNodes ; j++ )

se ( Alcançado [ i ] && ! Alcançado [ j ] &&


Custo do Link [ i ] [ j ] < Custo do Link [ x ] [ y ] )
{
x = eu ;
y=j;
}
}
Sistema .out .println ("Custo mínimo da aresta: (" + + x + "," + +
y + ")" + "custo =
+ LinkCost [ x ]
"
[y]);
predNode [ y ] = x ;
Atingido [ y ] = true ;
printReachSet ( Atingido ) ;
Sistema .out .println ( ) ;

} int [ ] a = predNode ;
para ( i = 0 ; i < NNodes ; i++ )
" --> "
Sistema .out .println ( a [ i ] + + eu ) ;

} void printReachSet (boolean [ ] Atingido )

Notas de algoritmos do [Link] para profissionais 110


Machine Translated by Google

{
Sistema .out .print ("ReachSet = " ) ; para
(int i = 0 ; i < Alcanç[Link] ; i++ ) se ( Alcançado
[i])
Sistema .out .print ( i + " " ) ; //
[Link]();
}
público estático vazio principal (String [ ] args )
{
int [ ] [ ] conn = { { 0 { 3 { 0 , 3 , 0 , 2 , 0 , 0 , 0 , 0 , 4 }, // 0
, 0 , 0 , 0 , 0 , 0 , 0 , 4 , 0 }, // 1
, 0 , 0 , 6 , 0 , 1 , 0 , 2 , 0 }, // 2
, 0 , 6 , 0 , 1 , 0 , 0 , 0 , 0 }, // 3
, 0 , 0 , 1 , 0 , 0 , 0 , 0 , 8 }, // 4
, 0 , 1 , 0 , 0 , 0 , 8 , 0 , 0 }, // 5
, 0 , 0 , 0 , 0 , 8 , 0 , 0 , 0 }, // 6
, 4 , 2 , 0 , 0 , 0 , 0 , 0 , 0 }, // 7 0 } //
, 0, 0, 0, 8, 0, 0, 0, 8
{2{0{0{0{0{4};
Gráfico G = novo gráfico (conn ) ;
[Link] ( ) ;
}
}

Compile o código acima usando javac [Link]

Saída:

$ java Gráfico
*3*2****4
3******4*
***6*1*2*2*6*1****

***1****8
**1***8*******8***

*42******
4***8****
ReachSet = 0 Custo mínimo de borda : ( 0 , 3 )custo = 2
AlcanceConjunto = 0 3
Margem de custo mínimo : ( 3 , 4 )custo = 1
AlcanceConjunto = 0 3 4
Custo mínimo de vantagem(: 01 ,) custo = 3
ReachSet = 0 1 3 4 8 )custo
Margem de custo mínimo : ( 0 ,= 4
AlcanceConjunto = 0 1 3 4 8
Margem de custo mínimo : ( 1 , 7 )custo = 4
AlcanceConjunto = 0 1 3 4 7 8
Custo mínimo de vantagem( :72,) custo = 2
ReachSet = 0 1 2 3 4 7 8 5 )custo = 1
Margem de custo mínimo : ( 2,
AlcanceConjunto = 0 1 2 3 4 5 7 8
Custo mínimo de vantagem( :56,) custo = 8
AlcanceConjunto = 0 1 2 3 4 5 6 7 8
0 --> 0 0
--> 1
7 --> 2
0 --> 3 3
--> 4
2 --> 5
5 --> 6

Notas de algoritmos do [Link] para profissionais 111


Machine Translated by Google

1 --> 7
0 --> 8

Notas de algoritmos do [Link] para profissionais 112


Machine Translated by Google

Capítulo 20: Algoritmo Bellman-Ford


Seção 20.1: Algoritmo de caminho mais curto de fonte única (dado que há um
ciclo negativo em um gráfico)

Antes de ler este exemplo, é necessário ter uma breve ideia sobre relaxamento de borda. Você pode aprender aqui

Bellman-Ford O algoritmo calcula os caminhos mais curtos de um único vértice de origem para todos os outros vértices em um dígrafo ponderado.
Embora seja mais lento que o Algoritmo de Dijkstra, ele funciona nos casos em que o peso da aresta é negativo e também encontra um ciclo de
peso negativo no gráfico. O problema com o Algoritmo de Dijkstra é que, se houver um ciclo negativo, você continua passando pelo ciclo
repetidamente e continua reduzindo a distância entre dois vértices.

A ideia desse algoritmo é percorrer todas as arestas desse gráfico, uma por uma, em alguma ordem aleatória. Pode ser qualquer ordem aleatória. Mas você deve

garantir que, se uv (onde u e v são dois vértices em um gráfico) for uma de suas ordens, então deve haver uma aresta de u a v. Normalmente, ela é tirada

diretamente da ordem da entrada fornecida. Novamente, qualquer ordem aleatória funcionará.

Após selecionar a ordem, relaxaremos as arestas de acordo com a fórmula de relaxamento. Para uma aresta uv dada indo de u para v a fórmula de
relaxamento é:

se distância[u] + custo[u][v] < d[v]


d[v] = d[u] + custo[u][v]

Isto é, se a distância da fonte a qualquer vértice u + o peso da aresta uv for menor que a distância da fonte a outro vértice v, atualizamos a distância da

fonte a v. Precisamos relaxar as arestas no máximo (V-1) vezes, onde V é o número de arestas no gráfico. Por que (V-1), você pergunta? Explicaremos em

outro exemplo. Também manteremos o controle do vértice pai de qualquer vértice, ou seja, quando relaxamos uma aresta, definiremos:

pai[v] = u

Isso significa que encontramos outro caminho mais curto para chegar a v via u. Precisaremos disso mais tarde para imprimir o caminho mais curto da origem
até o vértice de destino.

Vamos ver um exemplo. Temos um gráfico:

Notas de algoritmos do [Link] para profissionais 113


Machine Translated by Google

Selecionamos 1 como o vértice de origem . Queremos descobrir o caminho mais curto da origem para todos os outros
vértices.

A princípio, d[1] = 0 porque é a fonte. E o resto é infinito, porque ainda não sabemos sua distância.

Relaxaremos as arestas nesta sequência:

+--------+--------+--------+--------+--------+---- ----+--------+
| Série | 1 | 2 | 3 | 4| 5 | 6 |
+--------+--------+--------+--------+--------+---- ----+--------+
| Borda | 4->5 | 3->4 | 1->3 | 1->4 | 4->6 | 2->3 |
+--------+--------+--------+--------+--------+---- ----+--------+

Você pode pegar qualquer sequência que quiser. Se relaxarmos as bordas uma vez, o que obtemos? Obtemos a distância da fonte
para todos os outros vértices do caminho que usa no máximo 1 aresta. Agora vamos relaxar as arestas e atualizar os valores de d[]. Nós
pegar:

1. d[4] + cost[4][5] = infinito + 7 = infinito. Não podemos atualizar este.


2. d[2] + cost[3][4] = infinito. Não podemos atualizar este.
3. d[1] + custo[1][3] = 0 + 2 = 2 < d[2]. Então d[3] = 2. Também pai[1] = 1.
4. d[1] + custo[1][4] = 4. Então d[4] = 4 < d[4]. pai[4] = 1.
5. d[4] + custo[4][6] = 9. d[6] = 9 < d[6]. pai[6] = 4.
6. d[2] + cost[2][3] = infinito. Não podemos atualizar este.

Não conseguimos atualizar alguns vértices, porque a condição d[u] + cost[u][v] < d[v] não correspondeu. Como dissemos
antes, encontrávamos os caminhos da origem para outros nós usando no máximo 1 aresta.

Nossa segunda iteração nos fornecerá o caminho usando 2 nós. Obtemos:

1. d[4] + custo[4][5] = 12 < d[5]. d[5] = 12. pai[5] = 4.


2. d[3] + custo[3][4] = 1 < d[4]. d[4] = 1. pai[4] = 3.
3. d[3] permanece inalterado.
4. d[4] permanece inalterado.
5. d[4] + custo[4][6] = 6 < d[6]. d[6] = 6. pai[6] = 4.
6. d[3] permanece inalterado.

Notas de algoritmos do [Link] para profissionais 114


Machine Translated by Google

Nosso gráfico ficará assim:

Nossa 3ª iteração atualizará apenas o vértice 5, onde d[5] será 8. Nosso gráfico ficará assim:

Depois disso, não importa quantas iterações fizermos, teremos as mesmas distâncias. Então, manteremos um sinalizador que verifica se alguma atualização

ocorre ou não. Se não ocorrer, simplesmente quebramos o loop. Nosso pseudocódigo será:

Procedimento Bellman-Ford(Gráfico, fonte): n := número de


vértices no gráfico para i de 1 a n d[i] := infinito
pai[i] := NULO fim para
d[fonte] := 0 para i de 1 a
n-1 sinalizador := falso
para todas
as arestas de (u,v) no
gráfico se d[u] + custo[u][v] <
d[v]

d[v] := d[u] + custo[u][v] pai[v] := u


sinalizador :=
verdadeiro

Notas de algoritmos do [Link] para profissionais 115


Machine Translated by Google
fim se
fim para
se sinalizador == falso
quebrar
fim para
Retornar d

Para manter o controle do ciclo negativo, podemos modificar nosso código usando o procedimento descrito aqui. Nosso pseudocódigo
completo será:

Procedimento Bellman-Ford-With-Negative-Cycle-Detection(Graph, source): n := número de vértices no gráfico


para i de 1 a n d[i] := infinito pai[i] := NULL

fim para
d[fonte] := 0 para i de
1 a n-1
flag := false para
todas as arestas de (u,v) no gráfico se d[u] + custo[u]
[v] < d[v]
d[v] := d[u] + custo[u][v] pai[v] := u
sinalizador :=
verdadeiro fim se
fim para
se
sinalizador == falso
quebrar
fim para
todas as arestas de (u,v) em Graph se d[u] + cost[u]
[v] < d[v]
Retornar "Ciclo negativo detectado" fim se fim para

Retornar d

Caminho de impressão:

Para imprimir o caminho mais curto para um vértice, iteraremos de volta ao seu pai até encontrarmos NULL e então imprimiremos os vértices.
O pseudocódigo será:

Procedimento PathPrinting(u) v :=
parent[u] se v ==
NULL
retornar
PathPrinting(v) imprimir
-> u

Complexidade:

*
Como precisamos relaxar as arestas no máximo (V-1) vezes, a complexidade de tempo desse algoritmo será igual a O(VE), onde E denota
o número de arestas, se usarmos a lista de adjacência para representar o grafo. No entanto, se a matriz de adjacência for usada para
representar o grafo, a complexidade de tempo será O(V^3). O motivo é que podemos iterar por todas as arestas em tempo O(E) quando a
lista de adjacência é usada, mas leva tempo O(V^2) quando a matriz de adjacência é usada.

Seção 20.2: Detectando ciclo negativo em um gráfico


Para entender este exemplo, é recomendável ter uma breve ideia sobre o algoritmo Bellman-Ford que pode ser encontrado

Notas de algoritmos do [Link] para profissionais 116


Machine Translated by Google

aqui

Usando o algoritmo Bellman-Ford, podemos detectar se há um ciclo negativo em nosso gráfico. Sabemos que, para descobrir o caminho mais
curto, precisamos relaxar todas as arestas do gráfico (V-1) vezes, onde V é o número de vértices em um gráfico.
Já vimos que neste exemplo, após iterações (V-1) , não podemos atualizar d[], não importa quantas iterações façamos. Ou podemos?

Se houver um ciclo negativo em um gráfico, mesmo após (V-1) iterações, podemos atualizar d[]. Isso acontece porque para cada iteração,
atravessar o ciclo negativo sempre diminui o custo do caminho mais curto. É por isso que o algoritmo Bellman-Ford limita o número de iterações
para (V-1). Se usássemos o Algoritmo de Dijkstra aqui, estaríamos presos em um loop infinito. No entanto, vamos nos concentrar em encontrar
o ciclo negativo.

Vamos supor que temos um gráfico:

Vamos escolher o vértice 1 como a fonte. Após aplicar o algoritmo de caminho mais curto de fonte única de Bellman-Ford ao gráfico, descobriremos
as distâncias da fonte para todos os outros vértices.

É assim que o gráfico se parece após (V-1) = 3 iterações. Deve ser o resultado, já que há 4 arestas, precisamos de no máximo 3 iterações para
descobrir o caminho mais curto. Então, ou esta é a resposta, ou há um ciclo de peso negativo no gráfico. Para descobrir isso, após (V-1) iterações,
fazemos mais uma iteração final e se a distância continuar a diminuir, significa que definitivamente há um ciclo de peso negativo no gráfico.

Notas de algoritmos do [Link] para profissionais 117


Machine Translated by Google

Para este exemplo: se verificarmos 2-3, d[2] + cost[2][3] nos dará 1, que é menor que d[3]. Então podemos concluir que há um ciclo
negativo em nosso gráfico.

Então como descobrimos o ciclo negativo? Fazemos uma pequena modificação no procedimento de Bellman-Ford:

Procedimento NegativeCycleDetector(Graph, source): n := número de vértices


no gráfico para i de 1 a n d[i] := infinito fim para
d[source] := 0 para i de 1 a
n-1 flag := falso para
todas as
arestas de (u,v) no
gráfico se d[u] + cost[u][v] <
d[v]

d[v] := d[u] + custo[u][v] flag := verdadeiro


fim se fim para se
flag ==
falso
quebrar

fim para
todas as arestas de (u,v) em Graph se d[u] + cost[u][v]
< d[v]
Retornar "Ciclo negativo detectado" fim se

fim para
Retornar "Nenhum ciclo negativo"

É assim que descobrimos se há um ciclo negativo em um gráfico. Também podemos modificar o Algoritmo Bellman-Ford para manter o
controle de ciclos negativos.

Seção 20.3: Por que precisamos relaxar todas as arestas no


máximo (V-1) vezes
Para entender este exemplo, é recomendável ter uma breve ideia sobre o algoritmo de caminho mais curto de fonte única de Bellman-Ford,
que pode ser encontrado aqui

No algoritmo Bellman-Ford, para descobrir o caminho mais curto, precisamos relaxar todas as arestas do gráfico. Esse processo é repetido
no máximo (V-1) vezes, onde V é o número de vértices no gráfico.

O número de iterações necessárias para descobrir o caminho mais curto da origem para todos os outros vértices depende da ordem
que selecionamos para relaxar as arestas.

Vejamos um exemplo:

Aqui, o vértice de origem é 1. Descobriremos a menor distância entre a origem e todos os outros vértices.
Podemos ver claramente que, para atingir o vértice 4, no pior caso, serão necessárias (V-1) arestas. Agora, dependendo da ordem em
que as arestas são descobertas, pode levar (V-1) vezes para descobrir o vértice 4. Não entendeu? Vamos usar Bellman-Ford

Notas de algoritmos do [Link] para profissionais 118


Machine Translated by Google

algoritmo para descobrir o caminho mais curto aqui:

Vamos usar esta sequência:

+--------+--------+--------+--------+
| Série | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borda | 3->4 | 2->3 | 1->2 |
+--------+--------+--------+--------+

Para nossa primeira iteração:

1. d[3] + cost[3][4] = infinito. Não mudará nada.


2. d[2] + cost[2][3] = infinito. Não mudará nada.
3. d[1] + custo[1][2] = 2 < d[2]. d[2] = 2. pai[2] = 1.

Podemos ver que nosso processo de relaxamento mudou apenas d[2]. Nosso gráfico ficará assim:

Segunda iteração:

1. d[3] + cost[3][4] = infinito. Não mudará nada.


2. d[2] + custo[2][3] = 5 < d[3]. d[3] = 5. pai[3] = 2.
3. Não será alterado.

Desta vez o processo de relaxamento mudou d[3]. Nosso gráfico ficará assim:

Terceira iteração:

1. d[3] + custo[3][4] = 7 < d[4]. d[4] = 7. pai[4] = 3.


2. Não será alterado.
3. Não será alterado.

Nossa terceira iteração finalmente descobriu o caminho mais curto de 1 para 4. Nosso gráfico ficará assim:

Então, levou 3 iterações para descobrir o caminho mais curto. Depois dessa, não importa quantas vezes relaxamos as bordas,

Notas de algoritmos do [Link] para profissionais 119


Machine Translated by Google

os valores em d[] permanecerão os mesmos. Agora, se considerarmos outra sequência:

+--------+--------+--------+--------+
| Série | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borda | 1->2 | 2->3 | 3->4 |
+--------+--------+--------+--------+

Teríamos:

1. d[1] + custo[1][2] = 2 < d[2]. d[2] = 2.


2. d[2] + custo[2][3] = 5 < d[3]. d[3] = 5.
3. d[3] + custo[3][4] = 7 < d[4]. d[4] = 5.

Nossa primeira iteração encontrou o caminho mais curto da fonte para todos os outros nós. Outra sequência 1->2,
3->4, 2->3 é possível, o que nos dará o caminho mais curto após 2 iterações. Podemos chegar à decisão de que, não importa
como organizamos a sequência, não levará mais de 3 iterações para descobrir o caminho mais curto da fonte neste
exemplo.

Podemos concluir que, para o melhor caso, levará 1 iteração para descobrir o caminho mais curto da fonte. Para o pior
nesse caso, serão necessárias (V-1) iterações, e é por isso que repetimos o processo de relaxamento (V-1) vezes.

Notas de algoritmos do [Link] para profissionais 120


Machine Translated by Google

Capítulo 21: Algoritmo de Linha


O desenho de linha é realizado calculando posições intermediárias ao longo do caminho da linha entre duas posições de ponto final
especificadas. Um dispositivo de saída é então direcionado para preencher essas posições entre os pontos finais.

Seção 21.1: Algoritmo de desenho de linha de Bresenham


Teoria de fundo: O Algoritmo de Desenho de Linha de Bresenham é um algoritmo de geração de linha raster eficiente e preciso desenvolvido
por Bresenham. Ele envolve apenas cálculo de inteiros, por isso é preciso e rápido. Ele também pode ser estendido para exibir círculos e outras
curvas.

No algoritmo de desenho de linha de Bresenham:

Para Inclinação |m|<1:


Qualquer valor de x é aumentado

OU tanto x quanto y são aumentados usando o parâmetro de decisão.

Para Inclinação |m|>1:


O valor de y é aumentado OU x e y
são aumentados usando o parâmetro de decisão.

Algoritmo para declive |m|<1:

1. Insira dois pontos finais (x1,y1) e (x2,y2) da linha.

2. Trace o primeiro ponto (x1,y1).

3. Calcular

Delx =| x2 – x1 |
Atraso = | y2 – y1 |

4. Obtenha o parâmetro de decisão inicial como P = 2 *


dely – delx

5. Para I = 0 para delx no passo de 1

Se p < 0 então
X1 = x1 + 1

Pote(x1,y1)
P = p+ 2dely

Outro

X1 = x1 + 1

Y1 = y1 + 1
Gráfico(x1,y1)

P = p + 2dely – 2 * delx

Fim se

Fim para

6. FIM

Código fonte:

Notas de algoritmos do [Link] para profissionais 121


Machine Translated by Google

/ * Programa AC para implementar o algoritmo de desenho de linha de Bresenham para |m|<1 */ #include<stdio.h>

#incluir<conio.h>
#incluir<gráficos.h>
#incluir<matemática.h>

int main() { int

gdriver=DETECTAR,gmode; int
x1,y1,x2,y2,delx,dely,p,i; initgraph(&gdriver,&gmode,"c:
\\TC\\BGI");

printf("Digite os pontos iniciais: "); scanf("%d",&x1);


scanf("%d",&y1);
printf("Digite os pontos
finais: "); scanf("%d",&x2); scanf("%d",&y2);

putpixel(x1,y1,VERMELHO);

delx=fabs(x2-x1);
dely=fabs(y2-y1); p=(2*dely)-
delx; para(i=0;i<delx;i++)
{ se(p<0) { x1=x1+1;

putpixel(x1,y1,RED); p=p+(2*dely); }
senão { x1=x1+1;

y1=y1+1;

putpixel(x1,y1,RED); p=p+(2*dely)-
(2*delx); } } getch(); closegraph();

retornar 0; }

Algoritmo para declive |m|>1:

1. Insira dois pontos finais (x1,y1) e (x2,y2) da linha.


2. Trace o primeiro ponto (x1,y1).
3. Calcular

Delx =| x2 – x1 |
Dely = | y2 – y1 | 4.
Obtenha o parâmetro de decisão inicial como P =
2 * delx – dely 5.
Para I = 0 para dely no passo de 1

Se p < 0
então y1 = y1 + 1
Pote(x1,y1)

Notas de algoritmos do [Link] para profissionais 122


Machine Translated by Google

P = p+ 2delx

Outro

X1 = x1 + 1

Y1 = y1 + 1
Gráfico(x1,y1)

P = p + 2delx – 2 * dely

Fim se

Fim para

6. FIM

Código fonte:

/ * Programa AC para implementar o algoritmo de desenho de linha de Bresenham para |m|>1 */ #include<stdio.h>
#include<conio.h>

#include<graphics.h>
#include<math.h> int
main() { int

gdriver=DETECT,gmode; int
x1,y1,x2,y2,delx,dely,p,i; initgraph(&gdriver,&gmode,"c:
\\TC\\BGI"); printf("Digite os pontos iniciais: "); scanf("%d",&x1);
scanf("%d",&y1); printf("Digite os pontos finais: ");
scanf("%d",&x2);
scanf("%d",&y2);
putpixel(x1,y1,RED); delx=fabs(x2-x1); dely=fabs(y2-
y1); p=(2*delx)-dely;
for(i=0;i<delx;i++){ if(p<0)
{ y1=y1+1; putpixel(x1,y1,RED);
p=p+(2*delx); } senão
{ x1=x1+1; y1=y1+1;
putpixel(x1,y1,RED);
p=p+(2*delx)-(2*dely); } } getch();

closegraph();
return 0; }

Notas de algoritmos do [Link] para profissionais 123


Machine Translated by Google

Capítulo 22: Algoritmo Floyd-Warshall


Seção 22.1: Algoritmo do caminho mais curto para todos os pares

De Floyd-Warshall O algoritmo é para encontrar os caminhos mais curtos em um grafo ponderado com pesos de aresta positivos ou negativos.
Uma única execução do algoritmo encontrará os comprimentos (pesos somados) dos caminhos mais curtos entre todos os pares de
vértices. Com uma pequena variação, ele pode imprimir o caminho mais curto e pode detectar ciclos negativos em um gráfico.
Floyd-Warshall é um algoritmo de Programação Dinâmica.

Vamos dar uma olhada em um exemplo. Vamos aplicar o algoritmo de Floyd-Warshall neste gráfico:

A primeira coisa que fazemos é pegar duas matrizes 2D. Essas são matrizes de adjacência. O tamanho das matrizes será
o número total de vértices. Para nosso gráfico, tomaremos matrizes 4 * 4. A Matriz de Distância armazenará o
distância mínima encontrada até agora entre dois vértices. A princípio, para as arestas, se houver uma aresta entre uv e o
distância/peso é w, armazenaremos: distância[u][v] = w. Para todas as arestas que não existem, colocaremos infinito.
A Matriz de Caminho é para regenerar o caminho de distância mínima entre dois vértices. Então, inicialmente, se houver um caminho
entre u e v, vamos colocar path[u][v] = u. Isso significa que a melhor maneira de chegar ao vértice-v a partir do vértice-u
é usar a aresta que conecta v com u. Se não houver caminho entre dois vértices, vamos colocar N lá
indicando que não há caminho disponível agora. As duas tabelas para nosso gráfico ficarão assim:

+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| |1|2|3|4| | |1| 2 |3|4|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 1 | 0 | 3 | 6 | 15 | |1|E|1|1|1|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 2 | inf | 0 | -2 | inf | |2|E|E|2|E|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 3 | inf | inf | 0 | 2 | |3|E|E|E|3|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 4 | 1 | inf | inf | 0 | |4|4|E|E|E|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
distância caminho

Como não há loop, as diagonais são definidas como N. E a distância do próprio vértice é 0.

Para aplicar o algoritmo Floyd-Warshall, vamos selecionar um vértice médio k. Então, para cada vértice i, vamos
verifique se podemos ir de i para k e depois k para j, onde j é outro vértice e minimizar o custo de ir de i para j. Se
a distância atual[i][j] é maior que distância[i][k] + distância[k][j], vamos colocar distância[i][j] igual a
a soma dessas duas distâncias. E o path[i][j] será definido como path[k][j], pois é melhor ir de i para k,

Notas de algoritmos do [Link] para profissionais 124


Machine Translated by Google

e então k para j. Todos os vértices serão selecionados como k. Teremos 3 loops aninhados: para k indo de 1 a 4, i indo de
1 a 4 e j indo de 1 a 4. Vamos verificar:

se distância[i][j] > distância[i][k] + distância[k][j]


distância[i][j] := distância[i][k] + distância[k][j]
caminho[i][j] := caminho[k][j]
fim se

Então, o que estamos basicamente verificando é, para cada par de vértices, obtemos uma distância menor ao passar por outro
vértice? O número total de operações para o nosso gráfico será 4 * 4 * 4 = 64. Isso significa que faremos esta verificação
64 vezes. Vamos dar uma olhada em algumas delas:

Quando k = 1, i = 2 e j = 3, distância[i][j] é -2, que não é maior que distância[i][k] + distância[k][j] = -2 + 0 = -2.
Portanto, permanecerá inalterado. Novamente, quando k = 1, i = 4 e j = 2, distância [i] [j] = infinito, que é maior que
distância[i][k] + distância[k][j] = 1 + 3 = 4. Então colocamos distância[i][j] = 4, e colocamos caminho[i][j] = caminho[k][j] = 1. O que
isto significa que, para ir do vértice-4 ao vértice-2, o caminho 4->1->2 é mais curto que o caminho existente. É assim que nós
preencha ambas as matrizes. O cálculo para cada etapa é mostrado aqui. Após fazer as alterações necessárias, nossas matrizes
ficará assim:

+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| |1|2|3|4| | |1| 2 |3|4|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|1|0|3|1|3| |1|E|1|2|3|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 2 | 1 | 0 | -2 | 0 | |2|4|E|2|3|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|3|3|6|0|2| |3|4|1|N|3|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|4|1|4|2|0| |4|4|1|2|E|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
distância caminho

Esta é a nossa matriz de distância mais curta. Por exemplo, a distância mais curta de 1 a 4 é 3 e a distância mais curta
entre 4 e 3 é 2. Nosso pseudocódigo será:

Procedimento Floyd-Warshall (Gráfico):


para k de 1 a V // V denota o número de vértices
para i de 1 a V
para j de 1 a V
se distância[i][j] > distância[i][k] + distância[k][j]
distância[i][j] := distância[i][k] + distância[k][j]
caminho[i][j] := caminho[k][j]
fim se
fim para
fim para
fim para

Imprimindo o caminho:

Para imprimir o caminho, verificaremos a matriz Path . Para imprimir o caminho de u a v, começaremos em path[u][v]. Definiremos
continue mudando v = path[u][v] até encontrarmos path[u][v] = u e empilhar todos os valores de path[u][v] em uma pilha. Depois
encontrando u, imprimiremos u e começaremos a retirar itens da pilha e imprimi-los. Isso funciona porque a matriz de caminho
armazena o valor do vértice que compartilha o caminho mais curto para v de qualquer outro nó. O pseudocódigo será:

Procedimento PrintPath(origem, destino):

Notas de algoritmos do [Link] para profissionais 125


Machine Translated by Google

s = Pilha()
[Link](destino) enquanto
Path[fonte][destino] não é igual a fonte [Link](Path[fonte][destino]) destino := Path[fonte]
[destino] fim enquanto

imprimir -> fonte


enquanto S não estiver vazio
imprimir -> [Link] fim
enquanto

Encontrando o ciclo de borda negativa:

Para descobrir se há um ciclo de aresta negativo, precisamos verificar a diagonal principal da matriz de distância . Se qualquer valor na diagonal
for negativo, isso significa que há um ciclo negativo no gráfico.

Complexidade:

A complexidade do algoritmo Floyd-Warshall é O(V³) e a complexidade do espaço é: O(V²).

Notas de algoritmos do [Link] para profissionais 126


Machine Translated by Google

Capítulo 23: Algoritmo do Número Catalão


Seção 23.1: Informações básicas do algoritmo numérico catalão
O algoritmo de números catalães é um algoritmo de programação dinâmica.

Na matemática combinatória, os números catalães formam uma sequência de números naturais que ocorrem em vários problemas de
contagem, frequentemente envolvendo objetos definidos recursivamente. Os números de Catalan em inteiros não negativos n são um conjunto de
números que surgem em problemas de enumeração de árvores do tipo, 'De quantas maneiras um n-gono regular pode ser dividido em n-2
triângulos se diferentes orientações forem contadas separadamente?'

Aplicação do Algoritmo de Números Catalão:

1. O número de maneiras de empilhar moedas em uma fileira inferior que consiste em n moedas consecutivas em um plano, de modo que
nenhuma moeda possa ser colocada nos dois lados das moedas inferiores e cada moeda adicional deve estar acima de duas outras
moedas, é o enésimo número catalão.
2. O número de maneiras de agrupar uma sequência de n pares de parênteses, de modo que cada parêntese aberto tenha um
correspondente aos parênteses fechados, é o enésimo número catalão.
3. O número de maneiras de cortar um polígono convexo de n+2 lados em um plano em triângulos conectando vértices com linhas retas e não
interseccionais é o n-ésimo número catalão. Esta é a aplicação na qual Euler estava interessado.

Usando numeração baseada em zero, o enésimo número catalão é dado diretamente em termos de coeficientes binomiais pela seguinte
equação.

Exemplo de número catalão:

Aqui o valor de n = 4. (Melhor exemplo - Da Wikipedia)

Espaço Auxiliar: O(n)

Notas de algoritmos do [Link] para profissionais 127


Machine Translated by Google

Complexidade de tempo: O(n^2)

Notas de algoritmos do [Link] para profissionais 128


Machine Translated by Google

Capítulo 24: Algoritmos multithread


Exemplos de alguns algoritmos multithread.

Seção 24.1: Multiplicação de matriz quadrada multithread


multiplicar-matriz-quadrada-paralela(A, B) n = [Link]

C = Matrix(n,n) //cria uma nova matriz n*n paralela para i = 1 a n

paralelo para j = 1 a n
C[i][j] = 0 para k =
1 para n
C[i][j] = C[i][j] + A[i][k]*B[k][j]
retornar C

Seção 24.2: Matriz de multiplicação vetor multithread


matrix-vector(A,x) n = [Link]
y = Vector(n) //cria
um novo vetor de comprimento n paralelo para i = 1 a n y[i] = 0 paralelo para
i = 1 a n para j = 1 a n y[i] = y[i] + A[i]
[j]*x[j] return
y

Seção 24.3: merge-sort multithread


A é uma matriz e os índices p e q da matriz, como você vai classificar a submatriz A[p..r]. B é uma submatriz que será preenchida pela
classificação.

Uma chamada para p-merge-sort(A,p,r,B,s) classifica elementos de A[p..r] e os coloca em B[s..s+rp].

p-merge-sort(A,p,r,B,s) n = r-p+1 se
n==1

B[s] = A[p] senão

T = new Array(n) //cria um novo array T de tamanho n q = floor((p+r)/2)) q_prime


= q-p+1 spawn p-merge-
sort(A,p,q,T,1) p-merge-
sort(A,q+1,r,T,q_prime+1)

sincronizar p-merge(T,1,q_prime,q_prime+1,n,B,s)

Aqui está a função auxiliar que realiza a mesclagem em paralelo. p-merge


assume que os dois subarrays a serem mesclados estão no mesmo array, mas não assume que eles são adjacentes no array. É por
isso que precisamos de p1,r1,p2,r2.

p-merge(T,p1,r1,p2,r2,A,p3) n1 = r1-p1+1 n2
= r2-p2+1 se n1<n2

// verifique se n1>=n2

Notas de algoritmos do [Link] para profissionais 129


Machine Translated by Google

permutar p1 e p2 permutar r1 e
r2 permutar n1 e n2 se n1==0 //
ambos vazios?

retornar
senão

q1 = andar((p1+r1)/2) q2 = busca-
dicotômica(T[q1],T,p2,r2) q3 = p3 + (q1-p1) + (q2-p2)

A[q3] = T[q1] gerar p-


merge(T,p1,q1-1,p2,q2-1,A,p3) p-merge(T,q1+1,r1,q2,r2,A,q3+1)
sincronizar

E aqui está a função auxiliar dicotomic-search.

x é a chave a ser procurada na submatriz T[p..r].

pesquisa-dicotômica(x,T,p,r) inf = p sup =


max(p,r+1)
enquanto inf<sup metade =
chão((inf+sup)/2) se
x<=T[metade] sup = metade senão inf =
metade+1

retornar sup

Notas de algoritmos do [Link] para profissionais 130


Machine Translated by Google

Capítulo 25: Knuth Morris Pratt (KMP)


Algoritmo
O KMP é um algoritmo de correspondência de padrões que busca ocorrências de uma "palavra" W dentro de uma "cadeia de texto" principal S ,
empregando a observação de que, quando ocorre uma incompatibilidade, temos informações suficientes para determinar onde a próxima correspondência
pode começar. Aproveitamos essas informações para evitar a correspondência de caracteres que sabemos que corresponderão de qualquer forma.
A pior complexidade do caso para pesquisar um padrão se reduz a O(n).

Seção 25.1: Exemplo de KMP


Algoritmo

Este algoritmo é um processo de duas etapas. Primeiro, criamos uma matriz auxiliar lps[] e então usamos essa matriz para pesquisar o padrão.

Pré-processamento :

1. Pré-processamos o padrão e criamos um array auxiliar lps[] que é usado para pular caracteres enquanto
correspondência.

2. Aqui lps[] indica o prefixo próprio mais longo que também é sufixo. Um prefixo próprio é um prefixo no qual a string inteira não está incluída. Por
“ “
exemplo, os prefixos da string ABC são “AB”. Os sufixos da string ”, “A”, “AB” e “ABC”. Os prefixos próprios são ”, “A” e

são ”, “C”, “BC” e “ABC”.

Procurando

1. Continuamos combinando os caracteres txt[i] e pat[j] e continuamos incrementando i e j enquanto pat[j] e txt[i] continuam
correspondência.

2. Quando vemos uma incompatibilidade, sabemos que os caracteres pat[0..j-1] correspondem a txt[i-j+1…i-1]. Também sabemos que
lps[j-1] é a contagem de caracteres de pat[0…j-1] que são prefixo e sufixo próprios. Disto podemos concluir que não precisamos corresponder
esses caracteres lps[j-1] com txt[ij…i-1] porque sabemos que esses caracteres corresponderão de qualquer maneira.

Implementação em Java

classe pública KMP {

public static void main(String[] args) { // TODO Stub


de método gerado automaticamente String
str = "abcabdabc"; String pattern
= "abc"; KMP obj = new KMP();

[Link]([Link]([Link](), [Link]()));
}

público int[] computeLPS(char[] str){ int lps[] =


novo int[[Link]];

lps[0] = 0; int
j = 0; para(int
i =1;i<[Link];i++){ se(str[j] == str[i])
{ lps[i] = j+1; j++;

Notas de algoritmos do [Link] para profissionais 131


Machine Translated by Google

i++; }

senão{ se(j!=0){ j
= lps[j-1]; }

senão{ lps[i] = j+1; i++;

}
}

retornar lps;
}

público booleano patternExistKMP(char[] texto,char[] pat){


int[] lps = computeLPS(pat); int i=0,j=0;

enquanto(i<[Link] && j<[Link])


{ se(texto[i] == pat[j]){ i++; j++; }

senão{ se(j!=0){ j
= lps[j-1]; }senão{ i+
+;

}
}
}

if(j==[Link]) retorna
verdadeiro;
retorna falso;
}

Notas de algoritmos do [Link] para profissionais 132


Machine Translated by Google

Capítulo 26: Editar Distância Dinâmica


Algoritmo
Seção 26.1: Edições mínimas necessárias para converter a string 1 para
corda 2
A declaração do problema é como se nos fossem dadas duas strings str1 e str2, então qual o número mínimo de
operações podem ser executadas no str1 que é convertido para str2. As operações podem ser:

1. Insira
2. Remove

3. Substituir

Por exemplo

Entrada: str1 = "geek", str2 = "gesek"


Saída: 1
Precisamos apenas inserir s na primeira string

Entrada: str1 = "marcha", str2 = "carrinho"


Saída: 3
Precisamos substituir m por c e remover o caractere c e então substituir h por t

Para resolver este problema, usaremos uma matriz 2D dp[n+1][m+1] onde n é o comprimento da primeira string e m é o
comprimento da segunda string. Para nosso exemplo, se str1 for azcef e str2 for abcdef, então nossa matriz será dp[6][7]e
nossa resposta final será armazenada em dp[5][6].

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(a)| 1 | | | | | | |
+---+---+---+---+---+---+---+
(z)| 2 | | | | | | |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Para dp[1][1] temos que verificar o que podemos fazer para converter a em a. Será 0. Para dp[1][2] temos que verificar o que podemos fazer
fazemos para converter a em ab. Será 1 porque temos que inserir b. Então, após a primeira iteração, nossa matriz ficará assim

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | | | | | | |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |

Notas de algoritmos do [Link] para profissionais 133


Machine Translated by Google
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Para iteração 2

Para dp[2][1] temos que verificar que para converter az para a precisamos remover z, portanto dp[2][1] será 1. Similarmente para
dp[2][2] precisamos substituir z por b, portanto dp[2][2] será 1. Então, após a 2ª iteração, nosso array dp[] ficará assim.

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | 1 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Então nossa fórmula ficará assim

se os caracteres forem os mesmos


dp[i][j] = dp[i-1][j-1];
outro
dp[i][j] = 1 + Min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

Após a última iteração, nossa matriz dp[] ficará assim:

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | 1 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(c)| 3 | 2 | 2 | 1 | 2 | 3 | 4 |
+---+---+---+---+---+---+---+
(e)| 4 | 3 | 3 | 2 | 2 | 2 | 3 |
+---+---+---+---+---+---+---+
(f)| 5 | 4 | 4 | 2 | 3 | 3 | 3 |
+---+---+---+---+---+---+---+

Implementação em Java

público int getMinConversions(String str1, String str2){


int dp[][] = novo int[[Link]()+1][[Link]()+1];
para(int i=0;i<=[Link]();i++){
para(int j=0;j<=[Link]();j++){
se(i==0)

Notas de algoritmos do [Link] para profissionais 134


Machine Translated by Google

dp[i][j] = j; senão
se(j==0) dp[i][j]
= i; senão
se([Link](i-1) == [Link](j-1)) dp[i][j] = dp[i-1][j-1];
senão{ dp[i][j] = 1 +

[Link](dp[i-1][j], [Link](dp[i][j-1], dp[i-1][j-1]));


}
}

} retornar dp[[Link]()][[Link]()];
}

Complexidade de tempo

O(n^2)

Notas de algoritmos do [Link] para profissionais 135


Machine Translated by Google

Capítulo 27: Algoritmos online


Teoria

Definição 1: Um problema de otimização ÿ consiste em um conjunto de instâncias ÿÿ. Para cada instância ÿÿÿÿ há um conjunto ÿÿ de
soluções e uma função objetivo fÿ : ÿÿ ÿ ÿÿ0 que atribui um valor real positivo a cada solução.
Dizemos que OPT(ÿ) é o valor de uma solução ótima, A(ÿ) é a solução de um Algoritmo A para o problema ÿ e wA(ÿ)=fÿ(A(ÿ)) seu valor.

Definição 2: Um algoritmo online A para um problema de minimização ÿ tem uma razão competitiva de r ÿ 1 se houver uma constante
ÿÿÿ com

wA(ÿ) = fÿ(A(ÿ)) ÿ r ÿ OPT(&sigma) + ÿ

para todas as instâncias ÿÿÿÿ. A é chamado de algoritmo online r-competitivo . É mesmo

wA(ÿ) ÿ r ÿ OPT(&sigma)

para todas as instâncias ÿÿÿÿ então A é chamado de algoritmo online estritamente r-competitivo .

Proposição 1.3: LRU e FWF são algoritmos de marcação.

Prova: No início de cada fase (exceto na primeira), o FWF tem uma falta de cache e limpa o cache. Isso significa que temos k páginas vazias.
Em cada fase, há um máximo de k páginas diferentes solicitadas, então não haverá despejo durante a fase. Então, o FWF é um algoritmo
de marcação.
Vamos supor que LRU não seja um algoritmo de marcação. Então há uma instância ÿ onde LRU marcou uma página x na fase i despejada.
Seja ÿt a solicitação na fase i onde x é despejado. Como x é marcado, tem que haver uma solicitação anterior ÿt* para x na mesma fase, então
t* < t. Depois de t* x é a página mais nova do cache, então para ser despejado em t a sequência ÿt*+1,...,ÿt tem que solicitar pelo menos k de x
páginas diferentes. Isso implica que a fase i solicitou pelo menos k+1 páginas diferentes, o que é contraditório à definição da fase. Então
LRU tem que ser um algoritmo de marcação.

Proposição 1.4: Todo algoritmo de marcação é estritamente k-competitivo.

Prova: Seja ÿ uma instância para o problema de paginação e l o número de fases para ÿ. Se l = 1, então todo algoritmo de marcação é ótimo e
o algoritmo offline ótimo não pode ser melhor.
Assumimos que l ÿ 2. O custo de cada algoritmo de marcação, por exemplo ÿ, é limitado acima por l ÿ k porque em cada fase um algoritmo de
marcação não pode remover mais de k páginas sem remover uma página marcada.
Agora tentamos mostrar que o algoritmo offline ótimo despeja pelo menos k+l-2 páginas para ÿ, k na primeira fase e pelo menos uma para
cada fase seguinte, exceto a última. Para prova, vamos definir l-2 subsequências disjuntas de ÿ.
A subsequência i ÿ {1,...,l-2} começa na segunda posição da fase i+1 e termina na primeira posição da fase i+2.
Seja x a primeira página da fase i+1. No início da subsequência i há uma página x e no máximo k-1 páginas diferentes no cache do
algoritmo offline ótimo. Na subsequência i há k requisições de página diferentes de x, então o algoritmo offline ótimo tem que despejar pelo
menos uma página para cada subsequência. Como no início da fase 1 o cache ainda está vazio, o algoritmo offline ótimo causa k remoções
durante a primeira fase. Isso mostra que

wA(ÿ) ÿ lÿk ÿ (k+l-2)k ÿ OPT(ÿ) ÿ k

Corolário 1.5: LRU e FWF são estritamente k-competitivos.

Notas de algoritmos do [Link] para profissionais 136


Machine Translated by Google

Não existe uma constante r para a qual um algoritmo online A é r-competitivo, chamamos A de não competitivo.

Proposição 1.6: LFU e LIFO não são competitivos.

Prova: Seja l ÿ 2 uma constante, k ÿ 2 o tamanho do cache. As diferentes páginas do cache são numeradas 1,...,k+1. Observamos a
seguinte sequência:

A primeira página 1 é solicitada l vezes que a página 2 e assim por diante. No final, há (l-1) solicitações alternadas para a página k e k+1.

LFU e LIFO preenchem seu cache com páginas 1-k. Quando a página k+1 é solicitada, a página k é despejada e vice-versa. Isso significa
que cada solicitação da subsequência (k,k+1)l-1 despeja uma página. Além disso, há k-1 faltas de cache para o primeiro uso das páginas 1-
(k-1). Então LFU e LIFO despejam exatamente k-1+2(l-1) páginas.
Agora devemos mostrar que para cada constante ÿÿÿ e cada constante r ÿ 1 existe um l tal que

que é igual a

Para satisfazer essa desigualdade, você só precisa escolher l suficientemente grande. Então LFU e LIFO não são competitivos.

Proposição 1.7: Não existe algoritmo online determinístico r-competitivo para paginação com r < k.

Fontes
Material Básico

1. Script Online Algorithms (alemão), Heiko Roeglin, Universidade de Bonn 2.


Algoritmo de substituição de página

Leitura Adicional

1. Computação Online e Análise Competitiva por Allan Borodin e Ran El-Yaniv

Código fonte

1. Código fonte para cache offline 2.


Código fonte para jogo adversário

Seção 27.1: Paginação (Cache Online)


Prefácio

Em vez de começar com uma definição formal, o objetivo é abordar esses tópicos por meio de uma série de exemplos, introduzindo
definições ao longo do caminho. A seção de comentários Teoria consistirá de todas as definições, teoremas e proposições para dar a você
todas as informações para procurar aspectos específicos mais rapidamente.

Notas de algoritmos do [Link] para profissionais 137


Machine Translated by Google

As fontes da seção de comentários consistem no material base usado para este tópico e informações adicionais para leitura posterior. Além disso,
você encontrará os códigos-fonte completos para os exemplos lá. Por favor, preste atenção que para tornar o código-fonte para os exemplos mais
legível e mais curto, ele se abstém de coisas como tratamento de erros, etc. Ele também passa alguns recursos de linguagem específicos
que obscureceriam a clareza do exemplo, como uso extensivo de bibliotecas avançadas, etc.

Paginação

O problema de paginação surge da limitação de espaço finito. Vamos supor que nosso cache C tenha k páginas. Agora queremos processar uma
sequência de m solicitações de página que devem ter sido colocadas no cache antes de serem processadas. Claro que se m<=k então colocamos
todos os elementos no cache e funcionará, mas geralmente é m>>k.

Dizemos que uma solicitação é um cache hit, quando a página já está no cache, caso contrário, é chamado de cache miss. Nesse caso,
devemos trazer a página solicitada para o cache e despejar outra, assumindo que o cache esteja cheio. O objetivo é um cronograma de
despejo que minimize o número de despejos.

Existem inúmeras estratégias para esse problema, vejamos algumas:

1. Primeiro a entrar, primeiro a sair (FIFO): A página mais antiga é


removida. 2. Último a entrar, primeiro a sair (LIFO): A página mais
nova é removida. 3. Menos usada recentemente (LRU): Remove a página cujo acesso mais recente
foi o mais antigo. 4. Menos usada com frequência (LFU): Remove a página que foi solicitada com
menos frequência. 5. Maior distância de avanço (LFD): Remove a página no cache que não é solicitada até o momento mais distante no futuro.
6. Flush when full (FWF): limpe o cache completamente assim que ocorrer uma falha de cache

Há duas maneiras de abordar esse problema:

1. offline: a sequência de solicitações de página é conhecida com antecedência 2.


online: a sequência de solicitações de página não é conhecida com antecedência

Abordagem Offline

Para a primeira abordagem, veja o tópico Applications of Greedy Technique. Seu terceiro Example Offline Caching considera as cinco
primeiras estratégias acima e fornece um bom ponto de entrada para o seguinte.

O programa de exemplo foi estendido com a estratégia FWF :

classe FWF : estratégia pública { público:

FWF() : Estratégia("FWF") { }

int apply(int requestIndex) substituir {

para(int i=0; i<tamanhodocache; ++i) {

se(cache[i] == solicitação[índicesolicitação]) retornar


i;

// após a primeira página vazia todas as outras devem estar vazias else
if(cache[i] == emptyPage) return i;

// sem páginas livres

Notas de algoritmos do [Link] para profissionais 138


Machine Translated by Google

retornar 0;
}

void update(int cachePos, int requestIndex, bool cacheMiss) substituir


{

// nenhuma página livre -> falta -> limpar cache


if(cacheMiss && cachePos == 0)
{
para(int i = 1; i < tamanho do cache; ++i)
cache[i] = PáginaVazia;
}
}
};

O código-fonte completo está disponível aqui. Se reutilizarmos o exemplo do tópico, obteremos a seguinte saída:

Estratégia: FWF

Cache inicial: (a,b,c)

Solicitar cache 0 cache 1 cache 2 cache falta


um um b c
um um b c
e X X x
e dd e X
b e e b
b e e b
um um X X x
c um c X
um c e
fd X X x
e dd e X
um e um

X X x
Facebook b X
e dfff b e
c c X X x

Total de falhas de cache: 5

Embora o LFD seja ótimo, o FWF tem menos falhas de cache. Mas o objetivo principal era minimizar o número de
despejos e para FWF cinco erros significam 15 despejos, o que a torna a pior escolha para este exemplo.

Abordagem Online

Agora queremos abordar o problema online de paginação. Mas primeiro precisamos entender como fazê-lo.
Obviamente, um algoritmo online não pode ser melhor do que o algoritmo offline ideal. Mas quão pior ele é? Nós
precisa de definições formais para responder a essa pergunta:

Definição 1.1: Um problema de otimização ÿ consiste em um conjunto de instâncias ÿÿ. Para cada instância ÿÿÿÿ existe um
conjunto ÿÿ de soluções e uma função objetivo fÿ : ÿÿ ÿ ÿÿ0 que atribui um valor real positivo a cada solução.
Dizemos que OPT(ÿ) é o valor de uma solução ótima, A(ÿ) é a solução de um Algoritmo A para o problema ÿ e
wA(ÿ)=fÿ(A(ÿ)) seu valor.

Definição 1.2: Um algoritmo online A para um problema de minimização ÿ tem uma razão competitiva de r ÿ 1 se houver um
constante ÿÿÿ com

Notas de algoritmos do [Link] para profissionais 139


Machine Translated by Google

wA(ÿ) = fÿ(A(ÿ)) ÿ r ÿ OPT(ÿ) + ÿ

para todas as instâncias ÿÿÿÿ. A é chamado de algoritmo online r-competitivo . É mesmo

wA(ÿ) ÿ r ÿ OPT(ÿ)

para todas as instâncias ÿÿÿÿ então A é chamado de algoritmo online estritamente r-competitivo .

Então a questão é quão competitivo é nosso algoritmo online comparado a um algoritmo offline ótimo. Em seu famoso livro Allan
Borodin e Ran El-Yaniv usaram outro cenário para descrever a situação de paginação online:

Há um adversário maligno que conhece seu algoritmo e o algoritmo offline ideal. Em cada etapa, ele tenta solicitar uma página que seja
pior para você e, simultaneamente, melhor para o algoritmo offline. O fator competitivo do seu algoritmo é o fator de quão mal seu algoritmo
se saiu contra o algoritmo offline ideal do adversário. Se você quiser tentar ser o adversário, pode tentar o Adversary Game (tente vencer
as estratégias de paginação).

Algoritmos de Marcação

Em vez de analisar cada algoritmo separadamente, vamos analisar uma família especial de algoritmos on-line para o problema de
paginação, chamados algoritmos de marcação.

Seja ÿ=(ÿ1,...,ÿp) uma instância para nosso problema e k nosso tamanho de cache, então ÿ pode ser dividido em fases:

A fase 1 é a subsequência máxima de ÿ desde o início até que o máximo de k páginas diferentes sejam solicitadas
A fase i ÿ 2 é a subsequência máxima de ÿ do final da fase i-1 até que o máximo de k páginas diferentes sejam solicitadas

Por exemplo com k = 3:

Um algoritmo de marcação (implícita ou explicitamente) mantém se uma página está marcada ou não. No início de cada fase, todas as páginas
estão desmarcadas. Se uma página for solicitada durante uma fase, ela será marcada. Um algoritmo é um algoritmo de marcação se ele
nunca remover uma página marcada do cache. Isso significa que as páginas que são usadas durante uma fase não serão removidas.

Proposição 1.3: LRU e FWF são algoritmos de marcação.

Prova: No início de cada fase (exceto na primeira), o FWF tem uma falta de cache e limpa o cache. Isso significa que temos k páginas
vazias. Em cada fase, há um máximo de k páginas diferentes solicitadas, então não haverá despejo durante a fase. Então, o FWF é
um algoritmo de marcação.
Vamos supor que LRU não seja um algoritmo de marcação. Então há uma instância ÿ onde LRU marcou uma página x na fase i
despejada. Seja ÿt a solicitação na fase i onde x é despejado. Como x é marcado, tem que haver uma solicitação anterior ÿt* para x na mesma
fase, então t* < t. Depois de t* x é a página mais nova do cache, então para ser despejado em t a sequência ÿt*+1,...,ÿt tem que solicitar pelo
menos k de x páginas diferentes. Isso implica que a fase i solicitou pelo menos k+1 páginas diferentes, o que é contraditório à definição
da fase. Então LRU tem que ser um algoritmo de marcação.

Notas de algoritmos do [Link] para profissionais 140


Machine Translated by Google

Proposição 1.4: Todo algoritmo de marcação é estritamente k-competitivo.

Prova: Seja ÿ uma instância para o problema de paginação e l o número de fases para ÿ. Se l = 1, então todo algoritmo de marcação é ótimo e
o algoritmo offline ótimo não pode ser melhor.
Assumimos que l ÿ 2. O custo de cada algoritmo de marcação, por exemplo, ÿ é limitado acima por l ÿ k porque em cada fase um algoritmo de
marcação não pode remover mais de k páginas sem remover uma página marcada.
Agora tentamos mostrar que o algoritmo offline ótimo despeja pelo menos k+l-2 páginas para ÿ, k na primeira fase e pelo menos uma para
cada fase seguinte, exceto a última. Para prova, vamos definir l-2 subsequências disjuntas de ÿ.
A subsequência i ÿ {1,...,l-2} começa na segunda posição da fase i+1 e termina na primeira posição da fase i+2.
Seja x a primeira página da fase i+1. No início da subsequência i há uma página x e no máximo k-1 páginas diferentes no cache do algoritmo
offline ótimo. Na subsequência i há k requisições de página diferentes de x, então o algoritmo offline ótimo tem que despejar pelo menos uma
página para cada subsequência. Como no início da fase 1 o cache ainda está vazio, o algoritmo offline ótimo causa k remoções durante a
primeira fase. Isso mostra que

wA(ÿ) ÿ lÿk ÿ (k+l-2)k ÿ OPT(ÿ) ÿ k

Corolário 1.5: LRU e FWF são estritamente k-competitivos.

Exercício: Mostre que FIFO não é um algoritmo de marcação, mas estritamente k-competitivo.

Não existe uma constante r para a qual um algoritmo online A é r-competitivo, chamamos A de não competitivo

Proposição 1.6: LFU e LIFO não são competitivos.

Prova: Seja l ÿ 2 uma constante, k ÿ 2 o tamanho do cache. As diferentes páginas do cache são numeradas 1,...,k+1. Observamos a
seguinte sequência:

A primeira página 1 é solicitada l vezes que a página 2 e assim por diante. No final, há (l-1) solicitações alternadas para a página k e k+1.

LFU e LIFO preenchem seu cache com páginas 1-k. Quando a página k+1 é solicitada, a página k é despejada e vice-versa. Isso significa
que cada solicitação da subsequência (k,k+1)l-1 despeja uma página. Além disso, há k-1 faltas de cache para o primeiro uso das páginas 1-(k-1).
Então LFU e LIFO despejam exatamente k-1+2(l-1) páginas.
Agora devemos mostrar que para cada constante ÿÿÿ e cada constante r ÿ 1 existe um l tal que

que é igual a

Para satisfazer essa desigualdade, você só precisa escolher l suficientemente grande. Então LFU e LIFO não são competitivos.

Proposição 1.7: Não existe algoritmo online determinístico r-competitivo para paginação com r < k.

Notas de algoritmos do [Link] para profissionais 141


Machine Translated by Google

A prova para esta última proposição é bastante longa e baseada na declaração de que LFD é um algoritmo offline ótimo. O leitor
interessado pode procurá-la no livro de Borodin e El-Yaniv (veja as fontes abaixo).

A questão é se poderíamos fazer melhor. Para isso, temos que deixar a abordagem determinística para trás e começar a randomizar nosso
algoritmo. Claramente, é muito mais difícil para o adversário punir seu algoritmo se ele for randomizado.

A paginação aleatória será discutida em um dos próximos exemplos...

Notas de algoritmos do [Link] para profissionais 142


Machine Translated by Google

Capítulo 28: Classificação


Parâmetro Descrição Um

algoritmo de classificação é estável se preserva a ordem relativa de elementos iguais após a classificação.
Estabilidade

Um algoritmo de classificação está em vigor se ele classifica usando apenas memória auxiliar O(1) (sem contar a matriz que precisa
No lugar
ser classificada).

Um algoritmo de classificação tem uma complexidade de tempo no melhor caso de O(T(n)) se seu tempo de execução for pelo
Melhor complexidade de caso
menos T(n) para todas as entradas possíveis.

Complexidade Um algoritmo de classificação tem uma complexidade média de tempo de caso de O(T(n)) se seu tempo de execução, calculado
média do caso sobre todas as entradas possíveis, for T(n).

Um algoritmo de classificação tem uma complexidade de tempo de pior caso de O(T(n)) se seu tempo de execução for no máximo
Pior caso de complexidade
T(n).

Seção 28.1: Estabilidade na classificação


Estabilidade na classificação significa se um algoritmo de classificação mantém a ordem relativa das chaves de igualdade da entrada original na saída do resultado.

Portanto, um algoritmo de classificação é considerado estável se dois objetos com chaves iguais aparecem na mesma ordem na saída classificada em que aparecem na

matriz de entrada não classificada.

Considere uma lista de pares:

(1, 2) (9, 7) (3, 4) (8, 6) (9, 3)

Agora vamos classificar a lista usando o primeiro elemento de cada par.

Uma classificação estável desta lista produzirá a lista abaixo:

(1, 2) (3, 4) (8, 6) (9, 7) (9, 3)

Porque (9, 3) aparece depois de (9, 7) na lista original também.

Uma classificação instável produzirá a lista abaixo:

(1, 2) (3, 4) (8, 6) (9, 3) (9, 7)

A classificação instável pode gerar a mesma saída que a classificação estável, mas nem sempre.

Tipos estáveis bem conhecidos:

Mesclar classificação

Ordenação por inserção

Classificação de radix

Tim classificar

Classificação por bolhas

Tipos instáveis bem conhecidos:

Classificação de pilha

Classificação rápida

Notas de algoritmos do [Link] para profissionais 143


Machine Translated by Google

Capítulo 29: Classificação por bolhas

Parâmetro Descrição
Estável Sim

No lugar Sim

Melhor complexidade de caso Sobre)

Complexidade média do caso O(n^2)

Pior caso de complexidade O(n^2)

Complexidade do espaço O(1)

Seção 29.1: Classificação por bolhas

O BubbleSort compara cada par sucessivo de elementos em uma lista não ordenada e inverte os elementos se eles não estiverem em ordem.

O exemplo a seguir ilustra a classificação por bolhas na lista {6,5,3,1,8,7,2,4} (os pares que foram comparados em cada etapa são encapsulados
em '**'):

{6,5,3,1,8,7,2,4}
{**5,6** ,3,1,8,7,2,4} -- 5 < 6 -> trocar
{5,**3,6**,1,8,7,2,4} -- 3 < 6 -> trocar
{5,3,**1,6**,8,7,2,4} -- 1 < 6 -> trocar
{5,3,1,**6,8**,7,2,4} -- 8 > 6 -> sem troca
{5,3,1,6,**7,8**,2,4} -- 7 < 8 -> trocar
{5,3,1,6,7,**2,8**,4} -- 2 < 8 -> trocar
{5,3,1,6,7,2,**4,8**} -- 4 < 8 -> trocar

Após uma iteração pela lista, temos {5,3,1,6,7,2,4,8}. Note que o maior valor não classificado no array (8 neste caso) sempre alcançará sua
posição final. Assim, para ter certeza de que a lista está classificada, devemos iterar n-1 vezes para listas de comprimento n.

Gráfico:

Seção 29.2: Implementação em C e C++

Um exemplo de implementação do BubbleSort em C++:

void bubbleSort(vetor<int>números) {

para(int i = nú[Link]() - 1; i >= 0; i--) { para(int j =


1; j <= i; j++) {
se(números[j-1] > números[j]) {
trocar(números[j-1],números(j));

Notas de algoritmos do [Link] para profissionais 144


Machine Translated by Google

}
}
}
}

Implementação C

void bubble_sort( lista longa[], longo n) {

longo c, d, t;

para (c = 0 ; c < ( n - 1 ); c++) {

para (d = 0 ; d < n - c - 1; d++) {

se (lista[d] > lista[d+1]) {

/ * Trocando */

para
= lista[d];
lista[d] = lista[d+1]; lista[d+1]
= t;
}
}
}
}

Classificação por bolhas com ponteiro

void pointer_bubble_sort(long * lista, longo n) {

longo c, d, t;

para (c = 0 ; c < ( n - 1 ); c++) {

para (d = 0 ; d < n - c - 1; d++) {

se ( * (lista + d ) > *(lista+d+1)) {

/ * Trocando */

para
= * (lista + d ); * (lista
+ d ) = * (lista + d + 1 ); * (lista + d + 1) = t;

}
}
}
}

Seção 29.3: Implementação em C#


Bubble sort também é conhecido como Sinking Sort. É um algoritmo de classificação simples que percorre repetidamente a lista a ser classificada, compara cada

par de itens adjacentes e os troca se estiverem na ordem errada.

Exemplo de classificação por bolhas

Notas de algoritmos do [Link] para profissionais 145


Machine Translated by Google

Implementação do Bubble Sort


Usei a linguagem C# para implementar o algoritmo bubble sort

classe pública BubbleSort {

público estático vazio SortBubble(int[] entrada) {

para (var i = [Link] - 1; i >= 0; i--) {

para (var j = [Link] - 1 - 1; j >= 0; j--) {

se (entrada[j] <= entrada[j + 1]) continuar; var temp


= entrada[j + 1]; entrada[j + 1] =
entrada[j]; entrada[j] = temp;

}
}
}

público estático int[] Main(int[] entrada) {

SortBubble(entrada);
retorna entrada;
}
}

Seção 29.4: Implementação Python


#!/ usr/ bin/ python

lista_de_entrada = [10,1,2,11]

para i em intervalo(len(lista_de_entrada)):
para j em intervalo(i):
se int(lista_de_entrada[j]) > int(lista_de_entrada[j+1]):
lista_de_entrada[j],lista_de_entrada[j+1] = lista_de_entrada[j+1],lista_de_entrada[j]

imprimir lista_de_entrada

Notas de algoritmos do [Link] para profissionais 146


Machine Translated by Google

Seção 29.5: Implementação em Java


classe pública MyBubbleSort {

público estático vazio bubble_srt(int array[]) {// lógica principal int n =


[Link]; int k; para (int
m = n;
m >= 0; m--) {
para (int i = 0; i < n - 1; i++) { k = i + 1; se
(matriz[i] >
matriz[k]) {
swapNumbers(i, k, matriz);
}

} imprimirNúmeros(matriz);
}
}

privado estático vazio swapNumbers(int i, int j, int[] array) {

int temp;
temp = matriz[i];
matriz[i] = matriz[j]; matriz[j]
= temp;
}

privado estático vazio printNumbers(int[] entrada) {

para (int i = 0; i < [Link]; i++)


{ [Link](input[i] + ", ");
}
[Link]("\n");
}

público estático vazio main(String[] args) { int[] entrada


= { 4, 2, 9, 6, 23, 12, 34, 0, 1 }; bubble_srt(entrada);

}
}

Seção 29.6: Implementação em Javascript


função bubbleSort(a)
{
var trocado;
faça
{ trocado = falso;
para (var i=0; i < [Link]-1; i++) { se (a[i] > a[i+1])
{ var temp = a[i]; a[i] =
a[i+1]; a[i+1] = temp;
trocado =
verdadeiro;

} } enquanto (trocado);
}

var a = [3, 203, 34, 746, 200, 984, 198, 764, 9];

Notas de algoritmos do [Link] para profissionais 147


Machine Translated by Google

bubbleSort(a);
[Link](a); //registros [ 3, 9, 34, 198, 200, 203, 746, 764, 984 ]

Notas de algoritmos do [Link] para profissionais 148


Machine Translated by Google

Capítulo 30: Mesclar classificação

Seção 30.1: Noções básicas de classificação por mesclagem

Merge Sort é um algoritmo de divisão e conquista. Ele divide a lista de entrada de comprimento n pela metade sucessivamente até que haja n
listas de tamanho 1. Então, pares de listas são mesclados com o menor primeiro elemento entre o par de listas sendo adicionado em cada etapa.
Por meio de mesclagens sucessivas e por meio da comparação dos primeiros elementos, a lista ordenada é construída.

Um exemplo:

Complexidade de tempo: T(n) = 2T(n/2) + ÿ(n)

A recorrência acima pode ser resolvida usando o método Recurrence Tree ou o método Master. Ela se enquadra no caso II do Método Master
e a solução da recorrência é ÿ(nLogn). A complexidade de tempo do Merge Sort é ÿ(nLogn) em todos os 3 casos (pior, médio e melhor), pois o
merge sort sempre divide o array em duas metades e leva tempo linear para mesclar as duas metades.

Espaço Auxiliar: O(n)

Paradigma Algorítmico: Dividir e Conquistar

Notas de algoritmos do [Link] para profissionais 149


Machine Translated by Google

Classificação no local: Não em uma implementação típica

Estável: Sim

Seção 30.2: Implementação de Merge Sort em Go


pacote principal

importar "fmt"

func mergeSort(a []int) []int { se len(a) <


2{
retornar um

} m := (len(a)) / 2

f := mesclarSort(a[:m]) s :=
mesclarSort(a[m:])

retornar mesclar(f, s)
}

func merge(f []int, s []int) []int { var i, j int


tamanho :=
len(f) + len(s)

a := make([]int, tamanho, tamanho)

para z := 0; z < tamanho; z++


{ lenF := len(f)
lenS := len(s)

se eu > lenF-1 && j <= lenS-1 { a[z] =


s[j] j++

} senão se j > lenS-1 && i <= lenF-1 { a[z] = f[i]

eu++

} senão se f[i] < s[j] { a[z] = f[i] i++ }


senão { a[z] = s[j]
j++

}
}

retornar um
}

func main() { a :=
[]int{75, 12, 34, 45, 0, 123, 32, 56, 32, 99, 123, 11, 86, 33} [Link](a)

[Link](mergeSort(a))
}

Seção 30.3: Implementação de Merge Sort em C e C#


C Mesclar classificação

Notas de algoritmos do [Link] para profissionais 150


Machine Translated by Google

int mesclar(int arr[],int l,int m,int h) {

int arr1[10],arr2[10]; // Dois arrays temporários para conter os dois arrays a


serem mesclados int n1,n2,i,j,k; n1=m-l+1; n2=hm;

para(i=0; i<n1; i++)


arr1[i]=arr[l+i]; para(j=0;
j<n2; j++)
arr2[j]=arr[m+j+1];

arr1[i]=9999; // Para marcar o fim de cada matriz temporária arr2[j]=9999;

i=0;
j=0;
for(k=l; k<=h; k++) { //processo de combinação de dois arrays ordenados
se(arr1[i]<=arr2[j]) arr[k]=arr1[i+
+]; senão arr[k]=arr2[j++];

retornar 0;
}

int merge_sort(int arr[],int baixo,int alto) {

int médio;
se(baixo<alto)
{ médio=(baixo+alto)/2;
// Dividir e Conquistar
merge_sort(arr,baixo,médio);
merge_sort(arr,médio+1,alto);
// Combine
merge(arr,baixo,médio,alto);
}

retornar 0;
}

C# Mesclar classificação

classe pública MergeSort {

estático vazio Mesclar(int[] entrada, int l, int m, int r) {

int eu, j; var


n1 = m - l + 1; var n2 = r - m;

var esquerda = novo int[n1]; var


direita = novo int[n2];

para (i = 0; i < n1; i++) {

esquerda[i] = entrada[l + i];


}

151
Notas de algoritmos do [Link] para profissionais
Machine Translated by Google

para (j = 0; j < n2; j++) {

direita[j] = entrada[m + j + 1];


}

eu = 0;
j = 0;
var k = l;

enquanto (i < n1 && j < n2) {

se (esquerda[i] <= direita[j]) {

entrada[k] = esquerda[i];
i++;

}
outro {
entrada[k] = direita[j]; j++;

} k++;
}

enquanto (i < n1) {

entrada[k] = esquerda[i];
i++;
k++;
}

enquanto (j < n2) {

entrada[k] = direita[j]; j++;


k++;

}
}

estático vazio SortMerge(int[] entrada, int l, int r) {

se (l < r) {

int m = l + (r - l) / 2;
SortMerge(entrada, l, m);
SortMerge(entrada, m + 1, r);
Mesclar(entrada, l, m, r);
}
}

público estático int[] Main(int[] entrada) {

SortMerge(entrada, 0, [Link] - 1);


retornar entrada;
}
}

Seção 30.4: Implementação de Merge Sort em Java


Abaixo está a implementação em Java usando uma abordagem genérica. É o mesmo algoritmo, que é apresentado acima.

Notas de algoritmos do [Link] para profissionais 152


Machine Translated by Google

interface pública InPlaceSort<T estende Comparable<T>> { void sort(final


T[] elementos); }

classe pública MergeSort < T estende Comparável < T >> implementa InPlaceSort < T > {

@Override

public void sort(T[] elementos) { T[] arr = (T[]) novo


Comparável[[Link]]; classificar(elementos, arr, 0, [Link] -
1);
}

// Verificamos ambos os lados e então os mesclamos private


void sort(T[] elements, T[] arr, int low, int high) { if (low >= high) return; int mid = low
+ (high - low) / 2; sort(elements,
arr, low, mid); sort(elements, arr, mid + 1,
high); merge(elements, arr, low, high,
mid);

privado void merge(T[] a, T[] b, int baixo, int alto, int médio) { int i = baixo; int j =
médio + 1;

// Selecionamos o menor elemento dos dois. E então o colocamos em b for (int k = low; k <=
high; k++) {

se (i <= médio e j <= alto) { se


(a[i].compareTo(a[j]) >= 0) { b[k] = a[j++]; }
senão { b[k] = a[i+
+];

}
} senão se (j > alto && i <= médio) { b[k] = a[i+
+]; } senão se (i >
médio && j <= alto) { b[k] = a[j++];

}
}

para (int n = baixo; n <= alto; n++) { a[n] = b[n];

}}}

Seção 30.5: Implementação de Merge Sort em Python


def merge(X, Y): mesclar
"
duas listas ordenadas " p1 = p2 = 0 out
= [] enquanto p1
< len(X) e
p2 < len(Y): se X[p1] < Y[p2]: [Link](X[p1]) p1 += 1
else: [Link](Y[p2]) p2
+= 1 out += X[p1:] + Y[p2:]

Notas de algoritmos do [Link] para profissionais 153


Machine Translated by Google
retornar para fora

def mergeSort(A): se len(A)


<= 1: retorna A se len(A)
== 2: retorna

ordenado(A)

mid = len(A) / 2
retornar mesclar(mergeSort(A[:mid]), mesclarSort(A[mid:]))

se __nome__ == "__principal__":
# Gere 20 números aleatórios e classifique-os
A = [randint(1, 100) para i em xrange(20)] imprimir
mergeSort(A)

Seção 30.6: Implementação Java Bottoms-up


classe pública MergeSortBU {
matriz estática privada Integer[] = { 4, 3, 1, 8, 9, 15, 20, 2, 5, 6, 30, 70,
60,80,0,9,67,54,51,52,24,54,7 };

público MergeSortBU() { }

privado estático vazio mesclar(Comparable[] arrayToSort, Comparable[] aux, int lo,int mid, int hi) {

para (int índice = 0; índice < [Link]; índice++) { aux[índice] =


arrayToSort[índice];
}

int i = lo; int j


= médio + 1; para (int
k = lo; k <= hi; k++) { se (i > médio)

arrayToSort[k] = aux[j++]; senão if (j


> hi) arrayToSort[k] =
aux[i++]; senão if (isLess(aux[i],
aux[j])) { arrayToSort[k] = aux[i++]; } else
{ arrayToSort[k] = aux[j++];

}
}

público estático vazio classificar(Comparable[] arrayToSort, Comparable[] aux, int lo, int hi) {
int N = [Link]; for
(int sz = 1; sz < N; sz = sz + sz) { for (int low = 0; low < N;
low = low + sz + sz) { [Link]("Tamanho:"+ sz );
mesclar(arrayToSort, aux, low, low + sz
-1 ,[Link](low + sz + sz - 1, N - 1)); imprimir(arrayToSort);

}
}

public static boolean isLess(Comparável a, Comparável b) {


retornar [Link](b) <= 0;

Notas de algoritmos do [Link] para profissionais 154


Machine Translated by Google

private static void print(Comparable[] array) {http://


[Link]/ documentation/ algorithm/ 5732/ merge-sort# StringBuffer buffer
= new StringBuffer();http://
[Link]/ documentation/ algorithm/ 5732/ merge-sort# for (Comparable value : array)
{ [Link](value); [Link](' ');

}
[Link](buffer);
}

público estático vazio main(String[] args)


{ Comparável[] aux = novo Comparável[[Link]];
imprimir(matriz);
[Link](matriz, aux, 0, [Link] - 1);
}
}

Notas de algoritmos do [Link] para profissionais 155


Machine Translated by Google

Capítulo 31: Ordenação por inserção

Seção 31.1: Implementação Haskell


insertSort :: Ord a => [a] -> [a] insertSort []
= [] insertSort (x:xs) =
inserir x (insertSort xs)

inserir :: Ord a => a-> [a] -> [a] inserir n [] =


[n] inserir n (x:xs) | n
<= x = (n:x:xs)
| caso contrário = x:inserir n xs

Notas de algoritmos do [Link] para profissionais 156


Machine Translated by Google

Capítulo 32: Bucket Sort


Seção 32.1: Implementação C#
classe pública BucketSort {

público estático vazio SortBucket(ref int[] entrada) {

int minValue = entrada[0]; int


maxValue = entrada[0]; int k =
0;

para (int i = [Link] - 1; i >= 1; i--) {

se (entrada[i] > maxValue) maxValue = entrada[i]; se


(entrada[i] < minValue) minValue = entrada[i];
}

Lista<int>[] bucket = nova Lista<int>[valormáximo - valormínimo + 1];

para (int i = [Link] - 1; i >= 0; i--) {

bucket[i] = nova Lista<int>();


}

foreach (int i na entrada) {

bucket[i - minValue].Adicionar(i);
}

foreach (Lista<int> b no intervalo) {

se ([Link] > 0)
{
para cada (int t em b) {

entrada[k] = t; k+
+;
}
}
}
}

público estático int[] Main(int[] entrada) {

SortBucket( entrada de
referência); retorna entrada;
}
}

Notas de algoritmos do [Link] para profissionais 157


Machine Translated by Google

Capítulo 33: Quicksort


Seção 33.1: Noções básicas de Quicksort

Classificação rápida é um algoritmo de classificação que escolhe um elemento ("o pivô") e reordena o array formando duas partições
de modo que todos os elementos menores que o pivô venham antes dele e todos os elementos maiores venham depois. O algoritmo é
então aplicado recursivamente às partições até que a lista seja classificada.

1. Mecanismo do esquema de partição Lomuto:

Este esquema escolhe um pivô que é tipicamente o último elemento no array. O algoritmo mantém o índice para colocar o pivô na variável i e cada vez que ele encontra

um elemento menor ou igual ao pivô, este índice é incrementado e aquele elemento seria colocado antes do pivô.

partição(A, baixo, alto) é pivô := A[alto] i :=


baixo

para j := baixo para alto – 1 faça se A[j] ÿ


pivot então troque A[i] por A[j]

eu := eu + 1
troque A[i] com A[alto] retorne i

Mecanismo de classificação rápida:

quicksort(A, baixo, alto) é se baixo < alto


então p := partition(A, baixo,
alto) quicksort(A, baixo, p – 1) quicksort(A, p +
1, alto)

Exemplo de classificação rápida:

Notas de algoritmos do [Link] para profissionais 158


Machine Translated by Google

2. Esquema de partição de Hoare:

Ele usa dois índices que começam nas extremidades do array que está sendo particionado, então se movem em direção um ao outro, até que
detectem uma inversão: um par de elementos, um maior ou igual ao pivô, um menor ou igual, que estão na ordem errada em relação um ao
outro. Os elementos invertidos são então trocados. Quando os índices se encontram, o algoritmo para e retorna o índice final. O esquema de Hoare é
mais eficiente que o esquema de partição de Lomuto porque ele faz três vezes menos trocas em média, e ele cria partições eficientes mesmo quando
todos os valores são iguais.

quicksort(A, lo, hi) é se lo < hi então


p := partition(A, lo, hi)
quicksort(A, lo, p) quicksort(A, p + 1, hi)

Partição:

partition(A, lo, hi) é pivô := A[lo] i := lo


- 1 j := hi + 1 loop para
sempre faça:

eu := eu + 1

Notas de algoritmos do [Link] para profissionais 159


Machine Translated by Google

enquanto A[i] < pivot do

fazer:

j := j - 1
enquanto A[j] > pivot do

se i >= j então
retorne j

trocar A[i] por A[j]

Seção 33.2: Quicksort em Python


def quicksort(arr): se
len(arr) <= 1:
retorno arr
pivot = arr[len(arr) / 2] esquerda =
[x para x em arr se x < pivot] meio = [x para x em
arr se x == pivot] direita = [x para x em arr se x >
pivot] return quicksort(esquerda) + meio +
quicksort(direita)

imprimir quicksort([3,6,8,10,1,2,1])

Imprime "[1, 1, 2, 3, 6, 8, 10]"

Seção 33.3: Implementação de partição Lomuto Java


classe pública Solução {

público estático vazio main(String[] args) { Scanner sc


= novo Scanner([Link]); int n = [Link]();
int[] ar = novo int[n]; para(int
i=0; i<n; i++) ar[i] = [Link]();
quickSort(ar, 0, [Link]-1);

público estático vazio quickSort(int[] ar, int baixo, int alto) {

se(baixo<alto) {

int p = partição(ar, baixo, alto); quickSort(ar,


0 , p-1); quickSort(ar, p+1, alto);

} público estático int partição(int[] ar, int l, int r) {

int pivô = ar[r]; int i =l;


para(int j=l;
j<r; j++) {

se(ar[j] <= pivô)


{
int t = ar[j]; ar[j] =
ar[i]; ar[i] = t; i++;

Notas de algoritmos do [Link] para profissionais 160


Machine Translated by Google

} int t = ar[i]; ar[i]


= ar[r]; ar[r] = t;

retornar i;
}

Notas de algoritmos do [Link] para profissionais 161


Machine Translated by Google

Capítulo 34: Classificação por contagem

Seção 34.1: Informações básicas sobre classificação por contagem


Classificação por contagem é um algoritmo de classificação de inteiros para uma coleção de objetos que classifica de acordo com as chaves
dos objetos.

Passos

1. Construa uma matriz de trabalho C que tenha tamanho igual ao intervalo da matriz de entrada A.
2. Itere por A, atribuindo C[x] com base no número de vezes que x apareceu em A.
3. Transforme C em uma matriz onde C[x] se refere ao número de valores ÿ x iterando pela matriz,
atribuindo a cada C[x] a soma de seu valor anterior e todos os valores em C que vêm antes dele.
4. Itere para trás através de A, colocando cada valor em uma nova matriz classificada B no índice registrado em C. Isso é feito para um dado A[x]
atribuindo B[C[A[x]]] a A[x], e decrementando C[A[x]] caso haja valores duplicados na matriz original não classificada.

Exemplo de classificação por contagem

Espaço Auxiliar: O(n+k)


Complexidade de tempo: Pior caso: O(n+k), Melhor caso: O(n), Caso médio O(n+k)

Seção 34.2: Implementação de pseudocódigo


Restrições:

1. Entrada (uma matriz a ser classificada)


2. Número de elementos na entrada (n)
3. Chaves no intervalo de 0..k-1 (k)
4. Contagem (uma matriz de números)

Pseudocódigo:

para x na entrada:
count[key(x)] += 1 total = 0
para i no
intervalo(k): oldCount =
count[i] count[i] = total total +=
oldCount

Notas de algoritmos do [Link] para profissionais 162


Machine Translated by Google

para x na entrada:
saída[contagem[chave(x)]] = x
contagem[chave(x)] += 1
retorno saída

Notas de algoritmos do [Link] para profissionais 163


Machine Translated by Google

Capítulo 35: Heap Sort


Seção 35.1: Implementação C#

classe pública HeapSort {

público estático vazio Heapify(int[] entrada, int n, int i) {

int maior = i; int l =


i + 1; int r = i + 2;

se (l < n && input[l] > input[maior]) maior = l;

se (r < n && input[r] > input[maior]) maior = r;

se (maior != i) {

var temp = input[i]; input[i]


= input[maior]; input[maior] = temp;

Heapify(entrada, n, maior);
}
}

público estático vazio SortHeap(int[] entrada, int n) {

para (var i = n - 1; i >= 0; i--) {

Heapify(entrada, n, i);

} para (int j = n - 1; j >= 0; j--) {

var temp = entrada[0];


entrada[0] = entrada[j];
entrada[j] = temp;
Heapify(entrada, j, 0);
}
}

público estático int[] Main(int[] entrada) {

SortHeap(entrada, [Link]);
retornar entrada;
}
}

Seção 35.2: Informações básicas sobre classificação de heap


Classificação de pilha é uma técnica de classificação baseada em comparação em estrutura de dados de heap binário. É semelhante à
classificação por seleção, na qual primeiro encontramos o elemento máximo e o colocamos no final da estrutura de dados. Em seguida, repetimos o
mesmo processo para os itens restantes.

Pseudocódigo para Heap Sort:

função heapsort(entrada, contagem)

Notas de algoritmos do [Link] para profissionais 164


Machine Translated by Google

heapify(a,count) fim <-


contagem - 1 enquanto
fim -> 0 faça swap(a[fim],a[0])
fim<-fim-1 restaure(a, 0, fim)

função heapify(a, contagem)


iniciar <- pai(contagem - 1) enquanto iniciar
>= 0 fazer restaurar(a, iniciar,

contagem - 1) iniciar <- iniciar - 1

Exemplo de Heap Sort:

Espaço Auxiliar: O(1)


Complexidade de tempo: O(nlogn)

Notas de algoritmos do [Link] para profissionais 165


Machine Translated by Google

Capítulo 36: Classificação cíclica

Seção 36.1: Implementação de pseudocódigo


(entrada)
saída = 0 para
cycleStart de 0 a length(array) - 2 item = array[cycleStart] pos = cycleStart
para i de cycleStart + 1 a length(array) - 1
se array[i] < item: pos += 1
se pos == cycleStart: continue

enquanto item == array[pos]: pos += 1


array[pos],
item = item, array[pos] escreve += 1 enquanto pos != cycleStart:
pos = cycleStart

para i de cycleStart + 1 para length(array) -


1 se array[i] < item: pos += 1
enquanto item == array[pos]: pos += 1 array[pos], item = item, array[pos] escreve +=
1

retorno para fora

Notas de algoritmos do [Link] para profissionais 166


Machine Translated by Google

Capítulo 37: Classificação Ímpar-Par


Seção 37.1: Informações básicas sobre classificação ímpar-par

Uma classificação ímpar-par ou brick sort é um algoritmo de classificação simples, que é desenvolvido para uso em processadores paralelos com
interconexão local. Ele funciona comparando todos os pares indexados ímpares/pares de elementos adjacentes na lista e, se um par estiver na ordem
errada, os elementos são trocados. A próxima etapa repete isso para pares indexados pares/ímpares. Então ele alterna entre etapas ímpares/
pares e pares/ímpares até que a lista seja classificada.

Pseudocódigo para classificação ímpar-par:

se n>2 então
1. aplicar merge(n/2) ímpar-par recursivamente à subsequência par a0, a2, ..., subsequência ímpar a1, a3, , ..., an-2 e para o
um-1
2. comparação [i : i+1] para todos os elementos i {1, 3, 5, 7, ..., n-3} senão comparação [0 : 1]

A Wikipedia tem a melhor ilustração da classificação Ímpar-Par:

Exemplo de classificação ímpar-par:

Notas de algoritmos do [Link] para profissionais 167


Machine Translated by Google

Implementação:

Usei a linguagem C# para implementar o algoritmo de classificação ímpar-par.

classe pública OddEvenSort {

privado estático vazio SortOddEven (int [ ] entrada, int n ) {

var sort = falso ;

enquanto ( !classificar )
{
ordenar =
verdadeiro ; para (var i = 1 ; i < n - 1 ; i += 2 ) {

se (entrada [ i ] <= entrada [i + 1 ]) continuar ; var temp =


entrada [ i ]; entrada [ i ] =
entrada [i + 1 ]; entrada [i + 1 ] =
temp ; ordenar = falso ;

} for (var i = 0 ; i < n - 1 ; i += 2 ) {

se (entrada [ i ] <= entrada [i + 1 ]) continuar ; var temp =


entrada [ i ]; entrada [ i ] =
entrada [i + 1 ]; entrada [i + 1 ] =
temp ; ordenar = falso ;

Notas de algoritmos do [Link] para profissionais 168


Machine Translated by Google

}
}

público estático int[] Main(int[] entrada) {

SortOddEven(entrada, [Link]);
retornar entrada;
}
}

Espaço Auxiliar: O(n)


Complexidade de tempo: O(n)

Notas de algoritmos do [Link] para profissionais 169


Machine Translated by Google

Capítulo 38: Seleção de classificação

Seção 38.1: Implementação do Elixir


defmodule Seleção fazer

def sort(lista) quando is_list(lista) do do_selection(lista, [])


fim

def do_selection([cabeçalho|[]], acc) do acc ++ [cabeçalho]


fim

def do_selection(lista, acc) do


min = min(lista)
do_selection(:[Link](min, lista), acc ++ [min]) fim

defp min([primeiro|[segundo|[]]]) do menor(primeiro,


segundo) fim

defp min([primeiro|[segundo|cauda]]) do
min([menor(primeiro, segundo)|cauda]) fim

defp menor(e1, e2) faça se e1 <= e2


faça
e1
senão
e2
fim
fim
fim

Seleçã[Link]([100,4,10,6,9,3])
|> [Link]

Seção 38.2: Informações básicas sobre classificação por seleção


Seleção de classificação é um algoritmo de classificação, especificamente uma classificação por comparação no local. Ele tem complexidade de
tempo O(n2), o que o torna ineficiente em listas grandes, e geralmente tem desempenho pior do que a classificação por inserção semelhante. A
classificação por seleção é conhecida por sua simplicidade, e tem vantagens de desempenho sobre algoritmos mais complicados em certas
situações, particularmente onde a memória auxiliar é limitada.

O algoritmo divide a lista de entrada em duas partes: a sublista de itens já classificados, que é construída da esquerda para a direita na frente (esquerda) da
lista, e a sublista de itens restantes a serem classificados que ocupam o restante da lista.
Inicialmente, a sublista ordenada está vazia e a sublista não ordenada é a lista de entrada inteira. O algoritmo prossegue encontrando o menor (ou
maior, dependendo da ordem de ordenação) elemento na sublista não ordenada, trocando-o (trocando-o) com o elemento não ordenado mais à esquerda
(colocando-o em ordem ordenada) e movendo os limites da sublista um elemento para a direita.

Pseudocódigo para classificação por seleção:

função select(lista[1..n], k)
para i de 1 a k

Notas de algoritmos do [Link] para profissionais 170


Machine Translated by Google
minIndex = i
minValue = list[i] para j de
i+1 a n se list[j] < minValue
minIndex = j minValue = list[j]
troque list[i] e
list[minIndex] retorne list[k]

Visualização da classificação por seleção:

Exemplo de classificação por seleção:

Notas de algoritmos do [Link] para profissionais 171


Machine Translated by Google

Espaço Auxiliar: O(n)

Complexidade de tempo: O(n^2)

Seção 38.3: Implementação de Selection sort em C#


Usei a linguagem C# para implementar o algoritmo de classificação por seleção.

classe pública SelectionSort {

privado estático vazio SortSelection(int[] entrada, int n) {

para (int i = 0; i < n - 1; i++) {

var minId = i; intj ;


para (j
= i + 1; j < n; j++) {

se (entrada[j] < entrada[minId]) minId = j;

} var temp = entrada[minId];

Notas de algoritmos do [Link] para profissionais 172


Machine Translated by Google

entrada[minId] = entrada[i];
entrada[i] = temp;
}
}

público estático int[] Main(int[] entrada) {

SortSelection(entrada, [Link]);
retornar entrada;
}
}

Notas de algoritmos do [Link] para profissionais 173


Machine Translated by Google

Capítulo 39: Procurando


Seção 39.1: Pesquisa Binária
Introdução

Binary Search é um algoritmo de busca de Dividir e Conquistar. Ele usa tempo O(log n) para encontrar a localização de um elemento em um
espaço de busca onde n é o tamanho do espaço de busca.

A Pesquisa Binária funciona dividindo pela metade o espaço de pesquisa a cada iteração após comparar o valor alvo com o valor médio do
espaço de pesquisa.

Para usar a Busca Binária, o espaço de busca deve ser ordenado (classificado) de alguma forma. Entradas duplicadas (aquelas que são
comparadas como iguais de acordo com a função de comparação) não podem ser distinguidas, embora não violem a propriedade Busca
Binária.

Convencionalmente, usamos menor que (<) como a função de comparação. Se a < b, retornará true. Se a não for menor que b e b não for menor
que a, a e b são iguais.

Exemplo de pergunta

Você é um economista, mas um bem ruim. Você recebeu a tarefa de encontrar o preço de equilíbrio (ou seja, o preço onde oferta = demanda)
para o arroz.

Lembre-se de que quanto mais alto for o preço definido, maior será a oferta e menor será a demanda

Como sua empresa é muito eficiente em calcular as forças de mercado, você pode obter instantaneamente a oferta e a demanda em unidades de
arroz quando o preço do arroz é definido em um determinado preço p.

Seu chefe quer o preço de equilíbrio o mais rápido possível, mas diz que o preço de equilíbrio pode ser um inteiro positivo que é no máximo 10^17
e que há garantia de haver exatamente 1 solução inteira positiva no intervalo. Então, comece seu trabalho antes que você o perca!

Você tem permissão para chamar as funções getSupply(k) e getDemand(k), que farão exatamente o que é declarado no problema.

Exemplo de explicação

Aqui nosso espaço de busca é de 1 a 10^17. Portanto, uma busca linear é inviável.

No entanto, observe que, à medida que k sobe, getSupply(k) aumenta e getDemand(k) diminui. Assim, para qualquer x > y,
getSupply(x) - getDemand(x) > getSupply(y) - getDemand(y). Portanto, esse espaço de busca é monotônico e podemos usar a
Busca Binária.

O pseudocódigo a seguir demonstra o uso da Pesquisa Binária:

alto = 100000000000000000 <- Limite superior do espaço de busca


baixo = 1 <- Limite inferior do espaço de busca
enquanto alto - baixo > 1
médio = (alto + baixo) / 2 <- Pegue o valor médio
oferta = obterOferta(médio)
demanda =
obterDemanda(médio)
<- A solução está na metade inferior do espaço de busca
se oferta > demanda alto = médio

Notas de algoritmos do [Link] para profissionais 174


Machine Translated by Google

senão se demanda > oferta


baixa = <- A solução está na metade superior do espaço de busca <- oferta==
média condição de demanda
senão retorna média <- Solução encontrada

Este algoritmo roda em tempo ~O(log 10^17) . Isso pode ser generalizado para tempo ~O(log S) onde S é o tamanho do espaço de busca, já que a cada

iteração do loop while , nós dividimos o espaço de busca pela metade (de [low:high] para [low:mid] ou [mid:high]).

Implementação C de Pesquisa Binária com Recursão

int binsearch(int a[], int x, int baixo, int alto) {


int meio;

se (baixo > alto)


retornar -1;

médio = (baixo + alto) / 2;

se (x == a[médio]) { retornar
(médio); } senão se
(x <
a[médio]) { binsearch(a, x,
baixo, médio - 1); } senão { binsearch(a, x,
médio + 1,
alto);
}
}

Seção 39.2: Rabin Karp


O algoritmo Rabin–Karp ou algoritmo Karp–Rabin é um algoritmo de busca de strings que usa hash para encontrar qualquer uma de um conjunto de
strings de padrões em um texto. Seu tempo de execução médio e no melhor caso é O(n+m) no espaço O(p), mas seu tempo no pior caso é O(nm), onde
n é o comprimento do texto e m é o comprimento do padrão.

Implementação de algoritmo em Java para correspondência de strings

void RabinfindPattern(String texto,String padrão){


/*
qa número primo p valor
hash para padrão t valor hash para
texto
d é o número de caracteres únicos no alfabeto de entrada */ int d=128; int q=100; int

n=[Link]();
int
m=[Link](); int t=0,p=0;
int h=1; int i,j; //função de cálculo do
valor hash for
(i=0;i<m-1;i+
+) h = (h*d)
%q; for (i=0;i<m;i++){ p = (d*p + [Link](i))%q;
t = (d*t + [Link](i))%q; }

// procurar o padrão

Notas de algoritmos do [Link] para profissionais 175


Machine Translated by Google

for(i=0;i<end-m;i++){ if(p==t){ //
se o valor do
hash corresponder, combine-os caractere por caractere for(j=0;j<m;j++) if([Link](j+i)!
=[Link](j))
break; if(j==m && i>=start)

[Link](" Correspondência de padrão encontrada no índice "+i);

} se(i<fim-m){ t
=(d*(t - [Link](i)*h) + [Link](i+m))%q; se(t<0) t=t+q;

}
}
}

Ao calcular o valor de hash, nós o dividimos por um número primo para evitar colisões. Depois de dividir por um número primo, as chances de
colisão serão menores, mas ainda há uma chance de que o valor de hash seja o mesmo para duas strings, então, quando obtemos uma
correspondência, temos que verificar caractere por caractere para garantir que obtivemos uma correspondência adequada.

t =(d*(t - [Link](i)*h) + [Link](i+m))%q;

Isso serve para recalcular o valor de hash do padrão, primeiro removendo o caractere mais à esquerda e depois adicionando o novo caractere
do texto.

Seção 39.3: Análise de busca linear (pior, médio e melhor caso)

Podemos ter três casos para analisar um algoritmo:

1. Pior Caso

2. Caso médio

3. Melhor cenário

#incluir <stdio.h>

// Busca linearmente x em arr[]. Se x estiver presente, então retorna o índice,

// caso contrário, retorne -1 int


search(int arr[], int n, int x) {

int i;
para (i=0; i<n; i++) {

se (arr[i] == x) retornar i;

retornar -1;
}

/* Programa de driver para testar as funções acima*/

int principal()

Notas de algoritmos do [Link] para profissionais 176


Machine Translated by Google

{
int arr[] = {1, 10, 30, 15}; int x = 30; int
n = sizeof(arr)/
sizeof(arr[0]); printf("%d está presente no índice
%d", x, search(arr, n, x));

obterchar();
retornar 0;
}

Análise do pior caso (geralmente feita)

Na análise do pior caso, calculamos o limite superior no tempo de execução de um algoritmo. Devemos saber o caso que faz com que o
número máximo de operações seja executado. Para a Busca Linear, o pior caso acontece quando o elemento a ser pesquisado (x
no código acima) não está presente no array. Quando x não está presente, as funções search() o comparam com todos os
elementos de arr[] um por um. Portanto, a complexidade de tempo do pior caso da busca linear seria ÿ(n)

Análise de caso médio (às vezes feito)

Na análise de caso médio, pegamos todas as entradas possíveis e calculamos o tempo de computação para todas as entradas. Some
todos os valores calculados e divida a soma pelo número total de entradas. Devemos saber (ou prever) a distribuição dos casos. Para o
problema de busca linear, vamos supor que todos os casos sejam uniformemente distribuídos (incluindo o caso de x não estar presente
na matriz). Então, somamos todos os casos e dividimos a soma por (n+1). A seguir está o valor da complexidade do tempo do caso
médio.

Análise do melhor caso (Falso)

Na análise do melhor caso, calculamos o limite inferior no tempo de execução de um algoritmo. Devemos saber o caso que faz com
que o número mínimo de operações seja executado. No problema de busca linear, o melhor caso ocorre quando x está presente no
primeiro local. O número de operações no melhor caso é constante (não dependente de n). Portanto, a complexidade de tempo no melhor
caso seria ÿ(1). Na maioria das vezes, fazemos a análise do pior caso para analisar algoritmos. Na pior análise, garantimos um limite
superior no tempo de execução de um algoritmo, o que é uma boa informação. A análise do caso médio não é fácil de fazer na maioria
dos casos práticos e raramente é feita. Na análise do caso médio, devemos saber (ou prever) a distribuição matemática de todas
as entradas possíveis. A análise do melhor caso é falsa. Garantir um limite inferior em um algoritmo não fornece nenhuma informação,
pois no pior caso, um algoritmo pode levar anos para ser executado.

Para alguns algoritmos, todos os casos são assintoticamente iguais, ou seja, não há pior e melhor caso. Por exemplo, Merge Sort.
Merge Sort faz operações ÿ(nLogn) em todos os casos. A maioria dos outros algoritmos de classificação tem pior e melhor caso. Por
exemplo, na implementação típica do Quick Sort (onde o pivô é escolhido como um elemento de canto), o pior ocorre quando o array
de entrada já está classificado e o melhor ocorre quando os elementos do pivô sempre dividem o array em duas metades. Para a
classificação por inserção, o pior caso ocorre quando o array é classificado inversamente e o melhor caso

Notas de algoritmos do [Link] para profissionais 177


Machine Translated by Google

ocorre quando a matriz é classificada na mesma ordem da saída.

Seção 39.4: Busca binária: em números ordenados


É mais fácil mostrar uma pesquisa binária em números usando pseudocódigo

int array[1000] = { lista ordenada de números }; int N = 100; // número de


entradas no espaço de busca; int alto, baixo, médio; // nossos temporários int x; //
valor a ser buscado

baixo = 0;
alto = N -1;
enquanto(baixo < alto) {

médio = (baixo + alto)/2;


if(array[médio] < x) baixo =
médio + 1; senão

alto = médio;

} if(array[low] == x) //
encontrado, índice é baixo else //
não
encontrado

Não tente retornar cedo comparando array[mid] com x para igualdade. A comparação extra só pode deixar o código mais lento. Note
que você precisa adicionar um a low para evitar ficar preso pela divisão inteira sempre arredondando para baixo.

Curiosamente, a versão acima da busca binária permite que você encontre a menor ocorrência de x no array. Se o array contiver
duplicatas de x, o algoritmo pode ser modificado levemente para retornar a maior ocorrência de x simplesmente adicionando ao
condicional if:

enquanto(baixo < alto) {

médio = baixo + ((alto - baixo) / 2); if(matriz[médio]


< x || (matriz[médio] == x && matriz[médio + 1] == x))
baixo = médio + 1;
senão
alto = médio;
}

Observe que, em vez de usar mid = (low + high) / 2, também pode ser uma boa ideia tentar mid = low + ((high - low) / 2) para implementações como Java para

diminuir o risco de ocorrer um estouro para entradas muito grandes.

Seção 39.5: Busca linear

A busca linear é um algoritmo simples. Ele faz um loop pelos itens até que a consulta seja encontrada, o que o torna um algoritmo
linear - a complexidade é O(n), onde n é o número de itens a serem percorridos.

Por que O(n)? No pior cenário, você tem que passar por todos os n itens.

Pode ser comparado a procurar um livro em uma pilha de livros: você folheia todos eles até encontrar aquele que deseja.

Abaixo está uma implementação Python:

Notas de algoritmos do [Link] para profissionais 178


Machine Translated by Google

def linear_search(lista_pesquisável, consulta): para x em


lista_pesquisável:
se consulta == x:
retornar Verdadeiro
retornar Falso

linear_search(['maçã', 'banana', 'cenoura', 'figo', 'alho'], 'figo') #retorna Verdadeiro

Notas de algoritmos do [Link] para profissionais 179


Machine Translated by Google

Capítulo 40: Pesquisa de substring


Seção 40.1: Introdução ao Knuth-Morris-Pratt (KMP)
Algoritmo
Suponha que temos um texto e um padrão. Precisamos determinar se o padrão existe no texto ou não. Por exemplo:

+-------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+-------+---+---+---+---+---+---+---+---+
| Texto | a | b | c | b | c | g | l | x |
+-------+---+---+---+---+---+---+---+---+

+---------+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 |
+---------+---+---+---+---+
| Padrão | b | c | g | l |
+---------+---+---+---+---+

Esse padrão existe no texto. Então nossa busca por substring deve retornar 3, o índice da posição da qual esse padrão começa. Então
como nosso procedimento de busca por substring por força bruta funciona?

O que normalmente fazemos é: começamos do índice 0 do texto e do índice 0 do nosso *padrão e comparamos Texto[0] com Padrão[0]. Como
eles não são uma correspondência, vamos para o próximo índice do nosso texto e comparamos Texto[1] com Padrão[0]. Como isso é uma
correspondência, incrementamos o índice do nosso padrão e o índice do Texto também. Comparamos Texto[2] com Padrão[1]. Eles também
são uma correspondência. Seguindo o mesmo procedimento declarado antes, agora comparamos Texto[3] com Padrão[2]. Como eles não
correspondem, começamos da próxima posição onde começamos a encontrar a correspondência. Esse é o índice 2 do Texto. Comparamos
Texto[2] com Padrão[0]. Eles não correspondem. Então, incrementando o índice do Texto, comparamos Texto[3] com Padrão[0]. Eles
correspondem. Novamente Texto[4] e Padrão[1] correspondem, Texto[5] e Padrão[2] correspondem e Texto[6] e Padrão[3] correspondem.
Como chegamos ao fim do nosso Pattern, agora retornamos o índice do qual nossa correspondência começou, que é 3. Se nosso pattern

fosse: bcgll, isso significa que se o pattern não existisse em nosso text, nossa busca deveria retornar exception ou -1 ou qualquer outro valor
predefinido. Podemos ver claramente que, no pior caso, esse algoritmo levaria tempo O(mn) onde m é o comprimento do Text e n é o comprimento
do Pattern. Como reduzimos essa complexidade de tempo? É aqui que o KMP Substring Search Algorithm entra em cena.

O algoritmo de busca de strings Knuth-Morris-Pratt ou o Algoritmo KMP busca ocorrências de um "Padrão" dentro de um "Texto"
principal empregando a observação de que quando ocorre uma incompatibilidade, a própria palavra incorpora informações
suficientes para determinar onde a próxima correspondência poderia começar, ignorando assim o reexame de caracteres previamente
correspondidos. O algoritmo foi concebido em 1970 por Donuld Knuth e Vaughan Pratt e independentemente por James H. Morris. O
trio publicou-o em conjunto em 1977.

Vamos estender nosso exemplo de Texto e Padrão para melhor compreensão:

+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+-- +--+--+--+--+--+--+--+--+--+
| Índice |0 |1 |2 |3 |4 |5 |6 |7 |8 |9 |10|11|12|13|14|15|16|17|18|19|20|21|22|
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+-- +--+--+--+--+--+--+--+--+--+
| Texto |a |b |c |x |a |b |c |d |a |b |x |a |b |c |d |a |b |c |d |a |b |c |y |
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+-- +--+--+--+--+--+--+--+--+--+

+---------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Notas de algoritmos do [Link] para profissionais 180


Machine Translated by Google
+---------+---+---+---+---+---+---+---+---+
| Padrão | a | b | c | d | a | b | c | y |
+---------+---+---+---+---+---+---+---+---+

Primeiro, nosso Text e Pattern correspondem até o índice 2. Text[3] e Pattern[3] não correspondem. Então, nosso objetivo é não voltar atrás
neste Text, ou seja, em caso de incompatibilidade, não queremos que nossa correspondência comece novamente da posição com a qual
começamos a correspondência. Para conseguir isso, procuraremos um sufixo em nosso Pattern logo antes de nossa incompatibilidade
ocorrer (substring abc), que também é um prefixo da substring de nosso Pattern. Para nosso exemplo, como todos os caracteres são
únicos, não há sufixo, que é o prefixo de nossa substring correspondente. Então, o que isso significa é que nossa próxima comparação
começará do índice 0. Espere um pouco, você entenderá por que fizemos isso. Em seguida, comparamos Text[3] com Pattern[0] e não
corresponde. Depois disso, para Text do índice 4 ao índice 9 e para Pattern do índice 0 ao índice 5, encontramos uma correspondência. Encontramos

uma incompatibilidade em Text[10] e Pattern[6]. Então pegamos a substring de Pattern logo antes do ponto onde ocorre a incompatibilidade
(substring abcdabc), verificamos se há um sufixo, que também é um prefixo desta substring. Podemos ver aqui que ab é o sufixo e o prefixo desta
substring. O que isso significa é que, como fizemos a correspondência até Text[10], os caracteres logo antes da incompatibilidade são ab.
O que podemos inferir disso é que, como ab também é um prefixo da substring que pegamos, não precisamos verificar ab novamente e a próxima
verificação pode começar em Text[10] e Pattern[2]. Não precisamos olhar para todo o Text, podemos começar diretamente de onde ocorreu
nossa incompatibilidade. Agora verificamos Text[10] e Pattern[2], já que é uma incompatibilidade, e a substring antes da incompatibilidade
(abc) não contém um sufixo que também é um prefixo, verificamos Text[10] e Pattern[0], eles não correspondem. Depois disso, para Text do
índice 11 ao índice 17 e para Pattern do índice 0 ao índice 6. Encontramos uma incompatibilidade em Text[18] e Pattern[7]. Então, novamente,
verificamos a substring antes da incompatibilidade (substring abcdabc) e descobrimos que abc é o sufixo e o prefixo. Então, como

fizemos a correspondência até Pattern[7], abc deve estar antes de Text[18]. Isso significa que não precisamos comparar até Text[17] e nossa
comparação começará em Text[18] e Pattern[3]. Assim, encontraremos uma correspondência e retornaremos 15 , que é nosso índice inicial
da correspondência. É assim que nossa Pesquisa de Substring KMP funciona usando informações de sufixo e prefixo.

Agora, como calculamos eficientemente se sufixo é o mesmo que prefixo e em que ponto iniciar a verificação se há uma incompatibilidade
de caractere entre Texto e Padrão. Vamos dar uma olhada em um exemplo:

+---------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Padrão | a | b | c | d | a | b | c | a |
+---------+---+---+---+---+---+---+---+---+

Geraremos um array contendo as informações necessárias. Vamos chamar o array de S. O tamanho do array será o mesmo que o comprimento do
padrão. Como a primeira letra do Padrão não pode ser o sufixo de nenhum prefixo, colocaremos S[0] = 0. Pegamos i = 1 e j = 0 primeiro. Em cada
etapa, comparamos Pattern[i] e Pattern[j] e incrementamos i. Se houver uma correspondência, colocamos S[i] = j + 1 e incrementamos j, se
houver uma incompatibilidade, verificamos a posição do valor anterior de j (se disponível) e definimos j = S[j-1] (se j não for igual a 0), continuamos
fazendo isso até que S[j] não corresponda a S[i] ou j não se torne 0. Para o último, colocamos S[i] = 0. Para nosso exemplo:

eu
eu

+---------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Padrão | a | b | c | d | a | b | c | a |
+---------+---+---+---+---+---+---+---+---+

Pattern[j] e Pattern[i] não correspondem, então incrementamos i e como j é 0, não verificamos o valor anterior e colocamos Pattern[i] = 0. Se
continuarmos incrementando i, para i = 4, obteremos uma correspondência, então colocamos S[i] = S[4] = j + 1 = 0 + 1 = 1 e

Notas de algoritmos do [Link] para profissionais 181


Machine Translated by Google

incremente j e i. Nosso array ficará assim:

eu
eu

+---------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Padrão | a | b | c | d | a | b | c | a |
+---------+---+---+---+---+---+---+---+---+
| S |0|0|0|0|1| | | |
+---------+---+---+---+---+---+---+---+---+

Como Pattern[1] e Pattern[5] são uma correspondência, colocamos S[i] = S[5] = j + 1 = 1 + 1 = 2. Se continuarmos, encontraremos
uma incompatibilidade para j = 3 e i = 7. Como j não é igual a 0, colocamos j = S[j-1]. E compararemos se os caracteres em i e j são iguais ou
não, já que são iguais, colocaremos S[i] = j + 1. Nosso array completo ficará assim:

+---------+---+---+---+---+---+---+---+---+
| S |0|0|0|0|1|2|3|1|
+---------+---+---+---+---+---+---+---+---+

Este é o nosso array necessário. Aqui, um valor diferente de zero de S[i] significa que há um sufixo de comprimento S[i] igual ao prefixo naquela
substring (substring de 0 a i) e a próxima comparação começará da posição S[i] + 1 do Pattern. Nosso algoritmo para gerar o array ficaria
assim:

Procedimento GenerateSuffixArray(Pattern): i := 1 j := 0 n :=

[Link] enquanto i for


menor que n se Pattern[i] for igual a

Pattern[j]
S[i] := j + 1 j := j + 1
i := i + 1 senão

se j não for igual a 0 j := S[j-1] senão

S[i] := 0 i := i +
1 fim se fim se
fim
enquanto

A complexidade de tempo para construir esta matriz é O(n) e a complexidade de espaço também é O(n). Para ter certeza de que você
entendeu completamente o algoritmo, tente gerar uma matriz para o padrão aabaabaa e verifique se o resultado corresponde a este um.

Agora vamos fazer uma pesquisa de substring usando o seguinte exemplo:

+---------+---+---+---+---+---+---+---+---+---+--- +---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 |
+---------+---+---+---+---+---+---+---+---+---+--- +---+---+
| Texto | a | b | x | a | b | c | a | b | c | a | b | y |
+---------+---+---+---+---+---+---+---+---+---+--- +---+---+

+---------+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 |

Notas de algoritmos do [Link] para profissionais 182


Machine Translated by Google
+---------+---+---+---+---+---+---+

| Padrão | a | b | c | a | b | y |
+---------+---+---+---+---+---+---+

| S |0|0|0|1|2|0|
+---------+---+---+---+---+---+---+

Temos um Text, um Pattern e um array S pré-calculado usando nossa lógica definida antes. Comparamos Text[0] e Pattern[0] e eles são
iguais. Text[1] e Pattern[1] são iguais. Text[2] e Pattern[2] não são iguais. Verificamos o valor na posição imediatamente antes da
incompatibilidade. Como S[1] é 0, não há sufixo que seja igual ao prefixo em nossa substring e nossa comparação começa na posição S[1], que é
0. Então Pattern[0] não é igual a Text[2], então seguimos em frente. Text[3] é igual a Pattern[0] e há uma correspondência até Text[8] e
Pattern[5]. Voltamos um passo no array S e encontramos 2. Então isso significa que há um prefixo de comprimento 2 que também é o sufixo
desta substring (abcab) que é ab. Isso também significa que há um ab antes de Text[8]. Então podemos ignorar com segurança Pattern[0] e
Pattern[1] e começar nossa próxima comparação de Pattern[2] e Text[8]. Se continuarmos, encontraremos o Pattern no Text. Nosso procedimento
será parecido com:

Procedimento KMP(Texto, Padrão)


GenerateSuffixArray(Padrão) m :=
[Link] n :=
Padrã[Link] i := 0

j := 0
enquanto i é menor que m

se Pattern[j] for igual a Text[i] j := j + 1 i := i + 1

se j é igual a n
Retorna (ji) senão
se i < m e Pattern[j] não for igual a t Text[i] se j não for igual a 0 j = S[j-1] senão

i := i + 1 fim se
fim se

fim enquanto
Retornar -1

A complexidade de tempo desse algoritmo, além do Cálculo de Matriz de Sufixos, é O(m). Como GenerateSuffixArray leva O(n), a complexidade de
tempo total do Algoritmo KMP é: O(m+n).

PS: Se você quiser encontrar múltiplas ocorrências de Pattern no Texto, em vez de retornar o valor, imprima-o/armazene-o e defina j := S[j-1].
Mantenha também um sinalizador para rastrear se você encontrou alguma ocorrência ou não e trate-a adequadamente.

Seção 40.2: Introdução ao Algoritmo Rabin-Karp


Algoritmo Rabin-Karp é um algoritmo de busca de strings criado por Richard M. Karp e Michael O. Rabin que usa hash para encontrar qualquer
uma de um conjunto de sequências de padrões em um texto.

Uma substring de uma string é outra string que ocorre em. Por exemplo, ver é uma substring de stackoverflow. Não deve ser confundido com
subsequência porque cover é uma subsequência da mesma string. Em outras palavras, qualquer subconjunto de letras consecutivas em
uma string é uma substring da string fornecida.

No algoritmo Rabin-Karp, geraremos um hash do nosso padrão que estamos procurando e verificaremos se o hash rolante do nosso texto
corresponde ao padrão ou não. Se não corresponder, podemos garantir que o padrão não existe no texto.

Notas de algoritmos do [Link] para profissionais 183


Machine Translated by Google

No entanto, se corresponder, o padrão pode estar presente no texto. Vejamos um exemplo:

Digamos que temos um texto: yeminsajid e queremos descobrir se o padrão nsa existe no texto. Para calcular o
hash e rolling hash, precisaremos usar um número primo. Pode ser qualquer número primo. Vamos pegar primo = 11 para
neste exemplo. Determinaremos o valor hash usando esta fórmula:

(1ª letra) X (primo) + (2ª letra) X (primo)¹ + (3ª letra) X (primo)² X + ......

Vamos denotar:

a -> 1 b g -> 7 h m -> 13 s -> 19 a -> 25


-> 2 -> 8 n -> 14 t -> 20 z -> 26
c -> 3 eu -> 9 o -> 15 você -> 21
d -> 4 e j -> 10 p -> 16 v -> 22
-> 5 f -> 6 k -> 11 q -> 17 w -> 23
eu -> 12 r -> 18 x -> 24

O valor de hash do nsa será:

14 X 11ÿ + 19 X 11¹ + 1 X 11² = 344

Agora encontramos o rolling-hash do nosso texto. Se o rolling hash corresponder ao valor do hash do nosso padrão, verificaremos se
as strings correspondem ou não. Como nosso padrão tem 3 letras, pegaremos as primeiras 3 letras yem do nosso texto e calcularemos
valor hash. Obtemos:

25 X 11ÿ + 5 X 11¹ + 13 X 11² = 1653

Este valor não corresponde ao valor hash do nosso padrão. Então a string não existe aqui. Agora precisamos considerar
o próximo passo. Para calcular o valor hash da nossa próxima string emi. Podemos calcular isso usando nossa fórmula. Mas isso
seria bastante trivial e nos custaria mais. Em vez disso, usamos outra técnica.

Subtraímos o valor da Primeira Letra da String Anterior do nosso valor hash atual. Neste caso, y. Nós
obtenha, 1653 - 25 = 1628.
Dividimos a diferença com nosso primo, que é 11 para este exemplo. Obtemos, 1628 / 11 = 148.
Adicionamos a nova letra X (primo)ÿ¹, onde m é o comprimento do padrão, com o quociente, que é i = 9. Nós
obtenha, 148 + 9 X 11² = 1237.

O novo valor hash não é igual ao valor hash dos nossos padrões. Seguindo em frente, para n obtemos:

String anterior : emi


Primeira letra da string anterior: e(5)
Nova Carta: n(14)
Nova sequência: "min"
1237 - 5 = 1232
1232 / 11 = 112
112 + 14 X 11² = 1806

Não corresponde. Depois disso, para s, obtemos:

String anterior : min


Primeira letra da string anterior: m(13)
Nova Carta: s(19)
Nova sequência: "ins"
1806 - 13 = 1793
1793 / 11 = 163

Notas de algoritmos do [Link] para profissionais 184


Machine Translated by Google

163 + 19 X 11² = 2462

Não corresponde. Em seguida, para a, obtemos:

String anterior : ins


Primeira letra da string anterior: i(9)
Nova Carta: a(1)
Nova sequência: "nsa" 2462
- 9 = 2453 2453 / 11 =
223
223 + 1 X 11² = 344

É uma correspondência! Agora comparamos nosso padrão com a string atual. Como ambas as strings correspondem, a substring existe nesta string. E
retornamos a posição inicial da nossa substring.

O pseudocódigo será:

Cálculo de hash:

Procedimento Calculate-Hash(String, Prime, x): hash := 0


// Aqui x denota o comprimento a ser considerado para m de 1 a x // para
encontrar o valor de hash

hash := hash + (Valor de String[m])ÿ¹ fim para

Retornar hash

Recálculo de Hash:

Procedimento Recalculate-Hash(String, Curr, Prime, Hash): Hash := Hash - Valor de


String[Curr] //aqui Curr denota a Primeira Letra da String Anterior Hash := Hash / Prime m := [Link] Novo := Curr + m - 1

Hash := Hash + (Valor de String[Novo])ÿ¹


Retornar Hash

Correspondência de sequência:

Procedimento String-Match(Texto, Padrão, m): para i de m para


Comprimento do padrão + m - 1
se Text[i] não for igual a Pattern[i]
Retorna falso fim
se fim
para
Retornar verdadeiro

Rabin-Karp:

Procedimento Rabin-Karp(Texto, Padrão, Primo): m := Padrã[Link]


HashValue := Calculate-
Hash(Padrão, Primo, m)
CurrValue := Calculate-Hash(Pattern, Prime, m) para i de 1 a [Link] - m

se HashValue == CurrValue e String-Match(Texto, Padrão, i) for verdadeiro


Retornar i
fim se

CurrValue := Recalculate-Hash(String, i+1, Prime, CurrValue) fim para

Notas de algoritmos do [Link] para profissionais 185


Machine Translated by Google
Retornar -1

Se o algoritmo não encontrar nenhuma correspondência, ele simplesmente retornará -1.

Este algoritmo é usado para detectar plágio. Dado o material de origem, o algoritmo pode pesquisar rapidamente em um artigo por instâncias de
frases do material de origem, ignorando detalhes como caso e pontuação. Devido à abundância das sequências procuradas, algoritmos de busca de
sequência única são impraticáveis aqui. Novamente, o algoritmo Knuth-Morris-Pratt ou o algoritmo Boyer-Moore String Search é um
algoritmo de busca de sequência única de padrão mais rápido do que Rabin-Karp. No entanto, é um algoritmo de escolha para busca de
padrões múltiplos. Se quisermos encontrar qualquer um dos grandes números, digamos k, padrões de comprimento fixo em um texto, podemos criar
uma variante simples do algoritmo Rabin-Karp.

Para texto de comprimento n e padrões p de comprimento combinado m, seu tempo de execução médio e melhor caso é O(n+m) no espaço
O(p), mas seu tempo de pior caso é O(nm).

Seção 40.3: Implementação Python do algoritmo KMP


Palheiro: A sequência na qual o padrão fornecido precisa ser pesquisado.
Agulha: O padrão a ser pesquisado.

Complexidade de tempo: A parte de pesquisa (método strstr) tem a complexidade O(n), onde n é o comprimento do palheiro, mas como a
agulha também é pré-analisada para construir a tabela de prefixos, O(m) é necessário para construir a tabela de prefixos, onde m é o comprimento
da agulha.

Portanto, a complexidade de tempo geral para KMP é O(n+m)


Complexidade do espaço: O(m) por causa da tabela de prefixos na agulha.

Observação: a implementação a seguir retorna a posição inicial da correspondência no palheiro (se houver uma correspondência), caso contrário,
retorna -1, para casos extremos, como se agulha/palheiro for uma string vazia ou agulha não for encontrada no palheiro.

def get_prefix_table(agulha): prefix_set = set() n =


len(agulha) prefix_table = [0]*n
delimitador = 1
while(delimitador<n):

prefix_set.add(agulha[:delimitador])
j = 1 while(j<delimitador+1): se agulha[j:delimitador+1] em

prefix_set:

prefix_table[delimitador] = delimitador - j + 1 quebra

j += 1
delimitador += 1

retornar prefix_table

def strstr(palheiro, agulha):


# m: denotando a posição dentro de S onde a correspondência potencial para W começa # i: denotando o índice do caractere
atualmente considerado em W. haystack_len = len(haystack) needle_len = len(needle) if (needle_len > haystack_len)
or (not haystack_len) or (not needle_len):

retornar -1

prefix_table = get_prefix_table(agulha) m = i = 0 while((i<needle_len)


e

(m<haystack_len)): se haystack[m] == agulha[i]: i += 1

Notas de algoritmos do [Link] para profissionais 186


Machine Translated by Google

m += 1
outro:
se eu != 0:
i = prefix_table[i-1] senão:

m += 1

se i==needle_len e haystack[m-1] == needle[i-1]: retorne m - needle_len senão:

retornar -1

se __nome__ == '__principal__':
agulha = 'abcaby' palheiro
= 'abxabcabcaby' print strstr(palheiro,
agulha)

Seção 40.4: Algoritmo KMP em C


Dado um texto txt e um padrão pat, o objetivo deste programa será imprimir todas as ocorrências de pat em txt.

Exemplos:

Entrada:

txt[] = "ESTE É UM TEXTO DE TESTE" pat[] =


"TESTE"

saída:

Padrão encontrado no índice 10

Entrada:

txt[] = "AABAACAADAABAAABAA" pat[] =


"AABA"

saída:

Padrão encontrado no índice 0


Padrão encontrado no índice 9
Padrão encontrado no índice 13

Implementação da Linguagem C:

// Programa C para implementação de pesquisa de padrões KMP // algoritmo #include<stdio.h>

#incluir<string.h>
#incluir<stdlib.h>

vazio computeLPSArray(char *pat, int M, int *lps);

vazio KMPSearch(char *pat, char *txt) {

int M = strlen(pat); int N =


strlen(txt);

// crie lps[] que conterá o maior prefixo sufixo

Notas de algoritmos do [Link] para profissionais 187


Machine Translated by Google

// valores para padrão int *lps =


(int *)malloc(sizeof(int)*M); int j = 0; // índice para pat[]

// Pré-processar o padrão (calcular array lps[]) computeLPSArray(pat, M, lps);

int i = 0; // índice para txt[] while (i < N) {

se (pat[j] == txt[i]) {

j++; i+
+;
}

se (j == M) {

printf(" Padrão encontrado no índice %d \n", ij); j = lps[j-1];

// incompatibilidade após j corresponde


else if (i < N && pat[j] != txt[i]) {

// Não corresponda aos caracteres lps[0..lps[j-1]], // eles corresponderão


de qualquer maneira if (j != 0) j = lps[j-1];
else

eu = i+1;
}

} free(lps); // para evitar vazamento de memória


}

vazio computeLPSArray(char *pat, int M, int *lps) {

int len = 0; // comprimento do sufixo de prefixo mais longo anterior int i;

lps[0] = 0; // lps[0] é sempre 0 i = 1;

// o loop calcula lps[i] para i = 1 a M-1 enquanto (i < M) {

se (pat[i] == pat[len]) {

len++;
lps[i] = len; i++;

} else // (pat[i] != pat[len]) {

se (comprimento != 0)
{
// Isso é complicado. Considere o exemplo // AAACAAAA e i =
7. len = lps[len-1];

// Observe também que não incrementamos i aqui

Notas de algoritmos do [Link] para profissionais 188


Machine Translated by Google

} senão // se (len == 0) {

lps[i] = 0; i++;

}
}
}
}

// Programa de driver para testar a função acima int


main() {

char *txt = "ABABDABACDABABCABAB"; char


*pat = "ABABCABAB";
KMPSearch(pat, txt);
retornar 0;
}

Saída:

Padrão encontrado no índice 10

Referência:

[Link]

Notas de algoritmos do [Link] para profissionais 189


Machine Translated by Google

Capítulo 41: Busca em Largura


Seção 41.1: Encontrando o caminho mais curto da fonte para outros nós

Busca em largura (BFS) é um algoritmo para percorrer ou pesquisar estruturas de dados de árvore ou gráfico. Ele começa na raiz da
árvore (ou algum nó arbitrário de um gráfico, às vezes chamado de "chave de pesquisa") e explora os nós vizinhos primeiro, antes
de passar para os vizinhos do próximo nível. O BFS foi inventado no final dos anos 1950 por Edward Forrest Moore, que o usou para
encontrar o caminho mais curto para sair de um labirinto e foi descoberto independentemente por CY Lee como um algoritmo de
roteamento de fios em 1961.

Os processos do algoritmo BFS funcionam sob estas suposições:

1. Não percorreremos nenhum nó mais de uma vez.


2. O nó de origem ou o nó de onde estamos partindo está situado no nível 0.
3. Os nós que podemos alcançar diretamente do nó de origem são nós de nível 1, os nós que podemos alcançar diretamente
nós de nível 1 são nós de nível 2 e assim por diante.

4. O nível denota a distância do caminho mais curto da fonte.

Vejamos um exemplo:

Vamos supor que este gráfico representa a conexão entre várias cidades, onde cada nó denota uma cidade e uma aresta entre
dois nós denota que há uma estrada ligando-os. Queremos ir do nó 1 ao nó 10. Então o nó 1 é nossa fonte, que é o nível 0. Marcamos
o nó 1 como visitado. Podemos ir para o nó 2, nó 3 e nó 4 daqui. Então eles serão nós de nível (0+1) = nível 1. Agora vamos
marcá-los como visitados e trabalhar com eles.

Notas de algoritmos do [Link] para profissionais 190


Machine Translated by Google

Os nós coloridos são visitados. Os nós com os quais estamos trabalhando atualmente serão marcados em rosa. Não visitaremos o mesmo nó
duas vezes. Do nó 2, nó 3 e nó 4, podemos ir para o nó 6, nó 7 e nó 8. Vamos marcá-los como visitados. O nível desses nós será nível (1+1) =
nível 2.

Notas de algoritmos do [Link] para profissionais 191


Machine Translated by Google

Se você não notou, o nível de nós simplesmente denota a distância do caminho mais curto da fonte. Por exemplo:
encontramos o nó 8 no nível 2. Portanto, a distância da fonte ao nó 8 é 2.

Ainda não chegamos ao nosso nó alvo, que é o nó 10. Então vamos visitar os próximos nós. Podemos ir diretamente do nó 6, nó 7 e nó 8.

Notas de algoritmos do [Link] para profissionais 192


Machine Translated by Google

Podemos ver que encontramos o nó 10 no nível 3. Portanto, o caminho mais curto da origem ao nó 10 é 3. Pesquisamos o
gráfico nível por nível e encontramos o caminho mais curto. Agora vamos apagar as arestas que não usamos:

Notas de algoritmos do [Link] para profissionais 193


Machine Translated by Google

Após remover as arestas que não usamos, obtemos uma árvore chamada BFS tree. Esta árvore mostra o caminho mais curto da fonte para
todos os outros nós.

Então nossa tarefa será ir da fonte para os nós de nível 1. Então, dos nós de nível 1 para os nós de nível 2 e assim por diante até chegarmos
ao nosso destino. Podemos usar a fila para armazenar os nós que vamos processar. Ou seja, para cada nó com o qual vamos trabalhar,
vamos empurrar todos os outros nós que podem ser atravessados diretamente e ainda não atravessados na fila.

A simulação do nosso exemplo:

Primeiro, colocamos a fonte na fila. Nossa fila ficará assim:

frente
++-----
|1|
++-----

O nível do nó 1 será 0. level[1] = 0. Agora iniciamos nosso BFS. Primeiro, retiramos um nó da nossa fila. Obtemos o nó 1. Podemos ir para
o nó 4, nó 3 e nó 2 a partir deste. Chegamos a esses nós a partir do nó 1. Então level[4] = level[3] = level[2] = level[1] + 1 = 1. Agora
os marcamos como visitados e os colocamos na fila.

frente
+-----+ +-----+ +-----+
|2| |3| |4|
+-----+ +-----+ +-----+

Notas de algoritmos do [Link] para profissionais 194


Machine Translated by Google

Agora, nós estouramos o nó 4 e trabalhamos com ele. Podemos ir para o nó 7 a partir do nó 4. level[7] = level[4] + 1 = 2. Nós marcamos o nó 7
como visitado e colocá-lo na fila.

frente
+-----+ +-----+ +-----+
|7| |2| |3|
+-----+ +-----+ +-----+

Do nó 3, podemos ir para o nó 7 e o nó 8. Como já marcamos o nó 7 como visitado, marcamos o nó 8 como


visitado, alteramos level[8] = level[3] + 1 = 2. Empurramos o nó 8 na fila.

frente
+-----+ +-----+ +-----+
|6| |7| |2|
+-----+ +-----+ +-----+

Este processo continuará até que cheguemos ao nosso destino ou a fila fique vazia. O array de níveis nos fornecerá
com a distância do caminho mais curto da fonte. Podemos inicializar a matriz de nível com valor infinito , que marcará
que os nós ainda não foram visitados. Nosso pseudocódigo será:

Procedimento BFS(Gráfico, fonte):


Q = fila();
nível[] = infinito
nível[fonte] := 0
[Link](fonte)
enquanto Q não estiver vazio
u -> [Link]()
para todas as arestas de u a v na lista de adjacência
se nível[v] == infinito
nível[v] := nível[u] + 1
[Link](v)
fim se
fim para
fim enquanto
Nível de retorno

Ao iterar através do array de níveis , podemos descobrir a distância de cada nó da fonte. Por exemplo: o
a distância do nó 10 da fonte será armazenada no nível[10].

Às vezes, podemos precisar imprimir não apenas a distância mais curta, mas também o caminho pelo qual podemos ir até nosso
nó destinado da fonte. Para isso, precisamos manter um array pai . parent[source] será NULL. Para cada
atualização no array de nível , simplesmente adicionaremos parent[v] := u em nosso pseudo código dentro do loop for. Após terminar o BFS,
para encontrar o caminho, percorreremos a matriz pai até chegarmos à origem , que será denotada pelo valor NULL.
O pseudocódigo será:

Procedimento PrintPath(u): //recursivo se parent[u] | Procedimento PrintPath(u): // iterativo


não for igual a nulo | S = Pilha()
PrintPath(parent[u]) fim se | enquanto parent[u] não for igual a null [Link](u)
imprimir
-> u | u := pai[u]
| | fim enquanto
| enquanto S não estiver vazio
| imprimir -> [Link]
| fim enquanto

Notas de algoritmos do [Link] para profissionais 195


Machine Translated by Google

Complexidade:

Visitamos cada nó uma vez e cada aresta uma vez. Então a complexidade será O(V + E) onde V é o número de nós e E é o número de arestas.

Seção 41.2: Encontrando o caminho mais curto da fonte em um gráfico 2D


Na maioria das vezes, precisamos descobrir o caminho mais curto de uma única fonte para todos os outros nós ou um nó específico em um gráfico
2D. Digamos, por exemplo: queremos descobrir quantos movimentos são necessários para um cavalo atingir uma determinada casa em um
tabuleiro de xadrez, ou temos uma matriz onde algumas células estão bloqueadas, temos que descobrir o caminho mais curto de uma célula
para outra. Podemos nos mover apenas horizontalmente e verticalmente. Até mesmo movimentos diagonais também podem ser possíveis.
Para esses casos, podemos converter os quadrados ou células em nós e resolver esses problemas facilmente usando BFS. Agora, nossos
visitados, pai e nível serão matrizes 2D. Para cada nó, consideraremos todos os movimentos possíveis. Para encontrar a distância até um nó
específico, também verificaremos se chegamos ao nosso destino.

Haverá uma coisa adicional chamada direction array. Isso simplesmente armazenará todas as combinações possíveis de direções para as
quais podemos ir. Digamos que, para movimentos horizontais e verticais, nossos direction arrays serão:

+----+-----+-----+-----+-----+

| dx | 1 | -1 | 0 | 0 |
+----+-----+-----+-----+-----+

| sim | 0 | 0 | 1 | -1 |
+----+-----+-----+-----+-----+

Aqui dx representa movimento no eixo x e dy representa movimento no eixo y. Novamente, esta parte é opcional. Você também pode escrever
todas as combinações possíveis separadamente. Mas é mais fácil lidar com isso usando o array direction. Pode haver mais e até mesmo
combinações diferentes para movimentos diagonais ou movimentos de cavalo.

A parte adicional que precisamos ter em mente é:

Se alguma célula estiver bloqueada, para cada movimento possível, verificaremos se a célula está bloqueada ou não.
Também verificaremos se saímos dos limites, ou seja, se cruzamos os limites da matriz.
O número de linhas e colunas será fornecido.

Nosso pseudocódigo será:

Procedimento BFS2D(Gráfico, bloco de sinal, linha, coluna): para i de 1


para linha para j de 1 para
coluna
visitou[i][j] := falso fim para

fim para
visitado[fonte.x][fonte.y] := verdadeiro nível[fonte.x]
[fonte.y] := 0 Q = fila()

[Link](source) m :=
[Link] enquanto
Q não estiver vazio top := [Link]
para i de 1 a m
temp.x := top.x + dx[i]
temp.y := top.y + dy[i] se temp estiver
dentro da linha e coluna e top não for
igual a blocksign visited[temp.x][temp.y] := true level[temp.x][temp.y] := level[top.x][top.y] + 1 [Link](temp)

Notas de algoritmos do [Link] para profissionais 196


Machine Translated by Google

fim se
de ponta a
ponta enquanto
Nível de retorno

Como discutimos anteriormente, o BFS só funciona para gráficos não ponderados. Para gráficos ponderados, precisaremos do algoritmo
de Dijkstra. Para ciclos de arestas negativas, precisamos do algoritmo de Bellman-Ford. Novamente, esse algoritmo é um algoritmo
de caminho mais curto de fonte única. Se precisarmos descobrir a distância de cada nó para todos os outros nós, precisaremos do
algoritmo de Floyd-Warshall.

Seção 41.3: Componentes conectados de grafo não


direcionado usando BFS
O BFS pode ser usado para encontrar os componentes conectados de um gráfico não direcionado. Também podemos descobrir se o
gráfico dado é conectado ou não. Nossa discussão subsequente assume que estamos lidando com gráficos não direcionados. A definição
de um gráfico conectado é:

Um grafo é conectado se houver um caminho entre cada par de vértices.

A seguir está um gráfico conectado.

O gráfico a seguir não é conectado e tem 2 componentes conectados:

1. Componente conectado 1: {a,b,c,d,e}


2. Componente conectado 2: {f}

Notas de algoritmos do [Link] para profissionais 197


Machine Translated by Google

BFS é um algoritmo de travessia de grafos. Então, começando de um nó de origem aleatório, se no término do algoritmo, todos os
nós forem visitados, então o grafo é conectado, caso contrário, não é conectado.

Pseudocódigo para o algoritmo.

boolean isConnected(Graph g) { BFS(v)//v é

um nó de origem aleatório. if(allVisited(g)) {

retornar

verdadeiro; } senão
retornar falso; }

Implementação em C para descobrir se um grafo não direcionado é conectado ou não:

#incluir<stdio.h>
#include<stdlib.h> #define
MAXVÉRTICES 100

void enqueue(int); int deque(); int

isConnected(char

**graph,int noOfVertices); void BFS(char **graph,int vertex,int noOfVertices); int count = 0; //

O nó da fila descreve um único elemento da fila //NÃO é um nó de gráfico. struct node {

int v;
struct nó *próximo;
};

typedef struct nó Nó; typedef struct nó


*Nodeptr;

Nodeptr Qfront = NULL; Nodeptr


Qrear = NULL; char *visited;//
matriz que rastreia os vértices visitados.

int principal()

Notas de algoritmos do [Link] para profissionais 198


Machine Translated by Google

{
int n,e;//n é o número de vértices, e é o número de arestas. int i,j; char **graph;//
matriz de
adjacência

printf("Digite o número de vértices:"); scanf("%d",&n);

if(n < 0 || n > MAXVERTICES)

{ fprintf(stderr, "Por favor, insira um inteiro positivo válido de 1 a %d",MAXVERTICES); return -1; }

gráfico = malloc(n * sizeof(char *)); visitado =


malloc(n*sizeof(char));

para(i = 0;i < n;++i) {

graph[i] = malloc(n*sizeof(int)); visited[i] = 'N';//


inicialmente todos os vértices não são visitados. for(j = 0;j < n;++j) graph[i][j] = 0;

printf("digite o número de arestas e depois insira-as em pares:"); scanf("%d",&e);

para(i = 0;i < e;++i) {

int u,v;
scanf("%d%d",&u,&v);
gráfico[u-1][v-1] = 1;
gráfico[v-1][u-1] = 1;
}

if(isConnected(graph,n)) printf("O
gráfico está conectado");
else printf("O gráfico NÃO está conectado\n");
}

void enqueue(int vértice) {

se(Qfront == NULL) {

Qfront = malloc(tamanho de(Nó));


Qfront->v = vértice;
Qfront->próximo = NULL;
Qrear = Qfrontal;

} outro
{
Nodeptr novoNode = malloc(tamanhode(Nó)); novoNode-
>v = vértice; novoNode-
>próximo = NULL;
Qrear->próximo = novoNó;
Qrear = novoNó;
}
}

int deque() {

Notas de algoritmos do [Link] para profissionais 199


Machine Translated by Google

se(Qfront == NULL) {

printf("Q está vazio, retornando -1\n"); return -1;

} outro
{
int v = Qfront->v;
Nodeptr temp= Qfront; se(Qfront
== Qrear) {

Qfront = Qfront->próximo;
Qrear = NULO;

} outro
Qfront = Qfront->próximo;

livre(temp);
retornar v;
}
}

int isConnected(char **gráfico,int noOfVertices) {

int eu;

//deixe o vértice de origem aleatório ser o vértice 0;


BFS(gráfico,0,noOfVertices);

for(i = 0;i < noOfVertices;++i) if(visited[i] == 'N')


return 0;//0 implica falso;

retornar 1;//1 implica verdadeiro;


}

vazio BFS(char **gráfico,int v,int noOfVertices) {

int i, vértice;
visitado[v] = 'Y';
enfileirar(v);
enquanto((vértice = deque()) != -1) {

para(i = 0;i < noOfVertices;++i) se(gráfico[vértice]


[i] == 1 && visitado[i] == 'N') {

enqueue(i);
visitado[i] = 'S';
}
}
}

Para encontrar todos os componentes conectados de um grafo não direcionado, precisamos apenas adicionar 2 linhas de código à função BFS. A ideia é
chamar a função BFS até que todos os vértices sejam visitados.

As linhas a serem adicionadas são:

printf("\ nComponente conectado %d\n",++count); //count é uma


variável global inicializada como 0 // adicione isso como primeira
linha à função BFS

Notas de algoritmos do [Link] para profissionais 200


Machine Translated by Google

printf("%d ",vertex+1); adicione


isso como primeira linha do loop while no BFS

e definimos a seguinte função:

lista vazia ConnectedComponents (char ** gráfico, int noOfVertices) {

int i;
para(i = 0;i < noOfVertices;++i) {

se(visitado[i] == 'N')
BFS(gráfico,i,noOfVertices);

}
}

Notas de algoritmos do [Link] para profissionais 201


Machine Translated by Google

Capítulo 42: Busca em Profundidade

Seção 42.1: Introdução à busca em profundidade


Busca em profundidade é um algoritmo para percorrer ou pesquisar estruturas de dados de árvore ou gráfico. Começa-se na
raiz e explora-se o máximo possível ao longo de cada ramo antes de retroceder. Uma versão de busca em profundidade
foi investigada pelo matemático francês do século XIX Charles Pierre Trémaux como uma estratégia para resolver labirintos.

Busca em profundidade é uma maneira sistemática de encontrar todos os vértices alcançáveis de um vértice de origem. Assim como a busca em largura, o

DFS percorre um componente conectado de um dado grafo e define uma árvore de abrangência. A ideia básica da busca em profundidade é explorar

metodicamente cada aresta. Começamos de novo de um vértice diferente, conforme necessário. Assim que descobrimos um vértice, o DFS começa a explorar

a partir dele (ao contrário do BFS, que coloca um vértice em uma fila para que ele explore a partir dele mais tarde).

Vamos dar uma olhada em um exemplo. Vamos percorrer este gráfico:

Percorreremos o gráfico seguindo estas regras:

Começaremos pela fonte.


Nenhum nó será visitado duas vezes.

Os nós que ainda não visitamos serão coloridos de branco.

O nó que visitamos, mas não visitamos todos os seus nós filhos, será colorido em cinza.

Os nós completamente percorridos serão coloridos de preto.

Vamos analisar passo a passo:

Notas de algoritmos do [Link] para profissionais 202


Machine Translated by Google

Notas de algoritmos do [Link] para profissionais 203


Machine Translated by Google

Notas de algoritmos do [Link] para profissionais 204


Machine Translated by Google

Podemos ver uma palavra-chave importante. Que é backedge. Você pode ver. 5-1 é chamado de backedge. Isso ocorre porque ainda não
terminamos com o nó-1, então ir de outro nó para o nó-1 significa que há um ciclo no gráfico. No DFS, se pudermos ir de um nó cinza
para outro, podemos ter certeza de que o gráfico tem um ciclo. Esta é uma das maneiras de detectar o ciclo em um gráfico. Dependendo
do nó de origem e da ordem dos nós que visitamos, podemos descobrir qualquer aresta em um ciclo como backedge. Por exemplo: se
fôssemos para 5 a partir de 1 primeiro, teríamos descoberto 2-1 como backedge.

A aresta que pegamos para ir do nó cinza para o nó branco é chamada de aresta da árvore. Se mantivermos apenas as arestas da árvore
e removermos outras, teremos a árvore DFS.

Em grafos não direcionados, se pudermos visitar um nó já visitado, isso deve ser um backedge. Mas para grafos direcionados, devemos
verificar as cores. Se e somente se pudermos ir de um nó cinza para outro nó cinza, isso é chamado de backedge.

No DFS, também podemos manter registros de data e hora para cada nó, que podem ser usados de várias maneiras (por exemplo: classificação topológica).

1. Quando um nó v é alterado de branco para cinza, o tempo é registrado em d[v].

Notas de algoritmos do [Link] para profissionais 205


Machine Translated by Google

2. Quando um nó v é alterado de cinza para preto, o tempo é registrado em f[v].

Aqui d[] significa tempo de descoberta e f[] significa tempo de término. Nosso pseudocódigo ficará assim:

Procedimento DFS(G): para


cada nó u em V[G] cor[u] := branco
pai[u] := NULL

fim para o
tempo := 0
para cada nó u em V[G]
se cor[u] == branco
DFS-Visit(u) fim se

fim para

Procedimento DFS-Visit(u): color[u] :=


cinza time := tempo + 1
d[u] := tempo para cada
nó v adjacente a u
se color[v] == branco parent[v] := u

DFS-Visit(v) fim se
fim para
cor[u] :=
preto tempo := tempo + 1
f[u] := tempo

Complexidade:

Cada nó e aresta são visitados uma vez. Então a complexidade do DFS é O(V+E), onde V denota o número de nós e E denota o número de arestas.

Aplicações da Busca em Profundidade:

Encontrando o caminho mais curto de todos os pares em um grafo não direcionado.

Detectando ciclo em um gráfico.

Encontrando o caminho.

Classificação topológica.

Testando se um gráfico é bipartido.

Encontrando Componente Fortemente Conectado.

Resolvendo quebra-cabeças com uma solução.

Notas de algoritmos do [Link] para profissionais 206


Machine Translated by Google

Capítulo 43: Funções Hash


Seção 43.1: Códigos de hash para tipos comuns em C#
Os códigos hash produzidos pelo método GetHashCode() para built-in e tipos comuns de C# do namespace System são mostrados
abaixo.

Booleano

1 se o valor for verdadeiro, 0 caso contrário.

Byte, UInt16, Int32, UInt32, Solteiro

Valor (se necessário convertido para Int32).

SByte

((int)m_valor ^ (int)m_valor << 8);


Carvão

(int)m_valor ^ ((int)m_valor << 16);


Int16

((int)((ushort)m_valor) ^ (((int)m_valor) << 16));


Int64, Dobro

Xor entre os 32 bits inferiores e superiores do número de 64 bits

(desmarcado((int)((longo)m_value)) ^ (int)(m_value >> 32));

UInt64, Data e hora, Intervalo de

tempo ((int)valor_m) ^ (int)(valor_m >> 32);

Decimal

((((int *)&dbl)[0]) & 0xFFFFFFF0) ^ ((int *)&dbl)[1];

Objeto

[Link](este);

A implementação padrão é usar o índice do bloco de sincronização.

Corda

A computação do código hash depende do tipo de plataforma (Win32 ou Win64), do recurso de uso de hash de string aleatório, do modo
Debug/ Release. No caso da plataforma Win64:

int hash1 = 5381; int


hash2 = hash1; int c;
char *s
= src; enquanto
((c = s[0]) != 0) { hash1 = ((hash1
<< 5) + hash1) ^ c = s[1]; se (c == 0) quebrar; c;
hash2 =
((hash2 << 5)
+ hash2)
^ s += 2; c;

Notas de algoritmos do [Link] para profissionais 207


Machine Translated by Google

retornar hash1 + (hash2 * 1566083941);

Tipo de valor

O primeiro campo não estático é look for e obtém seu hashcode. Se o tipo não tiver campos não estáticos, o hashcode do tipo retorna. O hashcode de
um membro estático não pode ser obtido porque se esse membro for do mesmo tipo que o tipo original, o cálculo termina em um loop infinito.

Anulável<T>

retornar hasValue ? [Link]() : 0;

Variedade

int ret = 0; para


(int i = (Comprimento >= 8 ? Comprimento - 8 : 0); i < Comprimento; i++) {

ret = ((ret << 5) + ret) ^ [Link](GetValue(i));


}

Referências

GitHub .Net Core CLR

Seção 43.2: Introdução às funções hash

A função hash h() é uma função arbitrária que mapeia dados x ÿ X de tamanho arbitrário para valor y ÿ Y de tamanho fixo: y = h(x). Boas funções
hash têm as seguintes restrições:

funções hash se comportam como distribuição uniforme

funções hash são determinísticas. h(x) deve sempre retornar o mesmo valor para um dado x

cálculo rápido (tem tempo de execução O(1))

No caso geral, o tamanho da função hash é menor que o tamanho dos dados de entrada: |y| < |x|. As funções hash não são reversíveis ou, em
outras palavras, podem ser colisões: ÿ x1, x2 ÿ X, x1 ÿ x2: h(x1) = h(x2). X pode ser um conjunto finito ou infinito e Y é um conjunto finito.

Funções hash são usadas em muitas partes da ciência da computação, por exemplo, em engenharia de software, criptografia, bancos de dados, redes,
aprendizado de máquina e assim por diante. Existem muitos tipos diferentes de funções hash, com diferentes propriedades específicas de domínio.

Frequentemente hash é um valor inteiro. Existem métodos especiais em linguagens de programação para cálculo de hash. Por exemplo, em C#, o
método GetHashCode() para todos os tipos retorna o valor Int32 (número inteiro de 32 bits). Em Java, cada classe fornece o método hashCode() que retorna
int. Cada tipo de dado tem implementações próprias ou definidas pelo usuário.

Métodos de hash

Existem várias abordagens para determinar a função hash. Sem perda de generalidade, deixe x ÿ X = {z ÿ ÿ: z ÿ 0} serem números inteiros
positivos. Frequentemente m é primo (não muito próximo de uma potência exata de 2).

Método Função hash


Método de divisão h(x) = x mod m

Método de multiplicação h(x) = ÿm (xA mod 1)ÿ, A ÿ {z ÿ ÿ: 0 < z < 1}

Tabela de hash

Funções de hash usadas em tabelas de hash para calcular índice em uma matriz de slots. A tabela de hash é uma estrutura de dados para

Notas de algoritmos do [Link] para profissionais 208


Machine Translated by Google

implementando dicionários (estrutura chave-valor). Boas tabelas hash implementadas têm tempo O(1) para o próximo
operações: inserir, pesquisar e excluir dados por chave. Mais de uma chave pode fazer hash no mesmo slot. Existem dois
maneiras de resolver colisões:

1. Encadeamento: a lista vinculada é usada para armazenar elementos com o mesmo valor de hash no slot

2. Endereçamento aberto: zero ou um elemento é armazenado em cada slot

Os próximos métodos são usados para calcular as sequências de sondagem necessárias para endereçamento aberto

Método Fórmula

Sondagem linear h(x, i) = (h'(x) + i) mod m


Sondagem quadrática h(x, i) = (h'(x) + c1*i + c2*i^2) mod m
Hash duplo h(x, i) = (h1(x) + i*h2(x)) mod m

Onde i ÿ {0, 1, ..., m-1}, h'(x), h1(x), h2(x) são funções hash auxiliares, c1, c2 são funções auxiliares positivas
constantes.

Exemplos

Seja x ÿ U{1, 1000}, h = x mod m. A próxima tabela mostra os valores de hash em caso de not prime e prime. Negrito
o texto indica os mesmos valores de hash.

xm = 100 (não primo) m = 101 (primo)


723 23 16

103 3 2

738 38 31

292 92 90

61 61 61

87 87 87

995 95 86

549 49 44

991 91 82

757 57 50

920 20 11

626 26 20

557 57 52

831 31 23

619 19 13

Ligações

Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introdução aos Algoritmos.

Visão geral das tabelas de hash

Wolfram MathWorld - Função Hash

Notas de algoritmos do [Link] para profissionais 209


Machine Translated by Google

Capítulo 44: Caixeiro Viajante


Seção 44.1: Algoritmo de Força Bruta
Um caminho através de cada vértice exatamente uma vez é o mesmo que ordenar o vértice de alguma forma. Assim, para calcular o custo mínimo
de viajar através de cada vértice exatamente uma vez, podemos forçar brutamente cada uma das N! permutações dos números de 1 a N.

Pseudocódigo

mínimo = INF para

todas as permutações P

atual = 0

para i de 0 a N-2
atual = atual + custo[P[i]][P[i+1]] <- Adicione o custo de ir de um vértice para o próximo

atual = atual + custo[P[N-1]][P[0]] primeiro <- Adicione o custo de ir do último vértice ao

se atual < mínimo <- Atualizar o mínimo se necessário


mínimo = atual

saída mínima

Complexidade de tempo

Existem N! permutações a serem percorridas e o custo de cada caminho é calculado em O(N), portanto esse algoritmo leva O(N * N!) tempo para gerar a
resposta exata.

Seção 44.2: Algoritmo de programação dinâmica


Observe que se considerarmos o caminho (em ordem):

(1,2,3,4,6,0,5,7)

e o caminho

(1,2,3,5,0,6,7,4)

O custo de ir do vértice 1 para o vértice 2 para o vértice 3 permanece o mesmo, então por que ele deve ser recalculado? Este resultado pode ser salvo
para uso posterior.

Deixe dp[bitmask][vertex] representar o custo mínimo de viajar por todos os vértices cujo bit correspondente em bitmask é definido como 1 terminando
no vértice. Por exemplo:

dp[12][2]

12 = 1100
^^

vértices: 3 2 1 0

Como 12 representa 1100 em binário, dp[12][2] representa passar pelos vértices 2 e 3 no gráfico com o caminho terminando no vértice 2.

Notas de algoritmos do [Link] para profissionais 210


Machine Translated by Google

Assim podemos ter o seguinte algoritmo (implementação C++):

int custo[N][N]; int // Ajuste o valor de N se necessário


memo[1 << N][N]; int TSP(int // Defina tudo aqui como -1
bitmask, int pos){
int custo = INF;
se (máscara de bits == ((1 << N) - 1)){ retornar // Todos os vértices foram explorados
custo[pos][0]; // Custo para voltar
}
se (memo[bitmask][pos] != -1){ retornar // Se isso já foi calculado
memo[bitmask][pos]; // Apenas retorne o valor, não há necessidade de recalcular
}
para (int i = 0; i < N; ++i){ // Para cada vértice
if ((bitmask & (1 << i)) == 0){ //Se o vértice não foi visitado
cost = min(cost,TSP(bitmask | (1 << i) , i) + cost[pos][i]); //Visite o vértice
}
}
memo[bitmask][pos] = custo; retornar // Salvar o resultado
custo;
}
// Chamar TSP(1,0)

Esta linha pode ser um pouco confusa, então vamos analisá-la lentamente:

custo = min(custo,TSP(bitmask | (1 << i) , i) + custo[pos][i]);

Aqui, bitmask | (1 << i) define o bit iésimo de bitmask para 1, o que representa que o vértice iésimo foi visitado. O
i depois da vírgula representa a nova posição naquela chamada de função, que representa o novo "último" vértice.
cost[pos][i] é adicionar o custo de viajar do vértice pos ao vértice i.

Assim, esta linha é para atualizar o valor do custo para o valor mínimo possível de viajar para cada outro vértice que
ainda não foi visitado.

Complexidade de tempo

A função TSP(bitmask,pos) tem 2^N valores para bitmask e N valores para pos. Cada função leva O(N) tempo para

execute (o loop for ). Portanto, essa implementação leva tempo O(N^2 * 2^N) para gerar a resposta exata.

Notas de algoritmos do [Link] para profissionais 211


Machine Translated by Google

Capítulo 45: Problema da mochila


Seção 45.1: Noções básicas sobre o problema da mochila
O problema: Dado um conjunto de itens em que cada item contém um peso e um valor, determine o número de cada um
a ser incluído em uma coleção para que o peso total seja menor ou igual a um determinado limite e o valor total seja o
maior possível.

Pseudocódigo para o Problema da Mochila

Dado:

1. Valores(matriz v)
2. Pesos(matriz w)
3. Número de itens distintos(n)
4. Capacidade (W)

para j de 0 a W faça: m[0, j] := 0


para i de 1 a n faça:

para j de 0 a W faça: se w[i] > j


então: m[i, j] := m[i-1, j]
senão:

m[i, j] := max(m[i-1, j], m[i-1, jw[i]] + v[i])

Uma implementação simples do pseudocódigo acima usando Python:

def knapSack(W, peso, val, n):


K = [[0 para x no intervalo(W+1)] para x no intervalo(n+1)] para i no intervalo(n+1):
para w no intervalo(W+1): se
i==0 ou w==0:

K[i][w] = 0 elif
wt[i-1] <= w:
K[i][w] = max(val[i-1] + K[i-1][w-wt[i-1]], K[i-1][w]) senão:

K[i][w] = K[i-1][w]
retornar K[n][W] val =
[60, 100, 120] wt = [10, 20, 30]

W = 50
n = len(val)
print(knapSack(W, wt, val, n))

Executando o código: Salve isso em um arquivo chamado [Link]

$ python [Link] 220

Complexidade de tempo do código acima: O(nW) , onde n é o número de itens e W é a capacidade da mochila.

Seção 45.2: Solução implementada em C#

classe pública KnapsackProblem {

Notas de algoritmos do [Link] para profissionais 212


Machine Translated by Google

privado estático int Mochila(int w, int[] peso, int[] valor, int n) {

int i;
int[,] k = novo int[n + 1, w + 1]; para (i = 0; i
<= n; i++) {

int b;
para (b = 0; b <= w; b++) {

se (i==0 || b==0) {

k[i, b] = 0;

} senão se (peso[i - 1] <= b) {

k[i, b] = [Link](valor[i - 1] + k[i - 1, b - peso[i - 1]], k[i - 1, b]);

}
outro {
k[i, b] = k[i - 1, b];
}
}

} retornar k[n, w];


}

public static int Main(int nItens, int[] pesos, int[] valores) {

int n = [Link];
return Mochila(nItens, pesos, valores, n);
}
}

Notas de algoritmos do [Link] para profissionais 213


Machine Translated by Google

Capítulo 46: Resolução de equações

Seção 46.1: Equação Linear


Existem duas classes de métodos para resolver equações lineares:

1. Métodos diretos: As características comuns dos métodos diretos são que eles transformam a equação original em equações
equivalentes que podem ser resolvidas mais facilmente, o que significa que podemos resolver diretamente uma equação.

2. Método Iterativo: Métodos Iterativos ou Indiretos, começam com um palpite da solução e então refinam repetidamente a solução até que
um certo critério de convergência seja alcançado. Métodos iterativos são geralmente menos eficientes do que métodos diretos porque
um grande número de operações são necessárias. Exemplo- Método de Iteração de Jacobi, Método de Iteração de Gauss-Seidal.

Implementação em C-

// Implementação do Método de Jacobi void


JacobisMethod(int n, double x[n], double b[n], double a[n][n]){ double Nx[n]; //forma modificada de variáveis
int rootFound=0; //flag

int i, j;
enquanto(!rootFound)
{ para(i=0; i<n; i++){ Nx[i]=b[i]; // cálculo

para(j=0; j<n; j++){ se(i!=j)


Nx[i] = Nx[i]-a[i][j]*x[j];
}
Nx[i] = Nx[i] / a[i][i];
}

raizEncontrado=1; // verificação
para(i=0; i<n; i++){ se(!( (Nx[i]-
x[i])/x[i] > -0,000001 && (Nx[i]-x[i])/x[i] < 0,000001 )){
raizEncontrado=0;
quebrar;
}
}

para(i=0; i<n; i++){ x[i]=Nx[i]; // avaliação

}
}

retornar ;
}

// Implementação do Método Gauss-Seidal void


GaussSeidalMethod(int n, double x[n], double b[n], double a[n][n]){ double Nx[n]; //forma modificada de variáveis
int rootFound=0; //flag

int i, j;
para(i=0; i<n; i++){ // inicialização
Nx[i]=x[i];
}

Notas de algoritmos do [Link] para profissionais 214


Machine Translated by Google

enquanto(!rootFound)
{ para(i=0; i<n; i++){ Nx[i]=b[i]; // cálculo

para(j=0; j<n; j++){ se(i!=j) Nx[i]


= Nx[i]-a[i][j]*Nx[j];
}
Nx[i] = Nx[i] / a[i][i];
}

raizEncontrado=1; // verificação
para(i=0; i<n; i++){ se(!( (Nx[i]-
x[i])/x[i] > -0,000001 && (Nx[i]-x[i])/x[i] < 0,000001 )){
raizEncontrado=0;
quebrar;
}
}

para(i=0; i<n; i++){ x[i]=Nx[i]; // avaliação

}
}

retornar ;
}

// Imprimir array com separação por vírgula void print(int


n, double x[n]){ int i; for(i=0; i<n; i++){ printf("%lf, ",
x[i]);

} printf("\n\n");

retornar ;
}

int main(){ //
inicialização da equação // número de
variáveis int n=3;

duplo x[n]; // variáveis

duplo b[n], a[n][n]; // constantes //


argumentos

// atribuir valores a[0]


[0]=8; a[0][1]=2; a[0][2]=-2; b[0]=8; a[1][0]=1; a[1][1]=-8; a[1][2]=3; //8xÿ+2xÿ-2xÿ+8=0
b[1]=-4; //xÿ-8xÿ+3xÿ-4=0 a[2][0]=2; a[2][1]=1; a[2][2]=9; b[2]=12;
// 2xÿ+xÿ+9xÿ+12=0

int eu;

para(i=0; i<n; i++){ x[i]=0; // inicialização

}
Método Jacobis(n, x, b, a); imprimir(n, x);

para(i=0; i<n; i++){ // inicialização

Notas de algoritmos do [Link] para profissionais 215


Machine Translated by Google

x[i]=0;
}
Método GaussSeidal(n, x, b, a); imprimir(n, x);

retornar 0;
}

Seção 46.2: Equação não linear


Uma equação do tipo f(x)=0 é algébrica ou transcendental. Esses tipos de equações podem ser resolvidos usando dois tipos de métodos-

1. Método Direto: Este método fornece o valor exato de todas as raízes diretamente em um número finito de etapas.

2. Método indireto ou iterativo: os métodos iterativos são mais adequados para programas de computador para resolver um
equação. Baseia-se no conceito de aproximação sucessiva. No Método Iterativo, há duas maneiras de resolver uma equação:

Método de Bracketing: Pegamos dois pontos iniciais onde a raiz está entre eles. Exemplo-Método de Bissecção,
Método de Posição Falsa.

Método de Extremidade Aberta: Tomamos um ou dois valores iniciais onde a raiz pode estar em qualquer lugar. Exemplo-
Método de Newton-Raphson, Método de Aproximação Sucessiva, Método Secante.

Implementação em C:

/// Aqui defina diferentes funções para trabalhar com #define f(x) ( ((x)*(x)*(x))
- (x) - 2 ) #define f2(x) ( (3*(x)*(x)) - 1 ) #define g(x) ( cbrt( (x) +
2))

/ ** *
Recebe dois valores iniciais e encurta a distância em ambos os lados. **/ double BisectionMethod()

{ double root=0;

duplo a=1, b=2; duplo


c=0;

int loopCounter=0; se(f(a)*f(b)


< 0){ enquanto(1)
{ loopCounter+
+; c=(a+b)/2;

se(f(c)<0,00001 && f(c)>-0,00001){


raiz=c;
quebrar;
}

se((f(a))*(f(c)) < 0){ b=c; }senão{

uma=c;

Notas de algoritmos do [Link] para profissionais 216


Machine Translated by Google

} printf("Foram necessários %d loops.\n", loopCounter);

retornar raiz;
}

/ ** *
Recebe dois valores iniciais e encurta a distância por um lado. **/ double FalsePosition(){ double root=0;

duplo a=1, b=2; duplo c=0;

int loopCounter=0; se(f(a)*f(b)


< 0){ enquanto(1){ loopCounter+
+;

c=(a*f(b) - b*f(a)) / (f(b) - f(a));

/ */ printf("%lf\t %lf \n", c, f(c));/ **//// teste se(f(c)<0,00001 && f(c)>-0,00001)


{ root=c; quebrar;

se((f(a))*(f(c)) < 0){ b=c; }senão{ a=c;

}
}

} printf("Foram necessários %d loops.\n", loopCounter);

retornar raiz;
}

/ ** *
Usa um valor inicial e gradualmente aproxima esse valor do real. **/ double NewtonRaphson(){ double root=0;

duplo x1=1; duplo


x2=0;

int loopCounter=0; enquanto(1)


{ loopCounter+
+;

x2 = x1 - (f(x1)/f2(x1)); /*/ printf("%lf \t %lf


\n", x2, f(x2));/ **//// teste

se(f(x2)<0,00001 && f(x2)>-0,00001){ raiz=x2; quebrar;

Notas de algoritmos do [Link] para profissionais 217


Machine Translated by Google

x1=x2;

} printf("Foram necessários %d loops.\n", loopCounter);

retornar raiz;
}

/ ** *
Usa um valor inicial e gradualmente aproxima esse valor do real. **/ double FixedPoint(){ double root=0; double x=1;

int loopCounter=0; enquanto(1)


{ loopCounter+
+;

se( (xg(x)) <0,00001 && (xg(x)) >-0,00001){


raiz = x;
quebrar;
}

/ */ printf("%lf \t %lf \n", g(x), x-(g(x)));/ **//// teste

x=g(x);

} printf("Foram necessários %d loops.\n", loopCounter);

retornar raiz;
}

/ ** *
usa dois valores iniciais e ambos os valores se aproximam da raiz. **/ double Secant(){ double

root=0;

duplo x0=1; duplo


x1=2; duplo x2=0;

int loopCounter=0; enquanto(1)


{ loopCounter+
+;

/ */ printf("%lf \t %lf \t %lf \n", x0, x1, f(x1));/ **//// teste

se(f(x1)<0,00001 && f(x1)>-0,00001){ raiz=x1; quebrar;

x2 = ((x0*f(x1))-(x1*f(x0))) / (f(x1)-f(x0));

x0=x1;
x1=x2;

} printf("Foram necessários %d loops.\n", loopCounter);

retornar raiz;
}

Notas de algoritmos do [Link] para profissionais 218


Machine Translated by Google

int principal(){
raiz dupla ;

root = BisectionMethod();
printf("Usando o Método Bisection a raiz é: %lf \n\n", root);

root = FalsePosition();
printf("Usando o método False Position, a raiz é: %lf \n\n", root);

root = NewtonRaphson();
printf("Usando o método Newton-Raphson a raiz é: %lf \n\n", root);

root = FixedPoint();
printf("Usando o Método de Ponto Fixo a raiz é: %lf \n\n", root);

root = Secant();
printf("Usando o Método Secant a raiz é: %lf \n\n", root);

retornar 0;
}

Notas de algoritmos do [Link] para profissionais 219


Machine Translated by Google

Capítulo 47: Maior Comum


Subsequência
Seção 47.1: Explicação da maior subsequência comum
Uma das implementações mais importantes da Programação Dinâmica é descobrir o Maior Comprimento Comum
Subsequência. Vamos primeiro definir algumas das terminologias básicas.

Subsequência:

Uma subsequência é uma sequência que pode ser derivada de outra sequência pela exclusão de alguns elementos sem
alterando a ordem dos elementos restantes. Digamos que temos uma string ABC. Se apagarmos zero ou um ou mais de
um caractere desta string obtemos a subsequência desta string. Portanto, as subsequências da string ABC serão
{"A", "B", "C", "AB", "AC", "BC", "ABC", " "}. Mesmo se removermos todos os caracteres, a string vazia também será uma
subsequência. Para descobrir a subsequência, para cada caractere em uma string, temos duas opções - ou pegamos o
personagem, ou não. Então, se o comprimento da string é n, há 2n subsequências dessa string.

Maior subsequência comum:

Como o nome sugere, de todas as subsequências comuns entre duas strings, a maior subsequência comum (LCS)
é aquele com o comprimento máximo. Por exemplo: As subsequências comuns entre "HELLOM" e "HMLD"
são "H", "HL", "HM" etc. Aqui "HLL" é a maior subsequência comum que tem comprimento 3.

Método de Força Bruta:

Podemos gerar todas as subsequências de duas strings usando backtracking. Então podemos compará-las para descobrir a
subsequências comuns. Depois, precisaremos descobrir aquela com o comprimento máximo. Já vimos que,
há 2n subsequências de uma sequência de comprimento n. Levaria anos para resolver o problema se nosso n cruzasse 20-25.

Método de Programação Dinâmica:

Vamos abordar nosso método com um exemplo. Suponha que temos duas strings abcdaf e acbcf. Vamos denotar
estes com s1 e s2. Então a maior subsequência comum dessas duas strings será "abcf", que tem comprimento 4.
Novamente, lembro a você, subsequências não precisam ser contínuas na string. Para construir "abcf", ignoramos "da" em s1
e "c" em s2. Como descobrimos isso usando Programação Dinâmica?

Começaremos com uma tabela (uma matriz 2D) tendo todos os caracteres de s1 em uma linha e todos os caracteres de s2 em uma coluna.
Aqui a tabela é indexada em 0 e colocamos os caracteres de 1 em diante. Percorreremos a tabela da esquerda para a direita
para cada linha. Nossa tabela ficará assim:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | um | b | c | d | um | e |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | um | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|e| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Notas de algoritmos do [Link] para profissionais 220


Machine Translated by Google

5|e| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Aqui, cada linha e coluna representam o comprimento da maior subsequência comum entre duas strings se
pegue os caracteres dessa linha e coluna e adicione ao prefixo antes dela. Por exemplo: Table[2][3] representa o
comprimento da maior subsequência comum entre "ac" e "abc".

A coluna 0-ésima representa a subsequência vazia de s1. Da mesma forma, a linha 0-ésima representa a subsequência vazia
subsequência de s2. Se pegarmos uma subsequência vazia de uma string e tentarmos combiná-la com outra string, não importa
quanto tempo o comprimento da segunda substring é, a subsequência comum terá comprimento 0. Então podemos preencher o 0-
ª linhas e 0ª colunas com 0's. Obtemos:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | um | b | c | d | um | e |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | um | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|e|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Vamos começar. Quando estamos preenchendo a Tabela[1][1], estamos nos perguntando, se tivéssemos uma string a e outra string a e
nada mais, qual será a maior subsequência comum aqui? O comprimento do LCS aqui será 1. Agora vamos
veja a Tabela[1][2]. Temos a string ab e a string a. O comprimento do LCS será 1. Como você pode ver, o resto do
os valores também serão 1 para a primeira linha, pois considera apenas a string a com abcd, abcda, abcdaf. Portanto, nossa tabela ficará
como:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | um | b | c | d | um | e |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | um | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|e|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Para a linha 2, que agora incluirá c. Para a Tabela[2][1] temos ac de um lado e a do outro lado. Então o comprimento de
o LCS é 1. De onde tiramos esse 1? Do topo, que denota o LCS entre duas substrings. Então o que
estamos dizendo que, se s1[2] e s2[1] não forem iguais, então o comprimento do LCS será o máximo do comprimento de

221
Notas de algoritmos do [Link] para profissionais
Machine Translated by Google

LCS no topo, ou à esquerda. Tirar o comprimento do LCS no topo denota que não pegamos a corrente
personagem de s2. Da mesma forma, tomar o comprimento do LCS à esquerda denota que não tomamos a corrente
personagem de s1 para criar o LCS. Obtemos:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | um | b | c | d | um | e |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | um | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0|1| | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|e|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Então nossa primeira fórmula será:

se s2[i] não for igual a s1[j]


Tabela[i][j] = max(Tabela[i-1][j], Tabela[i][j-1]
fim se

Seguindo em frente, para a Tabela[2][2] temos as strings ab e ac. Como c e b não são iguais, colocamos o máximo do topo ou
deixado aqui. Neste caso, é novamente 1. Depois disso, para a Tabela[2][3] temos as strings abc e ac. Desta vez, os valores atuais de
ambas as linhas e colunas são iguais. Agora o comprimento do LCS será igual ao comprimento máximo do LCS até agora + 1.
Como obtemos o comprimento máximo do LCS até agora? Verificamos o valor diagonal, que representa a melhor correspondência
entre ab e a. Deste estado, para os valores atuais, adicionamos mais um caractere a s1 e s2 que

aconteceu de ser o mesmo. Então o comprimento do LCS irá, é claro, aumentar. Colocaremos 1 + 1 = 2 na Tabela[2][3]. Obtemos,

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | um | b | c | d | um | e |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | um | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0|1| 1 |2| | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|e|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Então nossa segunda fórmula será:

se s2[i] for igual a s1[j]


Tabela[i][j] = Tabela[i-1][j-1] + 1
fim se

Notas de algoritmos do [Link] para profissionais 222


Machine Translated by Google

Definimos ambos os casos. Usando essas duas fórmulas, podemos preencher a tabela inteira. Após preencher o
tabela, ficará assim:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | um | b | c | d | um | e |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | um | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0|1| 1 |2|2|2|2|
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0|1|2|2|2|2|
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0|1|2|3|3|3|3|
+-----+-----+-----+-----+-----+-----+-----+-----+
5|e|0|1|2|3|3|3|4|
+-----+-----+-----+-----+-----+-----+-----+-----+

O comprimento do LCS entre s1 e s2 será Tabela[5][6] = 4. Aqui, 5 e 6 são o comprimento de s2 e s1


respectivamente. Nosso pseudocódigo será:

Procedimento LCSlength(s1, s2):


Tabela[0][0] = 0
para i de 1 a [Link]
Tabela[0][i] = 0
fim para
para i de 1 a [Link]
Tabela[i][0] = 0
fim para
para i de 1 a [Link]
para j de 1 a [Link]
se s2[i] for igual a s1[j]
Tabela[i][j] = Tabela[i-1][j-1] + 1
outro
Tabela[i][j] = max(Tabela[i-1][j], Tabela[i][j-1])
fim se
fim para
fim para
Retornar Tabela[[Link]][[Link]]

A complexidade de tempo para este algoritmo é: O(mn), onde m e n denotam o comprimento de cada string.

Como descobrimos a maior subsequência comum? Começaremos pelo canto inferior direito. Verificaremos
de onde o valor está vindo. Se o valor está vindo da diagonal, isto é, se Table[i-1][j-1] for igual a
Tabela[i][j] - 1, empurramos s2[i] ou s1[j] (ambos são iguais) e movemos diagonalmente. Se o valor estiver vindo de cima,
isso significa que, se Table[i-1][j] for igual a Table[i][j], movemos para o topo. Se o valor estiver vindo da esquerda, isso significa que, se
Table[i][j-1] é igual a Table[i][j], movemos para a esquerda. Quando alcançamos a coluna mais à esquerda ou mais acima, nossa busca
termina. Então, retiramos os valores da pilha e os imprimimos. O pseudocódigo:

Procedimento PrintLCS(LCSlength, s1, s2)


temp := comprimento LCS
S = pilha()
i := [Link]
j := [Link]
enquanto i não é igual a 0 e j não é igual a 0
se Tabela[i-1][j-1] == Tabela[i][j] - 1 e s1[j]==s2[i]

Notas de algoritmos do [Link] para profissionais 223


Machine Translated by Google

[Link](s1[j]) //ou [Link](s2[i]) i := i - 1 j := j - 1 senão se


Tabela[i-1][j] ==

Tabela[i][j]

i := i-1 senão

j := j-1 endif
endwhile
enquanto S

não estiver vazio print([Link])


endwhile

Ponto a ser observado: se Table[i-1][j] e Table[i][j-1] forem iguais a Table[i][j] e Table[i-1][j-1] não for igual a Table[i][j] -
1, pode haver dois LCS para esse momento. Este pseudocódigo não considera essa situação. Você terá que resolver isso
recursivamente para encontrar vários LCSs.

A complexidade de tempo para este algoritmo é: O(max(m, n)).

Notas de algoritmos do [Link] para profissionais 224


Machine Translated by Google

Capítulo 48: Aumento mais longo


Subsequência
Seção 48.1: Informações básicas sobre a maior subsequência
crescente

A maior subsequência crescente o problema é encontrar subsequência da sequência de entrada fornecida na qual os elementos da
subsequência são classificados da ordem mais baixa para a mais alta. Todas as subsequências não são contíguas ou únicas.

Aplicação da Maior Subsequência Crescente:

Algoritmos como Longest Crescente Subsequência, Longest Comum Subsequência são usados em sistemas de controle de versão
como Git e etc.

Forma simples do algoritmo:

1. Encontre linhas únicas que sejam comuns a ambos os documentos.


2. Pegue todas essas linhas do primeiro documento e ordene-as de acordo com sua aparência no segundo
documento.

3. Calcule o LIS da sequência resultante (fazendo uma classificação de paciência), obtendo a sequência de correspondência mais longa
de linhas, uma correspondência entre as linhas de dois documentos.
4. Repita o algoritmo em cada intervalo de linhas entre as já correspondidas.

Agora, vamos considerar um exemplo mais simples do problema LCS. Aqui, a entrada é apenas uma sequência de inteiros distintos
a1,a2,...,an., e queremos encontrar a maior subsequência crescente nela. Por exemplo, se a entrada for 7,3,8,4,2,6 , então a maior
subsequência crescente é 3,4,6.

A abordagem mais fácil é classificar os elementos de entrada em ordem crescente e aplicar o algoritmo LCS às sequências originais e
classificadas. No entanto, se você olhar para o array resultante, notará que muitos valores são os mesmos e o array parece muito repetitivo.
Isso sugere que o problema LIS (longest increasing subsequence) pode ser feito com algoritmo de programação dinâmica usando
apenas array unidimensional.

Pseudocódigo:

1. Descreva uma matriz de valores que queremos calcular.


Para 1 <= i <= n, seja A(i) o comprimento de uma sequência crescente mais longa de entrada. Note que o comprimento
em que estamos interessados é max{A(i)|1 ÿ i ÿ n}.
2. Dê uma recorrência.

Para 1 <= i <= n, A(i) = 1 + max{A(j)|1 ÿ j < i e input(j) < input(i)}.


3. Calcule os valores de A.
4. Encontre a solução ideal.

O programa a seguir usa A para calcular uma solução ótima. A primeira parte calcula um valor m tal que A(m) é o comprimento de uma
subsequência crescente ótima de entrada. A segunda parte calcula uma subsequência crescente ótima, mas por conveniência,
imprimimos na ordem inversa. Este programa roda em tempo O(n), então o algoritmo inteiro roda em tempo O(n^2).

Parte 1:

mÿ1
para i : 2..n se A(i)

> A(m) então

Notas de algoritmos do [Link] para profissionais 225


Machine Translated by Google

m ÿ eu
fim se
fim para

Parte 2:

coloque
um tempo A(m) > 1 eu ÿ mÿ1

enquanto not(ai < am e A(i) = A(m)ÿ1) faça


eu ÿ iÿ1
fim enquanto
m ÿ eu

acabar
com isso enquanto

Solução recursiva:

Abordagem 1:

LIS(A[1..n]): se (n =
0) então retorne 0 m = LIS(A[1..(n ÿ
1)])
B é uma subsequência de A[1..(n ÿ 1)] com apenas elementos menores que a[n] (* seja h o tamanho de B, h
ÿ n-1 *) m = max(m, 1 + LIS(B[1..h]))

Saída m

Complexidade de tempo na abordagem 1: O(n*2^n)

Abordagem 2:

LIS(A[1..n], x): se (n = 0)
então retorne 0 m = LIS(A[1..(n ÿ 1)],
x) se (A[n] < x) então m = max(m, 1 +
LIS(A[1..(n ÿ 1)], A[n]))

Saída m

PRINCIPAL(A[1..n]):
retorna LIS(A[1..n], ÿ)

Complexidade de tempo na abordagem 2: O(n^2)

Abordagem 3:

LIS(A[1..n]): se (n =
0) retornar 0 m = 1

para i = 1 para n ÿ 1 faça


se (A[i] < A[n]) então m = max(m,
1 + LIS(A[1..i]))
retornar m

PRINCIPAL(A[1..n]):
retorna LIS(A[1..i])

Notas de algoritmos do [Link] para profissionais 226


Machine Translated by Google

Complexidade de tempo na abordagem 3: O(n^2)

Algoritmo Iterativo:

Calcula os valores iterativamente de baixo para cima.

LIS(A[1..n]): Matriz
L[1..n]
(* L[i] = valor da terminação LIS (A[1..i]) *) para i = 1 para n faça
L[i] = 1 para j = 1 para i ÿ 1
faça

se (A[j] < A[i]) faça


L[i] = máx(L[i], 1 + L[j])
retornar L

MAIN(A[1..n]): L =
LIS(A[1..n]) retorna o
valor máximo em L

Complexidade de tempo na abordagem iterativa: O(n^2)

Espaço Auxiliar: O(n)

Vamos pegar {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15} como entrada. Então, a Subsequência Crescente Mais Longa para a
entrada dada é {0, 2, 6, 9, 11, 15}.

Notas de algoritmos do [Link] para profissionais 227


Machine Translated by Google

Capítulo 49: Verifique se duas strings são anagramas

Duas strings com o mesmo conjunto de caracteres são chamadas de anagramas. Usei javascript aqui.

Criaremos um hash de str1 e aumentaremos a contagem em +1. Faremos um loop na 2ª string e verificaremos se todos os caracteres estão lá no
hash e diminuiremos o valor da chave hash. Verificaremos se todos os valores da chave hash são zero, será um anagrama.

Seção 49.1: Entrada e saída de amostra


Exemplo 1:

deixe str1 = 'stackoverflow'; deixe


str2 = 'flowerovstack';

Essas sequências são anagramas.

// Cria Hash a partir de str1 e aumenta uma contagem.

hashMap = { s : 1,
t : 1, a :
1, c : 1,
k : 1, o :
2, v : 1,
e : 1, r :
1, f : 1, l :
1, w : 1

Você pode ver que a hashKey 'o' contém o valor 2 porque o é 2 vezes na string.

Agora faça um loop em str2 e verifique se cada caractere está presente no hashMap. Se sim, diminua o valor da chave hashMap, caso contrário,
retorne false (o que indica que não é um anagrama).

hashMap =
{ s : 0,
t : 0, a :
0, c : 0,
k : 0,
o : 0,
v : 0,
e : 0, r :
0, f : 0,
l : 0,

em : 0
}

Agora, faça um loop no objeto hashMap e verifique se todos os valores são zero na chave do hashMap.

Notas de algoritmos do [Link] para profissionais 228


Machine Translated by Google

No nosso caso, todos os valores são zero, então é um anagrama.

Seção 49.2: Código genérico para anagramas


(função(){

var hashMap = {};

função éAnagrama (str1, str2) {

se([Link] !== [Link]){ retornar falso;

// Cria um mapa hash do caractere str1 e aumenta o valor em um (+1).


createStr1HashMap(str1);

// Verifica se o caractere str2 é a chave no mapa de hash e diminui o valor em um (-1); var valueExist
= createStr2HashMap(str2);

// Verifique se todos os valores das chaves hashMap são zero, então será um anagrama.
return isStringsAnagram(valueExist);
}

função createStr1HashMap (str1) { [].[Link](str1,


função(valor, índice, matriz){ hashMap[valor] = valor em hashMap ?
(hashMap[valor] + 1) : 1; valor de retorno ;

});
}

função createStr2HashMap (str2) {


var valueExist = [].[Link](str2, função(valor, índice, matriz){
if(valor em hashMap)
{ hashMap[valor] = hashMap[valor] - 1;

} valor de retorno em hashMap;


});
valor de retornoExist ;
}

função isStringsAnagram (valueExist) { if(!valueExist)


{ return valueExist; }
else { var isAnagram;
for(var i
em hashMap) {

se(hashMap[i] !== 0)
{ éAnagrama = falso;

quebrar; }
senão { éAnagrama = verdadeiro;
}
}

retornar éAnagrama;
}
}

isAnagram('stackoverflow', 'flowerovstack'); // verdadeiro


isAnagram('stackoverflow', 'flowervvstack'); // falso

Notas de algoritmos do [Link] para profissionais 229


Machine Translated by Google

})();

Complexidade de tempo: 3n ou seja, O(n).

Notas de algoritmos do [Link] para profissionais 230


Machine Translated by Google

Capítulo 50: Triângulo de Pascal


Seção 50.1: Triângulo de Pascal em C
int i, espaço, linhas, k=0, contagem = 0, contagem1 = 0; linha=5;
para(i=1;
i<=linhas; ++i) {

para(espaço=1; espaço <= linhas-i; ++espaço) {

printf(" "); +
+contagem;
}

enquanto(k != 2*i-1) {

se (contagem <= linhas-1) {

printf("%d ", i+k); ++contagem;

}
outro {
++contagem1;
printf("%d ", (i+k-2*contagem1));

} ++k;

} contagem1 = contagem = k = 0;

printf("\n");
}

Saída

1232
34543
4567654567
898765

Notas de algoritmos do [Link] para profissionais 231


Machine Translated by Google

Capítulo 51: Algoritmo: Imprima uma matriz m*n ao quadrado

Confira abaixo o exemplo de entrada e saída.

Seção 51.1: Exemplo de amostra

Entrada:

14 15 16 17 18 21 19
10 20 11 54 36 64 55
44 23 80 39 91 92 93
94 95 42

Saída:
imprimir valor no índice 14 15 16
17 18 21 36 39 42 95 94 93 92 91 64 19 10 20 11 54 80 23 44 55

ou imprimir índice 00
01 02 03 04 05 15 25 35 34 33 32 31 30 20 10 11 12 13 14 24 23 22 21

Seção 51.2: Escreva o código genérico


função noOfLooping(m,n) { if(m > n)
{ menorValor
= n; } senão { menorValor
= m;

se(menorValor % 2 == 0) { retorne
menorValor/2; } senão { retorne

(menorValor+1)/2;
}
}

função squarePrint(m,n) { var looping


= noOfLooping(m,n); para(var i = 0; i <
looping; i++) {
para(var j = i; j < m - 1 - i; j++) { [Link](i+''+j);

} para(var k = i; k < n - 1 - i; k++) { [Link](k+''+j);

} for(var l = j; l > i; l--) { [Link](k+''+l);

} para(var x = k; x > i; x--)


{ [Link](x+''+l);
}
}
}

squarePrint(6,4);

Notas de algoritmos do [Link] para profissionais 232


Machine Translated by Google

Capítulo 52: Exponenciação de matrizes


Seção 52.1: Exponenciação de matriz para resolver exemplo
Problemas
Encontre f(n): n-ésimo número de Fibonacci. O problema é bem fácil quando n é relativamente pequeno. Podemos usar recursão simples,
f(n) = f(n-1) + f(n-2), ou podemos usar a abordagem de programação dinâmica para evitar o cálculo da mesma função
repetidamente. Mas o que você fará se o problema disser, Dado 0 < n < 10ÿ, encontre f(n) mod 999983? Dinâmico
a programação falhará, então como lidamos com esse problema?

Primeiro, vamos ver como a exponenciação de matrizes pode ajudar a representar relações recursivas.

Pré-requisitos:

Dadas duas matrizes, saiba como encontrar o produto delas. Além disso, dada a matriz produto de duas matrizes, e
uma delas, saiba como encontrar a outra matriz.

Dada uma matriz de tamanho d X d, saiba como encontrar sua enésima potência em O(d3log(n)).

Padrões:

Primeiro precisamos de uma relação recursiva e queremos encontrar uma matriz M que possa nos levar ao estado desejado a partir de uma

conjunto de estados já conhecidos. Vamos supor que conhecemos os k estados de uma dada relação de recorrência e queremos
encontre o (k+1)ésimo estado. Seja M uma matriz k X k , e construímos uma matriz A:[k X 1] a partir dos estados conhecidos do

relação de recorrência, agora queremos obter uma matriz B:[k X 1] que representará o conjunto dos próximos estados, ou seja, MXA = B
como mostrado abaixo:

| f(n) | | f(n+1) |
| f(n-1) | | f(n) |
MX | f(n-2) | = | f(n-1) |
| ...... | ...... |
| f(nk) | |f(n-k+1)|

Então, se pudermos projetar M adequadamente, nosso trabalho estará feito! A matriz será então usada para representar a recorrência
relação.

Tipo 1:
Vamos começar com o mais simples, f(n) = f(n-1) + f(n-2)
Obtemos, f(n+1) = f(n) + f(n-1).
Vamos supor que conhecemos f(n) e f(n-1); Queremos descobrir f(n+1).
A partir da situação exposta acima, a matriz A e a matriz B podem ser formadas conforme mostrado abaixo:

Matriz A Matriz B

| f(n) | | f(n-1) | f(n+1) |


| | f(n) |

[Nota: A matriz A será sempre projetada de tal forma que, todo estado do qual f(n+1) depende, estará presente]
Agora, precisamos projetar uma matriz 2X2 M tal que ela satisfaça MXA = B, conforme declarado acima.
O primeiro elemento de B é f(n+1) que na verdade é f(n) + f(n-1). Para obter isso, da matriz A, precisamos de 1 X f(n) e 1
X f(n-1). Portanto, a primeira linha de M será [1 1].

| 1 1 | ----- | X | f(n) | = | f(n+1) |


| | f(n-1) | | ------ |

Notas de algoritmos do [Link] para profissionais 233


Machine Translated by Google

[Nota: ----- significa que não estamos preocupados com esse valor.]
Da mesma forma, o 2º item de B é f(n), que pode ser obtido simplesmente tomando 1 X f(n) de A, então a 2ª linha de M é [1 0].

| ----- | X | f(n) | = | ------ |


|1 0| | f(n-1) | | f(n) |

Então obtemos nossa matriz 2 X 2 desejada M.

|1 1 | X | f(n) | = | f(n+1) |
|1 0| | f(n-1) | | f(n) |

Essas matrizes são simplesmente derivadas usando multiplicação de matrizes.

Tipo 2:

Vamos tornar isso um pouco mais complexo: encontre f(n) = a X f(n-1) + b X f(n-2), onde a e b são constantes.
Isso nos diz que f(n+1) = a X f(n) + b X f(n-1).
Até aqui, deve estar claro que a dimensão das matrizes será igual ao número de dependências, ou seja,
neste exemplo em particular, novamente 2. Então, para A e B, podemos construir duas matrizes de tamanho 2 X 1:

Matriz A | f(n) Matriz B


| | f(n-1) | | f(n+1) |
| f(n) |

Agora para f(n+1) = a X f(n) + b X f(n-1), precisamos de [a, b] na primeira linha da matriz objetivo M. E para a 2ª
item em B, ou seja, f(n) já temos isso na matriz A, então apenas pegamos isso, o que leva, a 2ª linha da matriz M
para [1 0]. Desta vez obtemos:

| b | X | f(n) | = | f(n+1) |
um | 1 0 | | f(n-1) | | f(n) |

Bem simples, não é?

Tipo 3:

Se você sobreviveu até esse estágio, você ficou muito mais velho, agora vamos encarar uma relação um pouco complexa: encontre f(n) =
a X f(n-1) + c X f(n-3)?
Opa! Alguns minutos atrás, tudo o que víamos eram estados contíguos, mas aqui, o estado f(n-2) está faltando. Agora?

Na verdade isso não é mais um problema, podemos converter a relação da seguinte forma: f(n) = a X f(n-1) + 0 X f(n-2) +
c X f(n-3), deduzindo f(n+1) = a X f(n) + 0 X f(n-1) + c X f(n-2). Agora, vemos que, na verdade, esta é uma forma
descrito no Tipo 2. Então aqui a matriz objetiva M será 3 X 3, e os elementos são:

| um 0 c | | f(n) | | f(n+1) |
| 1 0 0 | X | f(n-1) | = | f(n) |
| 0 1 0 | | f(n-2) | | f(n-1) |

Eles são calculados da mesma forma que o tipo 2. Se você achar difícil, tente fazer isso em papel e caneta.

Tipo 4:

A vida está ficando complexa como o inferno, e o Sr. Problema agora pede que você encontre f(n) = f(n-1) + f(n-2) + c onde c é qualquer
constante.

Agora, isso é algo novo e tudo o que vimos no passado, após a multiplicação, cada estado em A se transforma em seu próximo

Notas de algoritmos do [Link] para profissionais 234


Machine Translated by Google

estado em B.

f(n) = f(n-1) + f(n-2) + c


f(n+1) = f(n) + f(n-1) + c
f(n+2) = f(n+1) + f(n) + c
.................... breve

Então, normalmente não conseguimos fazer isso da maneira anterior, mas que tal adicionarmos c como um estado:

| f(n) | | f(n+1) |
MX | f(n-1) | = | f(n) |
| c || | c

Agora, não é muito difícil projetar M. Veja como é feito, mas não se esqueça de verificar:

|11 1 | | f(n) | | f(n+1) |


| 1 0 0 | X | f(n-1) | = | f(n) |
|001|| | | c | c

Tipo 5:

Vamos colocar tudo junto: encontre f(n) = a X f(n-1) + c X f(n-3) + d X f(n-4) + e. Vamos deixar isso como um exercício para
você. Primeiro tente descobrir os estados e a matriz M. E verifique se ela corresponde à sua solução. Encontre também a matriz A e
B.

| um 0 cd 1 |
|10000|
|01000|
|00100|
|00001|

Tipo 6:

Às vezes a recorrência é dada assim:

f(n) = f(n-1) -> se n for ímpar


f(n) = f(n-2) -> se n for par

Resumidamente:

f(n) = (n&1) X f(n-1) + (!(n&1)) X f(n-2)

Aqui, podemos dividir as funções na base de ímpar e par e manter 2 matrizes diferentes para ambas e calcular
separadamente.

Tipo 7:

Sentindo-se pouco confiante demais? Bom para você. Às vezes, podemos precisar manter mais de uma recorrência, onde
eles estão interessados. Por exemplo, deixe uma recorrência re;atopm ser:

g(n) = 2g(n-1) + 2g(n-2) + f(n)

Aqui, a recorrência g(n) depende de f(n) e pode ser calculada na mesma matriz, mas de forma aumentada.
dimensões. A partir delas, vamos primeiro projetar as matrizes A e B.

Notas de algoritmos do [Link] para profissionais 235


Machine Translated by Google

Matriz A | Matriz B
g(n) | | g(n-1) | | | g(n+1) |
f(n+1) | | f(n) | | g(n) |
| f(n+2) |
| f(n+1) |

Aqui, g(n+1) = 2g(n-1) + f(n+1) e f(n+2) = 2f(n+1) + 2f(n). Agora, usando os processos declarados acima, nós
pode encontrar a matriz objetiva M como sendo:

|2210|
|1000|
|0022|
|0010|

Então, essas são as categorias básicas de relações de recorrência que são usadas para resolver por meio dessa técnica simples.

Notas de algoritmos do [Link] para profissionais 236


Machine Translated by Google

Capítulo 53: Algoritmo limitado em tempo polinomial para


cobertura mínima de vértices
Variável Significado
G Gráfico não direcionado conectado de entrada
X Conjunto de vértices

C Conjunto final de vértices

Este é um algoritmo polinomial para obter a cobertura mínima de vértices de um grafo não direcionado conectado. A complexidade de tempo deste
algoritmo é O(n2)

Seção 53.1: Pseudocódigo do algoritmo


Algoritmo PMinVertexCover (gráfico G)
Gráfico conectado de entrada G
Conjunto de cobertura de vértice mínimo de saída C

Definir C <- novo Conjunto<Vértice>()

Definir X <- novo Conjunto<Vértice>()

X <- [Link]()

para v em X faça
Lista<Vértice> adjacenteVertices1 <- [Link](v)

se !C contiver qualquer um dos verticesadjacentes1 então

[Link](v)

para vértice em C faça

Lista<vértice> adjacentVertices2 <- [Link](vértice)

se C contém qualquer um dos verticesadjacentes2 então

[Link](vértice)

retornar C

C é a cobertura mínima de vértices do grafo G

podemos usar a classificação por bucket para classificar os vértices de acordo com seu grau porque o valor máximo de graus é (n-1),
onde n é o número de vértices, então a complexidade de tempo da classificação será O(n)

Notas de algoritmos do [Link] para profissionais 237


Machine Translated by Google

Capítulo 54: Distorção Temporal Dinâmica

Seção 54.1: Introdução à distorção dinâmica do tempo


Distorção Temporal Dinâmica (DTW) é um algoritmo para medir a similaridade entre duas sequências temporais que podem
variam em velocidade. Por exemplo, semelhanças na caminhada podem ser detectadas usando DTW, mesmo se uma pessoa estivesse caminhando
mais rápido que o outro, ou se houve acelerações e desacelerações durante o curso de uma observação. Pode ser
usado para combinar um comando de voz de amostra com outros comandos, mesmo que a pessoa fale mais rápido ou mais devagar do que o
amostra de voz pré-gravada. O DTW pode ser aplicado a sequências temporais de dados de vídeo, áudio e gráficos - na verdade,
qualquer dado que possa ser transformado em uma sequência linear pode ser analisado com DTW.

Em geral, DTW é um método que calcula uma correspondência ótima entre duas sequências fornecidas com certas
restrições. Mas vamos nos ater aos pontos mais simples aqui. Digamos que temos duas sequências de voz Sample e Test, e
queremos verificar se essas duas sequências correspondem ou não. Aqui, a sequência de voz se refere ao sinal digital convertido de
sua voz. Pode ser a amplitude ou frequência da sua voz que denota as palavras que você diz. Vamos supor:

Amostra = {1, 2, 3, 5, 5, 5, 6}
Teste = {1, 1, 2, 2, 3, 5}

Queremos descobrir a correspondência ideal entre essas duas sequências.

Primeiro, definimos a distância entre dois pontos, d(x, y) onde x e y representam os dois pontos. Seja,

d(x, y) = |x - y| // diferença absoluta

Vamos criar uma matriz 2D Table usando essas duas sequências. Calcularemos as distâncias entre cada ponto de
Faça uma amostra com todos os pontos do teste e encontre a correspondência ideal entre eles.

+------+------+------+------+------+------+------+ ------+
| | 0| 1| 1| 2| 2|3| 5|
+------+------+------+------+------+------+------+ ------+
| 0| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 1| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 2| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 3| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5| | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 6| | | | | | | |
+------+------+------+------+------+------+------+ ------+

Aqui, a Tabela[i][j] representa a distância ótima entre duas sequências se considerarmos a sequência até
Amostra[i] e Teste[j], considerando todas as distâncias ótimas que observamos antes.

Para a primeira linha, se não pegarmos nenhum valor de Sample, a distância entre este e Test será infinita. Então colocamos
infinito na primeira linha. O mesmo vale para a primeira coluna. Se não pegarmos nenhum valor de Test, a distância entre este
um e Sample também será infinito. E a distância entre 0 e 0 será simplesmente 0. Obtemos,

Notas de algoritmos do [Link] para profissionais 238


Machine Translated by Google

+------+------+------+------+------+------+------+ ------+
| | 0| 1| 1| 2| 2|3| 5|
+------+------+------+------+------+------+------+ ------+
| 0| 0 | informações | informações | informações | informações | informações | informações |
+------+------+------+------+------+------+------+ ------+
| 1 | informações | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 2 | informações | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 3 | informações | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5 | informações | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5 | informações | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 5 | informações | | | | | | |
+------+------+------+------+------+------+------+ ------+
| 6 | informações | | | | | | |
+------+------+------+------+------+------+------+ ------+

Agora, para cada etapa, consideraremos a distância entre cada ponto em questão e adicionaremos o mínimo
distância que encontramos até agora. Isso nos dará a distância ótima de duas sequências até essa posição. Nossa fórmula
vai ser,

Tabela[i][j] := d(i, j) + min(Tabela[i-1][j], Tabela[i-1][j-1], Tabela[i][j-1])

Para o primeiro, d(1, 1) = 0, Table[0][0] representa o mínimo. Então o valor de Table[1][1] será 0 + 0 = 0. Para
o segundo, d(1, 2) = 0. Table[1][1] representa o mínimo. O valor será: Table[1][2] = 0 + 0 = 0. Se
continue assim, após terminar a tabela ficará assim:

+------+------+------+------+------+------+------+ ------+
| | 0| 1| 1| 2| 2|3| 5|
+------+------+------+------+------+------+------+ ------+
| 0| 0 | informações | informações | informações | informações | informações | informações |
+------+------+------+------+------+------+------+ ------+
| 1 | informações | 0| 0| 1| 2|4|8|
+------+------+------+------+------+------+------+ ------+
| 2 | informações | 1| 1| 0|0| 1|4|
+------+------+------+------+------+------+------+ ------+
| 3 | informações | 3| 3| 1| 1|0| 2|
+------+------+------+------+------+------+------+ ------+
| 5 | informações | 7| 7|4|4| 2|0|
+------+------+------+------+------+------+------+ ------+
| 5 | informações | 11 | 11 | 7| 7|4|0|
+------+------+------+------+------+------+------+ ------+
| 5 | inf | 15 | 15 | 10 | 10 | 6 | 0 |
+------+------+------+------+------+------+------+ ------+
| 6 | inf | 20 | 20 | 14 | 14 | 9 | 1|
+------+------+------+------+------+------+------+ ------+

O valor na Tabela[7][6] representa a distância máxima entre essas duas sequências dadas. Aqui 1 representa
a distância máxima entre Amostra e Teste é 1.

Agora, se voltarmos do último ponto até o ponto inicial (0, 0) , obteremos uma longa linha que
move horizontalmente, verticalmente e diagonalmente. Nosso procedimento de backtracking será:

se Tabela[i-1][j-1] <= Tabela[i-1][j] e Tabela[i-1][j-1] <= Tabela[i][j-1]

Notas de algoritmos do [Link] para profissionais 239


Machine Translated by Google
eu := eu - 1
j := j - 1
senão se Tabela[i-1][j] <= Tabela[i-1][j-1] e Tabela[i-1][j] <= Tabela[i][j-1]
eu := eu - 1
senão
j := j - 1 fim se

Continuaremos até chegarmos a (0, 0). Cada movimento tem seu próprio significado:

Um movimento horizontal representa exclusão. Isso significa que nossa sequência de teste acelerou durante esse intervalo.
Um movimento vertical representa inserção. Isso significa que nossa sequência de teste desacelerou durante esse intervalo.
Um movimento diagonal representa correspondência. Durante esse período, Teste e Amostra foram os mesmos.

Nosso pseudocódigo será:

Procedimento DTW(Amostra, Teste): n :=


[Link] m :=
[Link] Criar
Tabela[n + 1][m + 1] para i de 1 a n
Tabela[i][0] := infinito

fim para
para i de 1 a m
Tabela[0][i] := fim infinito para

Tabela[0][0] := 0 para i de
1 a n para j de 1 a m

Tabela[i][j] := d(Amostra[i], Teste[j]) + mínimo(Tabela[i-1]


[j-1], //
Tabela[i][j-1], correspondência //
Tabela[i-1][j]) inserção // exclusão
fim para
fim para
Retornar Tabela[n + 1][m + 1]

Também podemos adicionar uma restrição de localidade. Ou seja, exigimos que se Sample[i] for correspondido com Test[j], então |i - j|
não seja maior que w, um parâmetro de janela.

Notas de algoritmos do [Link] para profissionais 240


Machine Translated by Google

Complexidade:

*
A complexidade do cálculo de DTW é O(m) . As n) onde m e n representam o comprimento de cada sequência. Mais rápido
técnicas para cálculo de DTW incluem PrunedDTW, SparseDTW e FastDTW.

Aplicações:

Reconhecimento de palavras faladas

Análise de Poder de Correlação

Notas de algoritmos do [Link] para profissionais 241


Machine Translated by Google

Capítulo 55: Transformada rápida de Fourier

A forma Real e Complexa da DFT (Discrete Fourier Transforms) pode ser usada para executar análise de frequência ou síntese para
quaisquer sinais discretos e periódicos. A FFT (Fast Fourier Transform) é uma implementação da DFT que pode ser executada
rapidamente em CPUs modernas.

Seção 55.1: FFT de base 2

O método mais simples e talvez mais conhecido para calcular a FFT é o algoritmo Radix-2 Decimation in Time.
A FFT Radix-2 funciona decompondo um sinal de domínio de tempo de N pontos em N sinais de domínio de tempo, cada um composto
de um único ponto

A decomposição do sinal, ou 'decimação no tempo' é obtida pela reversão de bits dos índices para a matriz de dados do domínio do
tempo. Assim, para um sinal de dezesseis pontos, a amostra 1 (Binário 0001) é trocada pela amostra 8 (1000), a amostra 2 (0010) é
trocada pela 4 (0100) e assim por diante. A troca de amostras usando a técnica de reversão de bits pode ser obtida simplesmente
em software, mas limita o uso da FFT Radix 2 a sinais de comprimento N = 2^M.

O valor de um sinal de 1 ponto no domínio do tempo é igual ao seu valor no domínio da frequência, portanto, essa matriz de pontos
de domínio de tempo único decompostos não requer transformação para se tornar uma matriz de pontos de domínio da frequência.
Os N pontos únicos; no entanto, precisam ser reconstruídos em um espectro de frequência de N pontos. A reconstrução ótima
do espectro de frequência completo é realizada usando cálculos de borboleta. Cada estágio de reconstrução na FFT Radix-2 realiza
um número de borboletas de dois pontos, usando um conjunto semelhante de funções de ponderação exponencial, Wn^R.

Notas de algoritmos do [Link] para profissionais 242


Machine Translated by Google

A FFT remove cálculos redundantes na Transformada Discreta de Fourier explorando a periodicidade de Wn^R.
A reconstrução espectral é concluída em estágios log2(N) de cálculos de borboleta, fornecendo X[K]; os dados de domínio de
frequência real e imaginário em forma retangular. Para converter para magnitude e fase (coordenadas polares), é necessário
encontrar o valor absoluto, ÿ(Re2 + Im2), e o argumento, tan-1(Im/Re).

O diagrama de fluxo borboleta completo para uma FFT Radix 2 de oito pontos é mostrado abaixo. Observe que os sinais de entrada
foram reordenados anteriormente de acordo com o procedimento de dizimação no tempo descrito anteriormente.

Notas de algoritmos do [Link] para profissionais 243


Machine Translated by Google

A FFT normalmente opera em entradas complexas e produz uma saída complexa. Para sinais reais, a parte imaginária pode ser definida
como zero e a parte real definida como o sinal de entrada, x[n], no entanto, muitas otimizações são possíveis envolvendo a transformação
de dados somente reais. Os valores de Wn^R usados durante a reconstrução podem ser determinados usando a equação de ponderação
exponencial.

O valor de R (o poder de ponderação exponencial) é determinado pelo estágio atual na reconstrução espectral e pelo cálculo atual dentro
de uma borboleta específica.

Exemplo de código (C/C++)

Exemplo de código AC/C++ para calcular o FFT Radix 2 pode ser encontrado abaixo. Esta é uma implementação simples que funciona
para qualquer tamanho N onde N é uma potência de 2. É aproximadamente 3x mais lento do que a implementação FFTw mais rápida, mas
ainda é uma base muito boa para otimização futura ou para aprender sobre como este algoritmo funciona.

#incluir <math.h>

#define PI 3.1415926535897932384626433832795 #define TWOPI // PI para cálculos de seno/cos


6.283185307179586476925286766559 #define Deg2Rad // 2*PI para cálculos de seno/cos
0.017453292519943295769236907684886 // Fator de graus para radianos #define Rad2Deg 57.295779513082320876798154814105 // Fator
de radianos para graus #define log10_2 0.30102999566398119521373889472449 // Log10 de 2 #define log10_2_INV
3.3219280948873623478703194294948 //1/Log10(2)

// estrutura de variável complexa (precisão dupla) struct complex { public:


double Re, Im;

// Não é tão complicado assim


};

// Retorna verdadeiro se N for uma potência de 2 bool


isPwrTwo(int N, int *M) {

*M = (int)ceil(log10((double)N) * log10_2_INV);// M é o número de estágios a serem executados. 2^M = N int NN = (int)pow(2.0, *M);

Notas de algoritmos do [Link] para profissionais 244


Machine Translated by Google

if ((NN != N) || (NN == 0)) // Verifique se N é uma potência de 2.


retornar falso;

retornar verdadeiro;
}

vazio rad2FFT(int N, complexo *x, complexo *DFT)


{
int M = 0;

// Verifique se é potência de dois. Se não, saia if (!


isPwrTwo(N, &M))
throw "Rad2FFT(): N deve ser uma potência de 2 para Radix FFT";

// Variáveis Inteiras

int BSep; int // BSep é o espaçamento de memória entre borboletas


BWidth; int P; // BWidth é o espaçamento de memória das extremidades opostas da borboleta
int j; int // P é o número de Wn's semelhantes a serem usados naquele estágio
estágio // j é usado em um loop para executar todos os cálculos em cada estágio
= 1; (1 a M). // stage é o número do estágio da FFT. Existem M estágios no total

int HiIndex; // HiIndex é o índice da matriz DFT para o valor superior de cada
cálculo borboleta
int sem sinal iaddr; int ii; int // bitmask para reversão de bits
MM1 = M // Campo de bits inteiro para reversão de bits (Decimação no Tempo)
- 1;

int não assinado i;


eu inteiro ;
int sem sinal nMax = ( int sem sinal)N;

// Variáveis de Precisão Dupla


duplo TwoPi_N = TWOPI / (duplo)N; duplo TwoPi_NP; // constante para economizar tempo computacional. = 2*PI / N

// Variáveis complexas (Veja 'struct complex')


WN complexo; // Wn é a função de ponderação exponencial na forma a + jb
TEMP complexo; // TEMP é usado para salvar a computação no cálculo da borboleta
complexo *pDFT = DFT; // Ponteiro para os primeiros elementos na matriz DFT
complexo *pLo; // Ponteiro para valor lo / hi de cálculos de borboleta
complexo *pHi;
complexo *pX; // Ponteiro para x[n]

// Dizimação no tempo - classificação de amostra x[n]


para (i = 0; i < nMax; i++, DFT++)
{
pX = x + i; // // Calcula o x[n] atual a partir do endereço base *x e do índice i.
ii = 0; Redefinir novo endereço para DFT[n]
iaddr = i; // Copiar i para manipulações
para (l = 0; l < M; l++) // Inverta o bit i e armazene em ii...
{
se (iaddr & 0x01) ii += (1 // Determinar o bit menos significativo
<< (MM1 - l)); // Incrementa ii por 2^(M-1-l) se lsb for 1
iaddr >>= 1; // deslocamento para a direita iaddr para testar o próximo bit. Use lógico
operações para aumento de velocidade
se (! iaddr)
quebrar;
}
DFT = pDFT + ii; índice // Calcula DFT[n] atual a partir do endereço base *pDFT e bit
reverso ii

Notas de algoritmos do [Link] para profissionais 245


Machine Translated by Google

DFT->Re = pX->Re; // Atualiza a matriz complexa com sinal de domínio de tempo classificado por endereço
x[n]
DFT->Im = pX->Im; // NB: O imaginário é sempre zero
}

// Cálculo FFT por cálculo de borboleta


para (estágio = 1; estágio <= M; estágio++) // Loop para M estágios, onde 2^M = N
{
BSep = (int)(pow(2, stage)); // Separação entre borboletas = 2^stage
P = N / BSep; // Wn's semelhantes neste estágio = N/Bsep
LarguraB = BSep / 2; // Largura da borboleta (espaçamento entre pontos opostos) = Separação /
2.

DoisPi_NP = DoisPi_N*P;

para (j = 0; j < BWidth; j++) // Loop para cálculos j por borboleta


{
se (j != 0) { // Economize no cálculo se R = 0, pois WN^0 = (1 + j0)

//[Link] = cos(TwoPi_NP*j)
[Link] = cos(DoisPi_N*P*j); // Calcular Wn (Real e Imaginário)
[Link] = -sin(DoisPi_N*P*j);
}

para (HiIndex = j; HiIndex < N; HiIndex += BSep) // Loop para HiIndex Etapa BSep
borboletas por estágio
{
pHi = pDFT + HiIndex; pLo // Apontar para um valor mais alto
= pHi + BWidth; para // Aponte para o valor mais baixo (Observe que o VC++ ajusta
espaçamento entre elementos)

se (j != 0) { // Se a potência exponencial não for zero...

//CMult(pLo, &WN, &TEMP); // Executa multiplicação complexa de Lovalue


com Wn
[Link] = (pLo->Re * [Link]) - (pLo->Im * [Link]);
[Link] = (pLo->Re * [Link]) + (pLo->Im * [Link]);

//CSub (pHi, &TEMP, pLo);


pLo->Re = pHi->Re - [Link]; pLo- // Encontre um novo Lovalue (subtração complexa)
>Im = pHi->Im - [Link];

//CAdd (pHi, &TEMP, pHi); pHi- // Encontre novo Hivalue (adição complexa)
>Re = (pHi->Re + [Link]);
pHi->Im = (pHi->Im + [Link]);
}
outro
{
[Link] = pLo->Re;
[Link] = pLo->Im;

//CSub (pHi, &TEMP, pLo);


pLo->Re = pHi->Re - [Link]; pLo- // Encontre um novo Lovalue (subtração complexa)
>Im = pHi->Im - [Link];

//CAdd (pHi, &TEMP, pHi); pHi- // Encontre novo Hivalue (adição complexa)
>Re = (pHi->Re + [Link]);
pHi->Im = (pHi->Im + [Link]);
}
}
}
}

Notas de algoritmos do [Link] para profissionais 246


Machine Translated by Google

pLo = 0; // Null todos os ponteiros


pHi = 0;
pDFT = 0;
DFT = 0;
pX = 0;
}

Seção 55.2: FFT inversa de base 2

Devido à forte dualidade da Transformada de Fourier, ajustar a saída de uma transformada direta pode produzir a FFT inversa. Dados no domínio
de frequência podem ser convertidos para o domínio de tempo pelo seguinte método:

1. Encontre o conjugado complexo dos dados do domínio da frequência invertendo o componente imaginário para todos
instâncias de K.

2. Execute a FFT direta nos dados do domínio de frequência conjugados.


3. Divida cada saída do resultado desta FFT por N para fornecer o valor real do domínio do tempo.
4. Encontre o conjugado complexo da saída invertendo o componente imaginário dos dados do domínio do tempo para
todas as instâncias de n.

Nota: tanto os dados de domínio de frequência quanto os de domínio de tempo são variáveis complexas. Normalmente, o componente
imaginário do sinal de domínio de tempo após uma FFT inversa é zero ou ignorado como erro de arredondamento. Aumentar a precisão das
variáveis de float de 32 bits para double de 64 bits ou double longo de 128 bits reduz significativamente os erros de arredondamento
produzidos por várias operações FFT consecutivas.

Exemplo de código (C/C++)

#incluir <math.h>

#define PI 3.1415926535897932384626433832795 #define TWOPI // PI para cálculos de seno/cos


6.283185307179586476925286766559 #define Deg2Rad // 2*PI para cálculos de seno/cos
0.017453292519943295769236907684886 // Fator de graus para radianos #define Rad2Deg 57.295779513082320876798154814105 // Fator
de radianos para graus #define log10_2 0.30102999566398119521373889472449 // Log10 de 2 #define log10_2_INV
3.3219280948873623478703194294948 //1/Log10(2)

// estrutura de variável complexa (precisão dupla) struct complex { public:


double Re, Im;

// Não é tão complicado assim


};

vazio rad2InverseFFT(int N, complexo *x, complexo *DFT) {

// M é o número de estágios a serem executados. 2^M = N double


Mx = (log10((double)N) / log10((double)2)); int a = (int)(ceil(pow(2.0, Mx)));
int status = 0; if (a != N) // Verifique se N é uma
potência de 2 {

x = 0;
DFT = 0;
throw "rad2InverseFFT(): N deve ser uma potência de 2 para FFT inversa de base 2";
}

complexo *pDFT = DFT; // Redefinir vetor para ponteiros DFT


complexo *pX = x; // Redefinir vetor para ponteiro x[n]
duplo NN = 1 / (duplo)N; // Fator de escala para a FFT inversa

Notas de algoritmos do [Link] para profissionais 247


Machine Translated by Google

para (int i = 0; i < N; i++, DFT++)


DFT->Im *= -1; // Encontre o conjugado complexo do espectro de frequência

DFT = pDFT; // Redefinir ponteiro de domínio de frequência


rad2FFT(N, DFT, x); // Calcula o FFT direto com variáveis trocadas (tempo e frequência)

int eu;
complexo* x;
para ( i = 0, x = pX; i < N; i++, x++){
x->Re *= NN; x- // Divida o domínio do tempo por N para uma escala de amplitude correta
>Im *= -1; // Alterar o sinal do ImX
}
}

Notas de algoritmos do [Link] para profissionais 248


Machine Translated by Google

Apêndice A: Pseudocódigo
Seção A.1: Ações variáveis

Você poderia descrever a afetação variável de diferentes maneiras.

Digitado
int a = 1
int a := 1
deixe int a = 1
int a <- 1

Nenhum tipo
a=1
a := 1
deixe a = 1
um <- 1

Seção A.2: Funções

Desde que o nome da função, a instrução de retorno e os parâmetros sejam claros, está tudo bem.

definição incr n
retornar n + 1

ou

deixe incr(n) = n + 1

ou

função incr (n) retornar n + 1

são todos bem claros, então você pode usá-los. Tente não ser ambíguo com uma afetação variável

Notas de algoritmos do [Link] para profissionais 249


Machine Translated by Google

Créditos
Muito obrigado a todas as pessoas da documentação do Stack Overflow que ajudaram a fornecer este conteúdo,
mais alterações podem ser enviadas para web@[Link] para que novos conteúdos sejam publicados ou atualizados

Abdul Karim Capítulo 1


afeldspato Capítulo 43
Ahmad Faiyaz Capítulo 28
Alberto Tadrous Capítulo 53
Anagh Hegde Capítulos 29 e 39
Andrii Artamonov Capítulo 27
Anuku L Capítulo 40
Bakhtiar Hasan Capítulos 9, 11, 14, 17, 19, 20, 22, 40, 41, 42, 47, 52 e 54
Benson Lin Capítulos 14, 39 e 44
tijolos Capítulo 39
Chris Capítulo 15
João Criativo Capítulos 49 e 51
Dian Bakti Capítulo 10
Didgeridoo Capítulos 2 e 43
Dipesh Poudel Dr. Capítulo 21
Capítulo 55
Capítulos 2, 29, 30, 39 e 50
Filip Allberg Capítulos 1 e 9
ghilesZ Capítulo 17
goddek Capítulos 18 e 27
grande lobo Capítulo 5
Ijaz Khan Capítulo 29
invisal Capítulo 31
Isha Agarwal Capítulos 4, 5, 6, 7 e 8
Ishit Mehta Capítulo 5
IVlad Capítulos 16 e 28
Iwan Capítulo 30
Janaky Murthy Capítulo 6
JJTO Capítulo 9
Julien Rousé Capítulo 24
Juxhin Metaj Capítulos 2 e 30
Keyur Ramoliya Capítulos 23, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 45, 47, 48 e 50
Khaled.K Capítulo 39
kiner_shah Capítulo 12
lambda Capítulo 38
Amor Agarwal Capítulo 30
Linfato Capítulo 31
Senhor Hossain Capítulo 17
Malav Capítulo 33
Malcolm McLean Capítulos 4 e 39
Martinho Frank Capítulo 21
Mehedi Hasan Capítulo 5
Miljen Mikic Capítulos 2, 28 e 39
Minhas Kamal Capítulos 12 e 46
Mnoronha Capítulos 23, 29, 31, 32, 33, 34, 35, 36 e 45
msohng Capítulo 39
Nick Larsen Capítulo 2

Notas de algoritmos do [Link] para profissionais 250


Machine Translated by Google
Nick o codificador Capítulo 3
otimistanoop Capítulos 29 e 33
Pedro K Capítulo 2
Rashik Hasnat Capítulo 40
Roberto Fernandez Capítulo 12
samgak (sempre) Capítulo 29
Samuel Pedro Capítulo 3
Santiago Gil Capítulo 30
Sayakiss Capítulos 9 e 14
SHARMA Capítulo 30
Piscina Shree Capítulo 39
Shubham Capítulo 16
Sumeet Singh Capítulos 20 e 41
TajyMuitos Capítulos 12 e 13
Tejus Prasad Capítulos 2, 5, 9, 11, 18, 19 e 45
theJollySin Capítulo 17
umop apisdn Capítulo 39
Usuário0911 Capítulo 29
usuário23013 Capítulo 9
VermillionAzure Capítulos 4 e 9
Vishwas Capítulos 14, 25 e 26
Cofre Wit Capítulo 3
xenteros Capítulos 17, 29 e 39
Yair Twito Capítulo 2
yd1 Capítulo 4
Yerken Capítulos 16 e 20
Jovem Hobbit Capítulo 29

Notas de algoritmos do [Link] para profissionais 251


Machine Translated by Google

Você também pode gostar

Você também pode gostar