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

Algorithms Notes For Professionals - Traduzido

Enviado por

Gilcimar Barros
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)
21 visualizações257 páginas

Algorithms Notes For Professionals - Traduzido

Enviado por

Gilcimar Barros
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

GoalKicker. com Livros Isenção de


responsabilidade Este é um livro gratuito não oficial criado para fins educacionais
de programação gratuitos e não é afiliado a grupo(s) ou empresa(s) de algoritmos ociais.
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: O(log n) tipos de algoritmos ........................................ .................................................. ........................ 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 Pesquisa Binária .................................................. .................................................. ...................... 18

Seção 5.1: Árvore de pesquisa 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 pesquisa binária - Python ........................................... .................................................. ..................... 22

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

6.1: Algoritmo para verificar se uma determinada árvore binária é BST .................................................. ................................ 24

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

binária ou não Capítulo 7: Travessias de árvore .................................................. .................................................. ................. 26

binária Seção 7.1: Ordem de nível travessia - Implementação ................................................ ................................................ 26


Seção 7.2: Percurso de pré-pedido, pedido interno e pós-pedido 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 Gráficos (Matriz de Adjacência) ......................................... .................................................. ............ 30


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

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

9.6: Algoritmo de Thorup .............. .................................................. .................................................. ... 41


Capítulo 10: Travessias gráficas .................................................. .................................................. .......................... 43

Seção 10.1: Função de travessia da primeira pesquisa 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* Descoberta de Caminhos ........................................... .................................................. ........................................
49
Seção 12.1: Introdução a A* .................................................. .................................................. ................................ 49

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

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

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


Seção 13.1: Exemplo simples de descoberta de caminhos A*: 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 trabalho ponderado ........................................... .................................................. ..... 66


Seção 14.3: Subsequência Comum Mais Longa .......................................... .................................................. ............ 70
Seção 14.4: Número de Fibonacci .................................................. .................................................. ............................. 71

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


Capítulo 15: Aplicações de 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 ideal 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 mudança ..................................... .................................................. .............................. 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 intervalo ............................................. .................................................. .................................. 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 BellmanFord ............................................. .................................................. ............. 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 Numérico Catalão ........................................... .................................................. ......... 127

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

Capítulo 24: Algoritmos Multithreaded ............................................. .................................................. .......... 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 Seção .................................................. ........................................ 129
.................................................. .................................................. .................
24.3: Multithread de classificação por mesclagem 129
Capítulo 25: Algoritmo Knuth Morris Pratt (KMP) ........................................ ............................................. 131
Seção 25.1: Exemplo KMP .......................................... .................................................. ........................................ 131
Capítulo 26: Editar Algoritmo Dinâmico de Distância ........................................... ................................................ 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 bolha .................................................. .................................................. ........................................... 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 .................................................. .................................................. ................. 147

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


Capítulo 30: Classificação por mesclagem.................................................. .................................................. ..................................... 149

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

Merge Sort Seção 30.2: Implementação de Merge Sort .................................................. .................................................. 150

em Go Seção 30.3: Implementação de Merge Sort em C e .................................................. ........................................... 150

C# Seção 30.4: Implementação de Merge Sort em Java .................................................. ............................................... 152

Seção 30.5: Implementação de Merge Sort em Python .......... .................................................. .................................. 153
Seção 30.6: Implementação Java de baixo para cima .......................................... .................................................. ....... 154

Capítulo 31: Classificação por .................................................. .................................................. ................................. 156

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

Capítulo 32: Classificação de .................................................. .................................................. .................................... 157

bucket Seção 32.1: Implementação de C# ........................................ .................................................. ................................... 157

Capítulo 33: Classificação rápida ............................................. .................................................. ............................................. 158

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

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


Seção 33.3: Implementação Java da partição Lomuto ........................................... .................................................. 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 do Psuedocódigo ...................................... .................................................. .................... 162
Capítulo 35: Classificação de Heap .................................................. .................................................. ........................................ 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 de 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: Classificação por Seleçã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: Pesquisa binária .................................................. .................................................. .................................. 174

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

Seção 39.3: Análise de pesquisa linear (piores, médios e melhores casos) ................................... ..................... 176

Seção 39.4: Pesquisa binária: em números classificados .................................................. ................................................ 178


Seção 39.5: Pesquisa 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 em Python do algoritmo KMP .......................................... ........................................... 186
Seção 40.4: Algoritmo KMP em C Capítulo .................................................. .................................................. ........................ 187

41: Pesquisa em amplitude .................................................. .................................................. ................ 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 gráfico não direcionado usando BFS .................................................. ........... 197

Capítulo 42: Primeira pesquisa em .................................................. .................................................. .................... 202


.................................................. ................................................
profundidade Seção 42.1: Introdução à pesquisa em profundidade 202

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

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

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

Algoritmo 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 do problema da .................................................. .................................................. .............. 212

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: Subsequência Comum Mais Longa .......................................... ................................................ 220

Seção 47.1: Explicação da Subsequência Comum Mais Longa ........................................... ........................................ 220

Capítulo 48: Subsequência Crescente Mais Longa .......................................... ............................................. 225

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

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

Seção 49.1: Exemplo de entrada e saída ........................................... .................................................. ........................ 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: Algo: - Imprima a matriz am*n em 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 matriz 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 Dinâmica do Tempo .......................................... .................................................. ................ 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: Base 2 FFT .................................................. .................................................. ........................................... 242
Seção 55.2: FFT Inversa da Radix 2 .................................................. .................................................. ........................ 247

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


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

você 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:

https://goalkicker.com/AlgorithmsBook

Este livro Algorithms Notes for Professionals foi compilado da documentação do Stack
Overflow , o conteúdo foi escrito pelas pessoas bonitas do Stack Overflow.
O conteúdo do texto é liberado sob Creative Commons BY-SA, veja os créditos no final deste
livro quem contribuiu para os vários capítulos. As imagens podem ser protegidas por direitos
autorais de seus respectivos proprietários, salvo especificação em contrário

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
proprietários de empresas

Não há garantia de que as informações apresentadas neste livro sejam corretas ou precisas,
use por sua conta e risco

Por favor, envie comentários e correções para web@petercv.com

Notas sobre algoritmos do GoalKicker.com 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 sua saída após ser
executado em uma dessas instâncias. Esta distinção, entre um problema e uma instância de um problema, é fundamental. O problema
algorítmico conhecido como classificação é definido da seguinte forma: [Skiena:2008:ADM:1410219]

Problema:
Classificando Entrada: Uma sequência de n chaves, um.
a_1, a_2, ..., Saída: A reordenação da sequência de entrada tal 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 na programação em Swift e para aqueles que vêm de diferentes bases de programação, como Python ou Java, este
artigo deve ser bastante útil. Neste post, discutiremos uma solução simples para implementar algoritmos rápidos.

Fizz Buzz

Você deve ter visto Fizz Buzz escrito como Fizz Buzz, FizzBuzz ou Fizz-Buzz; todos estão se referindo à mesma coisa. Essa “coisa” é o principal
tema 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 será substituído por fizz; se um número for divisível por 5, ele será substituído por buzz. Se um número for simultaneamente múltiplo de 3 E
5, o número será substituído por "fizz buzz". Em essência, ele emula o famoso jogo infantil "fizz buzz".

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

// por exemplo,
let number = [1,2,3,4,5] // aqui 3
é efervescente e 5 é buzz

Para encontrar todos os efervescentes e buzz, devemos percorrer a matriz e verificar quais números são efervescentes 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 do módulo rapidamente, ou seja, -% para localizar o fizz e o buzz

Notas sobre algoritmos do GoalKicker.com para profissionais 2


Machine Translated by Google

para num em número


{ if num % 3 == 0
{ print("\(num) fizz") } else
{ print(num)

}
}

Ótimo! Você pode ir para o console de depuração no playground do Xcode para ver a saída. Você descobrirá que os "efervescentes" foram
classificados em sua matriz.

Para a parte do Buzz usaremos a mesma técnica. Vamos tentar antes de ler o artigo - você pode comparar seus resultados com este artigo
assim que terminar de fazer isso.

para num em número


{ if num % 3 == 0
{ print("\(num) fizz") } else
if num % 5 == 0 { print("\(num)
buzz") } else { print(num)

}
}

Verifique a saída!

É bastante simples - você dividiu o número por 3, fizz e dividiu o número por 5, buzz. Agora, aumente os números na matriz

deixe 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 a 10 para 1 a 15 para demonstrar o conceito de "efervescência". Como 15 é múltiplo de 3 e 5, o número
deve ser substituído por “fizz buzz”. Experimente você mesmo e verifique a resposta!

Aqui está a solução:

para num em número


{ if num % 3 == 0 && num % 5 == 0 { print("\
(num) fizz buzz")
} else if num % 3 == 0 { print("\
(num) fizz") } else if num
% 5 == 0 { print("\(num) buzz") }
else { print(num)

}
}

Espere... ainda não acabou! Todo o objetivo do algoritmo é personalizar o tempo de execução corretamente. Imagine se o intervalo aumentasse
de 1-15 para 1-100. O compilador verificará cada número para determinar se ele é divisível por 3 ou 5. Ele então percorrerá os números
novamente para verificar se os números são divisíveis por 3 e 5. O código teria essencialmente que percorrer cada número na matriz duas vezes -
seria necessário executar os números por 3 primeiro e depois 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 sobre algoritmos do GoalKicker.com para profissionais 3


Machine Translated by Google

if num % 15 == 0
{ print("\(num) fizz buzz")
} else if num % 3 == 0 { print("\
(num) fizz") } else if num
% 5 == 0 { print("\(num)
buzz") } else { print(num)

}
}

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

Aproveite a codificação

Notas sobre algoritmos do GoalKicker.com 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 é um
limite apertado; limite superior e inferior. O limite rígido é 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 suficientemente grandes 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 todo n > n0 e abs é o absoluto valor }

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 o contrário!

Definição matemática formal

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

ÿ(g(x)) = {f(x) tal que existam constantes positivas c1, c2, N tais que 0 <= c1*g(x) <= f(x)
<= c2*g(x) para todo 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, nós
normalmente escreverá f(x) = ÿ(g(x)) para expressar a mesma noção - essa é a maneira comum.

Sempre que ÿ(g(x)) aparece em uma fórmula, interpretamos isso como representando alguma função anônima que não sabemos.
cuidado em 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 existem constantes positivas K e L e um número real x0 tal que vale:

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 limite(x->infinito) f(x)/g(x) = c ÿ (0,ÿ) ou seja, o limite existe e é positivo, então f(x) = ÿ(g(x))

Classes de complexidade comum

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


Constante ÿ(1) 1 1

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

Linear 10 100

Notas sobre algoritmos do GoalKicker.com 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 S(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 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ÿ
ÿ c > 0, ÿ n0 > 0 : ÿ n ÿ n0, 0 ÿ f(n) ÿ c g(n) ÿ c > 0, ÿ n0 > 0 : ÿ n ÿ n0, 0 ÿ c g(n) ÿ f(n)
definição f(n) ÿ c2g(n) ÿn0, n0, 0
0ÿ ÿc
f(n) < g(n)
cg (n) <
f(n)

Analogia
Entre o
assintótico
uma ÿ b uma ÿ b uma = b a < não > b
comparação
de f, g e
numeros 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)
sobre)

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 sobre algoritmos do GoalKicker.com 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 existem
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 lentamente que g(n). Também podemos dizer sobre ÿ(g(n))
quando a análise do algoritmo não é suficiente para afirmar sobre ÿ(g(n)) ou / e O(g(n)).

Das definições de 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, emita "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 de
ÿ(n^2) usando a definição usual de ÿ , uma vez que o algoritmo é executado 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 infinitos n. Isso
fornece uma boa correspondência entre os limites superior e inferior: f(n)=ÿ(g(n)) se 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. Introdução aos
Algoritmos".

Notas sobre algoritmos do GoalKicker.com 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, reduzi-o ao que é usado na análise de
complexidade de algoritmos: funções definidas nos naturais, que têm valores diferentes de zero, e no caso de n crescente ao infinito.

O que isso significa ?

Tomemos o caso de f(n) = 100n^2 + 10n + 1 e g(n) = n^2. É bastante 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 que velocidade as funções se aproximam do
seu limite. Noções como Big-O ajudam a comparar e classificar funções pela 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 em 111.
Pode parecer que crescendo 111 vezes mais rápido não é “aproximadamente a mesma velocidade”. E, de fato, a notação Big-O não é uma
forma muito precisa de classificar a velocidade de convergência da função, e é por isso que em matemática usamos a relação de
equivalência quando queremos uma estimativa precisa da velocidade. Mas para fins de separação de algoritmos em grandes classes
de velocidade, Big-O é suficiente. Não precisamos separar funções que crescem um número fixo de vezes mais rápido que as outras, mas apenas
funções que crescem infinitamente mais rápido que as 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: 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), que pode ser entendido como “g é o menor Big-O para f”. Em matemática dizemos que tais funções são
Big-Thetas uma da outra.

Como isso é usado ?

Ao comparar o desempenho do algoritmo, estamos interessados no número de operações que um algoritmo executa. 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. Geralmente 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 isso não acontecer, dizemos
que o algoritmo é O(1)). Separamos nossos algoritmos em classes de grande velocidade definidas por Big-O: quando falamos de um
"algoritmo O(n^2)", queremos dizer que o número de operações que ele realiza, expresso em função de n, é um O( n^2). Isto diz que o nosso
algoritmo é aproximadamente tão rápido quanto um algoritmo que faria um número de operações igual ao quadrado do tamanho da sua
entrada, ou mais rápido. A parte "ou mais rápida" está aí porque usei Big-O em vez de Big-Theta, mas normalmente as pessoas dizem que Big-
O significa Big-Theta.

Notas sobre algoritmos do GoalKicker.com para profissionais 8


Machine Translated by Google

Ao contar operações, geralmente consideramos o pior caso: por exemplo, se tivermos 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
dos casos.

Nota rápida: um algoritmo rápido é aquele que executa poucas operações, portanto, se o número de operações crescer 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 espacial do nosso algoritmo. Para isso consideramos o número de bytes
de memória ocupados pelo algoritmo em função do tamanho da entrada, e utilizamos Big-O da mesma forma.

Seção 3.1: Um Loop Simples


A função a seguir encontra o elemento máximo em um array:

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


INT_MIN; for
(tamanho_t i = 0; i < len; i++) { if (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 máximo = INT_MIN;


tamanho_t i = 0;

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

if (max < matriz[i]) i++;

máximo = matriz[i]

Como existem 3 operações no loop, e o loop é feito n vezes, adicionamos 3n às nossas 2 operações já existentes para
obter 3n + 2. Portanto, 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 já percebeu que “operação” não está muito bem definida. Por exemplo, eu disse que if (max < array[i]) era
uma operação, mas dependendo da arquitetura esta instrução pode ser compilada para, por exemplo, três instruções: uma leitura
de memória, uma comparação e uma ramificação. Também considerei todas as operações iguais, embora, por exemplo,
as operações de memória sejam mais lentas que as outras e seu desempenho varie bastante devido, por exemplo, aos efeitos
de cache. Também ignorei completamente a instrução return, o fato de que um quadro será criado para a função, etc. No final
das contas isso não importa para a análise de complexidade, porque seja qual for a forma que eu escolher para contar as
operações, isso apenas mudará o coeficiente do fator n e da constante, então o resultado ainda será O(n).
A complexidade mostra como o algoritmo se adapta ao 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 um array tem alguma duplicata pegando cada elemento e, em seguida, iterando por todo o array para ver
se o elemento está lá

Notas sobre algoritmos do GoalKicker.com para profissionais 9


Machine Translated by Google

_Bool contém_duplicates(const int *array, size_t len) {


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

}
}

} retornar 0;
}

O loop interno executa a 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 próprio loop externo é executado n vezes. Portanto, 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 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 mais rápido_contains_duplicates(const int *array, size_t len) {


for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) { if (array[i] ==
array[j]) { return 1;

}
}

} retornar 0;
}

Obviamente, esta segunda versão realiza menos operações e por isso é 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). Reduzimos claramente a complexidade, pois dividimos aproximadamente
por 2 o número de operações que estamos realizando, mas ainda estamos na mesma classe de complexidade definida pelo
Big-O. Para diminuir a complexidade para uma classe inferior precisaríamos dividir o número de operações por algo que tende
ao infinito com n.

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


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

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

Etapa Problema
1 n/2
2n/4
3n/8
4 n/16

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

Notas sobre algoritmos do GoalKicker.com para profissionais 10


Machine Translated by Google

1. Digamos na k-ésima etapa ou número de operações:

tamanho do problema = 1

2. Mas sabemos que na k-ésima etapa, 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 tronco dos dois lados

loge n = k loge2

ou

k = loge n / loge 2

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

k = log2n

ou simplesmente k = log n

Agora sabemos que nosso algoritmo pode rodar no máximo até log n, portanto a complexidade do tempo vem como

O (log n)

Um exemplo muito simples de código para suportar o texto acima é:

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

// realiza alguma operação


}

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

será executado, você poderá calcular facilmente.

k = log2 256

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

k=8

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

Notas sobre algoritmos do GoalKicker.com para profissionais 11


Machine Translated by Google

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


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

while(baixo<=alto)
{ médio=baixo+(alto-baixo)/
2; if(arr[mid]==item)
return mid;
senão if(arr[mid]<item)
low=mid+1;
senão alto = médio-1; }

return –1;// Resultado sem sucesso


}

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


Introdução

Considere o seguinte problema:

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

Abordagem ingênua

A primeira coisa que vem à mente é apenas ler todos os índices até que 0 seja encontrado. Na pior das hipóteses, o número de
operações é n, então a complexidade é O(n).

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

Dicotomia

Considere o seguinte algoritmo (Python3):

uma = 0
b = n-1
enquanto Verdadeiro:

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


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

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

Na pior das hipóteses, 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 aeb por cerca de dois . Em vez disso, a complexidade é O(log
n).

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 sobre algoritmos do GoalKicker.com 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 que a complexidade é logarítmica.

Notas sobre algoritmos do GoalKicker.com 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. Observe que se uma árvore for binária, esta
representação cria nós extras.

Em seguida, iteramos sobre os irmãos e recorremos aos filhos. Como a maioria das árvores são relativamente rasas - 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, os ponteiros traseiros podem ser mantidos para permitir a subida da árvore. Estes são mais difíceis de manter.

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

nó de estrutura
{
nó de estrutura *próximo;
nó de estrutura *filho;
dados std::string;
}

void printtree_r(struct node *node, int profundidade) {

int eu;

enquanto(nó) {

if(nó->filho) {

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

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

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

void printtree(nó *raiz) {

printree_r(raiz, 0);
}

Seção 4.2: Introdução

Árvores são um subtipo da estrutura de dados gráfica mais geral da borda do nó.

Notas sobre algoritmos do GoalKicker.com para profissionais 14


Machine Translated by Google

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

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


Está conectado. Para qualquer nó no gráfico, todos os nós são alcançáveis. Todos os nós são acessíveis através de um caminho no
gráfico.

A estrutura de dados em árvore é bastante comum na ciência da computação. As árvores são usadas para modelar muitas estruturas
de dados algorítmicas diferentes, como árvores binárias comuns, árvores vermelho-pretas, árvores B, árvores AB, árvores 23, Heap e
tentativas.

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

escolhendo 1 célula para ser chamada de


`Root` pintando a `Root` na parte
superior criando uma camada inferior para cada célula no gráfico dependendo da distância da raiz - quanto maior a
distância, mais baixas serão 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

a)

Notas sobre algoritmos do GoalKicker.com para profissionais 15


Machine Translated by Google

b)

A saída deve ser verdadeira.

Exemplo:2

Se as entradas forem:

a)

b)

A saída deve ser falsa.

Pseudocódigo para o mesmo:

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

Notas sobre algoritmos do GoalKicker.com para profissionais 16


Machine Translated by Google

if(root1 == NULL && root2 == NULL) retorna


verdadeiro;

if(root1 == NULL || root2 == NULL) retorna


falso;

if(root1->dados == root2->dados &&


sameTree(root1->esquerda,root2->esquerda)
&& sameTree(root1->right, root2->right)) retorna
verdadeiro;

Notas sobre algoritmos do GoalKicker.com para profissionais 17


Machine Translated by Google

Capítulo 5: Árvores de Pesquisa Binária


Árvore binária é uma árvore em que cada nó possui no máximo dois filhos. Árvore de pesquisa 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 pesquisa binária - Inserção (Python)


Esta é uma implementação simples de inserção de árvore de pesquisa binária usando Python.

Um exemplo é mostrado abaixo:

Seguindo 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.

Nó de classe :
def __init__(self, val): self.l_child
= Nenhum self.r_child =
Nenhum self.data = val

def insert(root, node): se root


for Nenhum:
root = nó else:
se
root.data > node.data:
se root.l_child for Nenhum:
root.l_child = nó
outro:
inserir (root.l_child, nó)
outro:
se root.r_child for Nenhum:

Notas sobre algoritmos do GoalKicker.com para profissionais 18


Machine Translated by Google

root.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
root.data
in_order_print (root.r_child)

def pre_order_print(root): se não


for root:
retornar
impressão root.data
pre_order_print(root.l_child)
pre_order_print(root.r_child)

Notas sobre algoritmos do GoalKicker.com 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, quero apenas esclarecer o que é uma árvore de pesquisa binária (BST). Cada nó em
um BST pode ter no máximo dois nós (filho esquerdo e direito). chave menor ou igual à chave do nó pai. A subárvore
direita de um nó possui uma chave maior que a chave do nó pai.

Excluir um nó em uma árvore enquanto mantém sua propriedade de árvore de pesquisa binária.

Existem 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 possui um filho. (Nó com valor 26).
Caso 3: O nó a ser excluído possui ambos os filhos. (Nó com valor 49).

Explicação dos casos:

1. Quando o nó a ser excluído for um nó folha, simplesmente exclua o nó e passe 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 então
o valor mínimo pode ser excluído da subárvore direita do nó (convertido para o caso 2 )

Nota: O mínimo na subárvore direita pode ter no máximo um filho e aquele filho também certo se tiver o filho esquerdo
significa que não é o valor mínimo ou não está seguindo a propriedade BST.

Notas sobre algoritmos do GoalKicker.com para profissionais 20


Machine Translated by Google

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

nó de estrutura
{
dados
internos ; nó *esquerda, *direita;
};

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

if(root == nullptr) retornar raiz; senão


if(dados <root-> dados) root->esquerda = delete_node(raiz->esquerda, dados); senão
if(dados > root->dados) root->right = delete_node(root->right, dados);

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

grátis(raiz);
raiz = nullptr;

} else if(root->esquerda == nullptr) { // Caso 2

nó* temp = raiz; raiz=


raiz->direita;
grátis(temperatura);

} else if(root->right == nullptr) { // Caso 2

nó* temp = raiz; raiz =


raiz->esquerda;
grátis(temperatura);

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

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

root->dados = temp->dados;
root->direita = delete_node(root->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 sobre algoritmos do GoalKicker.com 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 o ancestral mais baixo dos nós

Código pseudo:

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

if(raiz == NULO)
retorna NULO;

senão if(node1->data == root->data || node2->data== root->data)


retornar raiz;

senão if((node1->data <= root->data && node2->data > root->data)


|| (node2->dados <= root->dados && node1->dados > raiz->dados)){

retornar raiz;
}

senão if(root->dados > max(node1->dados,node2->dados)){


retornar menorCommonAncestor(root->esquerda, node1, node2);
}

else
{ return lowerCommonAncestor(root->right, node1, node2);
}
}

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


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

classe BinarySearchTree (objeto):


def inserir(self, raiz, nó):

Notas sobre algoritmos do GoalKicker.com para profissionais 22


Machine Translated by Google
se root for Nenhum:
nó de retorno

se root.val < node.val:


root.r_child = self.insert(root.r_child, node) senão: root.l_child =

self.insert(root.l_child, node)

raiz de retorno

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

retornar Nenhum
outro:
self.in_order_place(root.l_child) imprimir root.val

self.in_order_place(root.r_child)

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


return Nenhum
outro:

imprimir root.val
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 root.val

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

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

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

print "------Em ordem ---------" print


(node.in_order_place(r)) print "------Pré-pedido
---------" print (node.pre_order_place(r)) print "------
Publicar pedido ---------" print
(node.post_order_place(r))

Notas sobre algoritmos do GoalKicker.com 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 satisfizer qualquer uma das seguintes condições:

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

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

Portanto, um algoritmo recursivo direto seria:

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

// Verifica os valores na subárvore esquerda


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

// Verifica os valores na subárvore direita if root-


>right != NULL: min_key_in_right
= find_min_key(root->right) if min_key_in_right < root->key: return
false

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

O algoritmo recursivo acima é 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ínimo e máximo possíveis das chaves na subárvore que estamos
visitando. Deixe o valor mínimo possível de qualquer chave ser K_MIN e o valor máximo ser K_MAX. Quando partimos da raiz da árvore, o intervalo de
valores na árvore é [K_MIN,K_MAX]. Deixe a chave do nó raiz ser 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? if root->chave


< min || root->chave > max: retornar falso

// verifica 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)

Será inicialmente chamado como:

is_BST(minha_árvore_raiz,KEY_MIN,KEY_MAX)

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

Notas sobre algoritmos do GoalKicker.com para profissionais 24


Machine Translated by Google

nó visitado anteriormente e compare-o com o nó atual.

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


da árvore de pesquisa binária

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 sobre algoritmos do GoalKicker.com 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: Passagem de ordem de nível - Implementação


Por exemplo, se a árvore fornecida for:

A passagem da ordem de nível será

1234567

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

Código:

#include<iostream>
#include<queue>
#include<malloc.h>

usando namespace std;

nó de estrutura{

dados
internos ; nó
*esquerdo; nó *direita;
};

void levelOrder( nó de estrutura *root){

if(raiz == NULO) retornar;

fila<nó *> Q;
Q.push(raiz);

enquanto(!Q.empty()){
estrutura nó* curr = Q.front(); cout<<
curr->dados <<" "; if(curr->left !=
NULL) Q.push(curr-> left);
if(curr->right != NULL) Q.push(curr-> right);

Q.pop();

Notas sobre algoritmos do GoalKicker.com para profissionais 26


Machine Translated by Google

} estrutura nó* newNode(int dados) {

nó de estrutura* nó = (nó de estrutura*)


malloc(sizeof( nó de estrutura));
nó->dados = dados; nó-
>esquerda = NULL; nó-
>direita = NULL;

retorno(nó);
}

int principal(){

nó de estrutura *root = newNode(1); raiz-


>esquerda = newNode(2); raiz->direita =
newNode(3); raiz->esquerda->esquerda
= newNode(4); raiz->esquerda->direita =
newNode(5); raiz->direita->esquerda =
newNode(6); root->direita->direita =
newNode(7);

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


nívelOrdem(raiz);

retornar 0;

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

Seção 7.2: Percurso de pré-pedido, pedido interno e pós-pedido de


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

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

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

1245367

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

Notas sobre algoritmos do GoalKicker.com para profissionais 27


Machine Translated by Google

nó.

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

4251637

A travessia pós-ordem (raiz) atravessa a subárvore esquerda do nó, depois a subárvore direita e depois o nó.

Portanto, o percurso pós-ordem da árvore acima será:

4526731

Notas sobre algoritmos do GoalKicker.com para profissionais 28


Machine Translated by Google

Capítulo 8: Menor ancestral comum de um


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

Seção 8.1: Encontrando o menor ancestral comum


Considere a árvore:

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

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

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

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

Notas sobre algoritmos do GoalKicker.com para profissionais 29


Machine Translated by Google

Capítulo 9: Gráfico
Um gráfico é uma coleção de pontos e linhas conectando alguns subconjuntos (possivelmente vazios) deles. Os pontos de um gráfico são chamados de vértices do

gráfico, "nós" ou simplesmente "pontos". Da mesma forma, as linhas que conectam 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) | você, v ÿ V}.

Seção 9.1: Armazenando Gráficos (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 gráfico finito. Os elementos da matriz indicam se os pares de vértices são

adjacentes ou não no gráfico.

Adjacente significa 'próximo ou adjacente a outra coisa' ou estar ao lado de alguma coisa. 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 por

meio da 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 é a 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, pois podem 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 pudermos ir para o nó 1 a partir do nó 2, também podemos ir para o nó 2 a partir do nó 1.

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 a matriz de adjacência.

Notas sobre algoritmos do GoalKicker.com para profissionais 30


Machine Translated by Google

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

O pseudocódigo para criar a matriz:

Matriz de adjacência do procedimento (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 -> Matrix[i][j]
endfor
endfor

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

Matriz de adjacência do procedimento (N, E): // N -> número de nós


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

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

As desvantagens de usar a Matriz de Adjacência:

A memória é um grande problema. 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 10.000 nós, o tamanho da matriz será 4 * 10.000 * 10.000 em torno de 381 megabytes.
Isso é um enorme desperdício de memória se considerarmos gráficos que possuem algumas arestas.

Suponha que queiramos descobrir para qual nó podemos ir a partir de um nó u. Precisaremos verificar toda a sua linha , o que custa 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 java.util.Scanner;

classe pública Representa_Graph_Adjacency_Matrix {

vértices int finais privados ;

Notas sobre algoritmos do GoalKicker.com para profissionais 31


Machine Translated by Google

private int[][] matriz_adjacência;

público Represent_Graph_Adjacency_Matrix (int v) {

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

public void makeEdge(int para, int de, int borda) {

tentar

matriz_adjacência[to][de] = borda;

} catch ( índice ArrayIndexOutOfBoundsException) {

System.out.println("Os vértices não existem");


}
}

public int getEdge(int para, int de) {

tentar

retornar matriz_adjacência[para][de];

} catch ( índice ArrayIndexOutOfBoundsException) {

System.out.println("Os vértices não existem");

} retornar -1;
}

public static void main(String args[]) {

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


sc = novo Scanner(System.in); Gráfico
Representa_Graph_Adjacency_Matrix; tentar {

System.out.println("Digite o número de vértices: "); v = sc.nextInt();


System.out.println("Digite
o número de arestas: "); e = sc.nextInt();

gráfico = novo Represent_Graph_Adjacency_Matrix(v);

System.out.println("Insira as arestas: <to> <from>"); enquanto


(contagem <= e) {

para = sc.nextInt(); de
= sc.nextInt();

gráfico.makeEdge(para, de, 1);


contar++;
}

System.out.println("A matriz de adjacência para o gráfico fornecido é: ");


System.out.print(" "); para
(int i = 1; i <= v; i++)
System.out.print(i + " ");
System.out.println();

Notas sobre algoritmos do GoalKicker.com para profissionais 32


Machine Translated by Google

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

System.out.print(i + " "); para (int


j = 1; j <= v; j++)
System.out.print(graph.getEdge(i, j) + " ");
System.out.println();
}

} catch (Exceção E) {

System.out.println("Algo deu errado");


}

sc.close();
}
}

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

Exemplo:

$ java Representa_Graph_Adjacency_Matrix
Insira o número de vértices:
4
Insira 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 fornecido é: 1 2 3 4
11101
20011

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 neste problema Cidade-Estrada, poderemos resolvê-lo facilmente
pela Teoria dos Grafos.

A Teoria dos Grafos tem muitas aplicações. Uma das aplicações mais comuns é encontrar a distância mais curta entre uma cidade e
outra. Todos nós sabemos que para chegar ao seu PC, esta página da web teve que percorrer vários roteadores do servidor.
A Teoria dos Grafos ajuda a descobrir os roteadores que precisavam 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 sobre algoritmos do GoalKicker.com 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 os marcamos como 1, 2, 3, 4, 5, 6. Agora conectamos as cidades que possuem 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 Borda. O gráfico é simplesmente uma conexão desses nós e arestas.

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

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

movimentos de um cavalo de uma casa para todas as outras casas, etc.

Notas sobre algoritmos do GoalKicker.com para profissionais 34


Machine Translated by Google

Caminho do Cavaleiro em um tabuleiro de xadrez

Em palavras simples, um Nó representa qualquer objeto e Edge 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.

Gráfico direcionado e não direcionado:

Em gráficos direcionados, as arestas possuem sinais de direção em um lado, o que significa que as arestas são unidirecionais. Por
outro lado, as arestas dos gráficos não direcionados possuem sinais de direção em ambos os lados, o que significa que são bidirecionais.
Normalmente, os 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 no partido são representadas por nós e há uma vantagem entre duas
pessoas se apertarem 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 retribuída. 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 grafo direcionado e as arestas são chamadas de arestas direcionadas.

Gráfico ponderado e não ponderado:

Um gráfico ponderado é um gráfico 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 sobre algoritmos do GoalKicker.com 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. Consiste em uma sequência de arestas. Pode haver vários

caminhos entre dois nós.

No exemplo acima, existem dois caminhos de A a D. A->B, B->C, C->D é um caminho. O custo deste 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 sobre algoritmos do GoalKicker.com para profissionais 36


Machine Translated by Google

Em gráficos direcionados, os nós possuem dois tipos de graus:

In-degree: O número de arestas que apontam para o nó.


Out-degree: 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
Pesquisa em profundidade
Pesquisa ampla

Seção 9.3: Armazenando Gráficos (Lista de Adjacências)


Lista de adjacências é uma coleção de listas não ordenadas usadas para representar um gráfico finito. Cada lista descreve o conjunto de
vizinhos de um vértice em um gráfico. É preciso menos memória para armazenar gráficos.

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

Agora criamos uma lista usando esses valores.

Notas sobre algoritmos do GoalKicker.com para profissionais 37


Machine Translated by Google

Isso é chamado de lista de adjacências. Mostra quais nós estão conectados a quais nós. Podemos armazenar essas informações
usando um array 2D. Mas nos custará a mesma memória que a Adjacency Matrix. Em vez disso, usaremos memória alocada
dinamicamente para armazenar esta.

Muitos idiomas suportam Vector ou List que podemos usar para armazenar listas de adjacências. Para estes, não precisamos
especificar o tamanho da Lista. Precisamos apenas especificar o número máximo de nós.

O pseudocódigo será:

Lista de adjacências de procedimento // maxN denota o número máximo de nós


(maxN, E): edge[maxN] = // E denota o número de arestas
Vector() para i de 1 a E
entrada -> x, y // Aqui x, y denota que há uma aresta entre x, y
borda[x].push(y)
borda[y].push(x)
fim para
Borda de retorno

Como este é um gráfico não direcionado, se houver uma aresta de x a y, também haverá uma aresta de y a 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 chamado cost[] para armazená-los. O pseudocódigo:

Lista de Adjacência de Procedimento (maxN,


E): borda[maxN] = Vector()
custo[maxN] = Vector()
para i de 1 a E
entrada -> x, y, w
borda[x].push(y)
custo[x].push(w)
fim para
borda de retorno, custo

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

Notas sobre algoritmos do GoalKicker.com para profissionais 38


Machine Translated by Google

Leva menos tempo do que a Matriz de Adjacência. Mas se precisássemos descobrir se existe uma aresta entre u e v, teria sido 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 classificação topológica, ordena os vértices em um gráfico 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 ordem não pode existir se o gráfico contiver um ciclo direcionado
porque não há como você continuar seguindo uma linha reta e ainda assim retornar ao ponto de partida.

Formalmente, em um grafo 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) ÿ Edo vértice u ao vértice v então u precede v na ordenação.

É importante notar que cada DAG possui 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 profundidade_primeira_pesquisa(G) para calcular os tempos de término vf para cada vértice


v 2. À medida que cada vértice for concluído, insira-o na frente de uma lista vinculada
3. a lista vinculada de vértices, como agora está classificada.

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

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

Os vértices em um grafo 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 (hours_to_complete: int), ou seja , Task(4) descreve uma tarefa que leva 4 horas
para ser concluída, e uma aresta e descreve um Cooldown(hours: int) tal que Cooldown(3) descreve uma duração de tempo
para esfriar após uma tarefa concluída.

Deixe nosso gráfico ser chamado de dag (já que é um gráfico acíclico direcionado) e contenha 5 vértices:

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


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

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

// A ---> C ----+
// | // | |
v // B em em

---> D --> E
dag.add_edge(A, B, Cooldown(2));
dag.add_edge(A, C, Recarga(2));
dag.add_edge(B, D, Recarga(1));
dag.add_edge(C, D, Recarga(1));
dag.add_edge(C, E, Recarga(1));
dag.add_edge(D, E, Recarga(3));

Notas sobre algoritmos do GoalKicker.com para profissionais 39


Machine Translated by Google

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

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


2. A -> C -> D -> E
3. A -> C -> E

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


Depth First Traversal
Um ciclo em um gráfico direcionado existe se houver uma borda posterior descoberta durante um DFS. Uma borda posterior é uma borda de um nó
para si mesmo ou para um dos ancestrais em uma árvore DFS. Para um gráfico desconectado, obtemos uma floresta DFS, então você precisa
percorrer todos os vértices do gráfico para encontrar árvores DFS disjuntas.

Implementação C++:

#include <iostream>
#include <lista>

usando namespace std;

#define NUM_V 4

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

visitado[u]=verdadeiro;
recStack[u]=verdadeiro;
lista<int>::iterador i; for(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; else if(*i==u) //se houver uma aresta do vértice para ele mesmo return
true; senão if(!
visited[*i]) { if(helper(graph,
*i, visitado, recStack))
retornar verdadeiro;
}

} recStack[u]=falso;
retorna falso;

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

bool isCyclic(lista<int> *gráfico, int V) {

bool visitado[V]; //array para rastrear vértices já visitados bool recStack[V]; //


array para rastrear vértices na pilha de recursão da travessia.

for(int i = 0;i<V;i++)
visitado[i]=false, recStack[i]=false; //inicializa todos os vértices como não visitados e não
recorrente

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

Notas sobre algoritmos do GoalKicker.com para profissionais 40


Machine Translated by Google

} retorna falso;

} /*
Função de 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, existem três arestas posteriores no gráfico. Um entre os vértices 0 e 2; entre os vértices 0, 1 e 2; e
vértice 3. A complexidade temporal da pesquisa é 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 caminho mais curto de fonte única para grafo não direcionado tem complexidade de tempo O(m), inferior a
Dijkstra.

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

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

Notas sobre algoritmos do GoalKicker.com para profissionais 41


Machine Translated by Google

totalmente crescido.

Selecione um número inteiro b (b>=2) e considere apenas as florestas abrangentes com limite de comprimento b^k.
Mescle os componentes que são exatamente iguais, mas com k diferentes, e chame o k mínimo de nível do
componente. Em seguida, transforme logicamente os componentes em uma árvore. u é o pai de v se u é o menor componente
distinto de v que contém totalmente v. 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 possui apenas O(n) nós.
Mantenha a distância de cada componente à 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 como 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 baldes compartilhados
por todos os nós do nível k (como no bucket sort, substituindo o heap no algoritmo de Dijkstra) pelo dígito k e superior 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 vincule novamente o nó atual ao seu próprio pai usando a nova distância e aguarde a próxima visita
para o seguinte baldes.
Quando uma folha é visitada, a distância atual é a distância final do vértice. Expanda todas as arestas dele 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.

É baseado no fato de que não existe uma aresta com comprimento menor que l entre dois componentes conectados da floresta
geradora com limitação de comprimento l, então, começando na distância x, você poderia focar apenas em um componente
conectado até chegar a distância x + eu. Você visitará alguns vértices antes que todos os vértices com distância mais curta sejam
visitados, mas isso não importa porque é sabido que não haverá um caminho mais curto até aqui a partir desses vértices. Outras partes
funcionam como bucket sort / MSD radix sort e, claro, requer a árvore geradora O(m).

Notas sobre algoritmos do GoalKicker.com para profissionais 42


Machine Translated by Google

Capítulo 10: Travessias gráficas


Seção 10.1: Função de travessia da primeira pesquisa 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 booleano para rastrear qual nó foi visitado.

void dfs(int node, vector<vector<int>>* gráfico, vector<bool>* visitado) {


// verifica 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;

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


(*graph)[node].size(); ++i) dfs((*graph)[node][i], gráfico,
visitado);
}

Notas sobre algoritmos do GoalKicker.com para profissionais 43


Machine Translated by Google

Capítulo 11: Algoritmo 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. É utilizado para encontrar os caminhos mais
curtos entre nós em um grafo, 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 pesquisa Broadth First Search (BFS). Este 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.

A princípio veremos como modificar o BFS para escrever o algoritmo de Dijkstra, depois adicionaremos a fila de prioridade para torná-lo
um algoritmo de Dijkstra completo.

Digamos que a distância de cada nó da fonte seja mantida no array d[] . Como em, d[3] representa que d[3] tempo é necessário para chegar
ao nó 3 da origem. Se não soubermos a distância, armazenaremos o infinito em d[3]. Além disso, deixe cost[u][v] representar o
custo de uv. Isso significa que é preciso custo[u][v] para ir de um nó para um nó v .

Precisamos entender o Edge Relaxation. Digamos que da sua casa, que é fonte, leva 10 minutos para ir ao lugar A. E leva 25 minutos
para ir ao lugar B. Temos,

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

Agora digamos que leva 7 minutos para ir do lugar A ao lugar 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 depois 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 nós atualizamos,

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

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

No BFS, não precisamos visitar nenhum nó duas vezes. Verificamos apenas se um nó foi visitado ou não. Se não foi visitado, colocamos
o nó na fila, marcamos-o como visitado e aumentamos a distância em 1. Em Dijkstra, podemos empurrar um nó

Notas sobre algoritmos do GoalKicker.com para profissionais 44


Machine Translated by Google

na fila e em vez de atualizá-lo com visitado, relaxamos ou atualizamos a nova borda. 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 o infinito porque ainda não sabemos a distância. E a distância da fonte é obviamente 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 as arestas 1-2. Como d[1] + 2 < d[2] o que fará d[2] = 2. Da mesma forma, percorreremos a
aresta 1-3 o que fará d[3] = 5.

Podemos ver claramente que 5 não é a distância mais curta que podemos cruzar para chegar ao nó 3. Portanto, atravessar um nó apenas
uma vez, como o BFS, não funciona aqui. Se formos do nó 2 para o nó 3 usando a aresta 2-3, podemos atualizar d[3] = d[2] + 1 = 3.
Portanto, 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. Vamos simplesmente modificar o BFS:

procedimento BFSmodified(G, source): Q =


queue()
distance[] = infinito
Q.enqueue(source)
distance[source]=0
enquanto Q não está
vazio u <-
Q.pop() para todas as arestas de u a v em G.adjacentEdges(v)
faça if distância[u] + custo[u][v] < distância[v]
distância[v] = distância[u] + custo[u][v] fim se fim
para

terminar enquanto

Distância de retorno

Notas sobre algoritmos do GoalKicker.com para profissionais 45


Machine Translated by Google

Isso pode ser usado para encontrar o caminho mais curto de todos os nós da origem. A complexidade deste 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 servir . Por exemplo, fomos para o nó 3 da
origem antes de processar o nó 2. Se formos para o nó 3 da origem, atualizamos o nó 4 como 5 + 3 = 8.
Quando atualizarmos novamente o nó 3 do nó 2, precisaremos atualizar o nó 4 como 3 + 3 = 6 novamente! Então o nó 4 é atualizado
duas vezes.

Dijkstra propôs, em vez de usar o método Primeiro a chegar, primeiro a servir , se atualizarmos os nós mais próximos primeiro, serão necessárias
menos atualizações. Se processássemos o nó 2 antes, o nó 3 teria sido atualizado antes e, após atualizar o nó 4 adequadamente, obteríamos
facilmente a distância mais curta! A ideia é escolher na fila, o nó, que está mais próximo da origem. Portanto , usaremos a Fila Prioritária aqui para que,
quando abrirmos a fila, ela nos traga o nó mais próximo da origem. Como isso será feito? Ele verificará o valor de d[u] com ele.

Vamos ver o pseudocódigo:

procedimento dijkstra(G, fonte): Q =


prioridade_queue()
distância[] = infinito
Q.enqueue(fonte)
distância[fonte] = 0
enquanto Q não está
vazio u <- nós em Q com distância mínima[] remove
u do Q para todas as
arestas de u a v em G.adjacentEdges(v) faça if distância[u] +
custo[u][v] < distância[v] distância[v] = distância[u] +
custo[u][v ]
Q.enqueue(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 origem. 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 vantagem negativa? Se houver um ciclo negativo, ocorrerá um loop infinito, pois continuará
reduzindo o custo sempre. Mesmo que haja uma vantagem negativa, Dijkstra não funcionará, a menos que retornemos logo após o alvo ser
estourado. Mas então, não será um algoritmo Dijkstra. Precisaremos do algoritmo Bellman-Ford para processar aresta/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 é semelhante, mas a
classificação da fila de prioridade leva O (logV). Portanto, a complexidade total é: O(Vlog(V)+E)

Abaixo está um exemplo Java para resolver o algoritmo de caminho mais curto de Dijkstra usando matriz de adjacência

importar java.util.*;
importar java.lang.*;
importar java.io.*;

class CaminhoMais {

final estático int V = 9; int


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

Notas sobre algoritmos do GoalKicker.com 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_mín = v;
}

retornar índice_minuto;
}

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

System.out.println(" Distância do vértice da fonte"); para (int i


= 0; i < V; i++)
System.out.println(i+" \t\t "+dist[i]);
}

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

Boolean sptSet[] = new Boolean[V];

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

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

dist[src] = 0;

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


+) {
int você = minDistance(dist, sptSet);

sptSet[u] = verdadeiro;

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

if (!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];
}

printSolução(dist, V);
}

public static void main (String[] args) { int

gráfico[][] = new 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} };

Caminho Mais Curto t = new Caminho Mais Curto();

Notas sobre algoritmos do GoalKicker.com para profissionais 47


Machine Translated by Google

t.dijkstra(gráfico, 0);
}
}

O resultado esperado 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 sobre algoritmos do GoalKicker.com para profissionais 48


Machine Translated by Google

Capítulo 12: A* Descoberta de Caminho


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

A* (Uma estrela) é um algoritmo de pesquisa usado para encontrar o caminho de um nó para outro. Portanto, pode ser comparado com
Primeira pesquisa em largura, ou algoritmo de Dijkstra, ou primeira pesquisa em profundidade, ou melhor primeira pesquisa. O algoritmo A* é amplamente utilizado

na busca de gráficos por ser melhor em eficiência e precisão, onde o pré-processamento de gráficos não é uma opção.

A* é uma especialização de Melhor Primeira Pesquisa , em que a função de avaliação f é definida de uma forma 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 até 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). Portanto, é completo e ideal. A seguinte animação
demonstra pesquisa A*-

Seção 12.2: A* Caminho através de um labirinto sem obstáculos


Digamos que temos a seguinte grade 4 por 4:

Notas sobre algoritmos do GoalKicker.com para profissionais 49


Machine Translated by Google

Vamos supor que isso seja um labirinto. Não há paredes/obstáculos, no entanto. Temos apenas um ponto inicial (o quadrado
verde) e um ponto final (o quadrado vermelho). Suponhamos também que, para passar 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 sobre algoritmos do GoalKicker.com para profissionais 50


Machine Translated by Google

Para escolher para qual quadrado passar, precisamos levar em consideração 2 heurísticas:

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


2. O valor "h" - É 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(from.x - to.x) + abs(from.y - to.y)

Isso é 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 obter o valor “f”: 1 + 4 = 5

Portanto, o valor final deste nó é “5”.

Vamos fazer o mesmo com todos os outros quadrados azuis. O grande número 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 sobre algoritmos do GoalKicker.com para profissionais 51


Machine Translated by Google

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

Aquele que tiver o menor valor de f.

Porém, neste caso, temos 2 nós com o mesmo valor de f, 5. Como escolhemos entre eles?

Simplesmente escolha um aleatoriamente ou tenha um conjunto de prioridades. Normalmente prefiro ter uma prioridade assim: "Direita > Cima >
Baixo > Esquerda"

Um dos nós com valor f 5 nos leva na direção “Baixo” e o outro nos leva na direção “Esquerda”. Como Baixo tem prioridade mais alta que Esquerda,

escolhemos o quadrado que nos leva para "Baixo".

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

Notas sobre algoritmos do GoalKicker.com 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 de f:

Notas sobre algoritmos do GoalKicker.com para profissionais 53


Machine Translated by Google

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

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

Notas sobre algoritmos do GoalKicker.com para profissionais 54


Machine Translated by Google

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

Vamos para lá:

Notas sobre algoritmos do GoalKicker.com para profissionais 55


Machine Translated by Google

Finalmente, podemos ver que temos uma casa vencedora ao nosso lado, então avançamos para lá e terminamos.

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

Definição de 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-se para 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 alcançar o
estado final a partir do estado inicial.

Estado inicial:

_ 13
425
786

Notas sobre algoritmos do GoalKicker.com 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 xey são coordenadas de células no estado atual
p e q são coordenadas de células 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 de determinado estado inicial
estado

Solução para 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 1 no estado final. Mesmo
vale para 2, 5, 6. _ está a 2 distâncias horizontais e 2 distâncias verticais. Portanto, o valor total para h(n) é 1 + 1 + 1 + 1 +

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

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

ou para baixo.

Portanto, os estados obtidos após mover 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 resulta ser
6 e 7 respectivamente. Escolhemos o estado com custo mínimo que é o estado (1). Os próximos movimentos possíveis podem ser para a esquerda,
Direita ou Baixo. Não nos moveremos para a esquerda como estávamos anteriormente naquele estado. Então, podemos mover para a direita ou para baixo.

Novamente encontramos os estados obtidos em (1).

13 _ 123

Notas sobre algoritmos do GoalKicker.com para profissionais 57


Machine Translated by Google
425 4 _ 5
786 786
(3) (4)

(3) leva à função de custo igual a 6 e (4) leva a 4. Além disso, consideraremos (2) obtido antes do qual tem custo
função igual a 7. Escolher o mínimo entre eles leva a (4). Os próximos movimentos possíveis podem ser para a esquerda, para a direita ou para baixo.
Obtemos estados:

123 123 123


_ 45 45 _ 485
786 786 76_

(5) (6) (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 e para baixo e claramente para baixo
nos levará ao estado final levando ao valor da função heurística igual a 0.

Notas sobre algoritmos do GoalKicker.com para profissionais 58


Machine Translated by Google

Capítulo 13: Algoritmo A* Pathfinding


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

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

Seção 13.1: Exemplo simples de descoberta de caminhos A*: um labirinto


sem obstáculos
Digamos que temos a seguinte grade 4 por 4:

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

Notas sobre algoritmos do GoalKicker.com para profissionais 59


Machine Translated by Google

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

Para escolher para qual quadrado passar, precisamos levar em consideração 2 heurísticas:

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


2. O valor "h" - É 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(from.x - to.x) + abs(from.y - to.y)

Isso é 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 obter o valor “f”: 1 + 4 = 5

Portanto, o valor final deste nó é “5”.

Vamos fazer o mesmo com todos os outros quadrados azuis. O grande número 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 sobre algoritmos do GoalKicker.com para profissionais 60


Machine Translated by Google

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

Aquele que tiver o menor valor de f.

Porém, neste caso, temos 2 nós com o mesmo valor de f, 5. Como escolhemos entre eles?

Simplesmente escolha um aleatoriamente ou tenha um conjunto de prioridades. Normalmente prefiro ter uma prioridade assim: "Direita > Cima >
Baixo > Esquerda"

Um dos nós com valor f 5 nos leva na direção “Baixo” e o outro nos leva na direção “Esquerda”. Como Baixo tem prioridade mais alta que Esquerda,

escolhemos o quadrado que nos leva para "Baixo".

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

Notas sobre algoritmos do GoalKicker.com 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 de f:

Notas sobre algoritmos do GoalKicker.com para profissionais 62


Machine Translated by Google

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

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

Notas sobre algoritmos do GoalKicker.com para profissionais 63


Machine Translated by Google

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

Vamos para lá:

Notas sobre algoritmos do GoalKicker.com para profissionais 64


Machine Translated by Google

Finalmente, podemos ver que temos uma casa vencedora ao nosso lado, então avançamos para lá e terminamos.

Notas sobre algoritmos do GoalKicker.com para profissionais 65


Machine Translated by Google

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


A programação dinâmica é um conceito amplamente utilizado e frequentemente utilizado para otimização. Refere-se à simplificação
de um problema complicado, dividindo-o em subproblemas mais simples de maneira 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 ideal" e "Subproblemas sobrepostos". Para conseguir sua otimização, a programação dinâmica utiliza um conceito
chamado memoização

Seção 14.1: Editar distância

A declaração do problema é como se recebêssemos duas strings str1 e str2, então quantos números mínimos de operações
podem ser executadas no str1 para que ele seja convertido em str2.

Implementação em Java

classe pública EditDistance {

public static void main(String[] args) {


// TODO stub de método gerado automaticamente
String str1 = "março";
String str2 = "carrinho";

EditDistance ed = new EditDistance();


System.out.println(ed.getMinConversions(str1, str2));
}

public int getMinConversions(String str1, String str2){


int dp[][] = new int[str1.length()+1][str2.length()+1]; for(int
i=0;i<=str1.length();i++){
for(int j=0;j<=str2.length();j++){ if(i==0) dp[i][j]
= j; senão
if(j==0) dp[i][j] =
i; senão
if(str1.charAt(i-1)
== str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1]; senão { dp[i][j] = 1
+ Math.min(dp[i-1][j],

Math.min(dp[i][j-1], dp[i-1][j-1 ]));


}
}

} retornar dp[str1.length()][str2.length()];
}

Saída

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


O Algoritmo de Agendamento de Trabalho Ponderado também pode ser denotado como Algoritmo de Seleção de Atividade Ponderada.

O problema é que, dados certos trabalhos com seu horário de início e de término, e o lucro que você obtém ao terminar o trabalho, qual é o
lucro máximo que você pode obter, dado que não há dois trabalhos que possam ser executados em paralelo?

Notas sobre algoritmos do GoalKicker.com para profissionais 66


Machine Translated by Google

Este se parece com a seleção de atividades usando o algoritmo ganancioso, mas há uma diferença adicional. Ou seja, em vez de
maximizando o número de trabalhos concluídos, nos concentramos em obter o lucro máximo. O número de trabalhos realizados
não importa aqui.

Vejamos um exemplo:

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | A | B | C | D | 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 indicados com um nome, horário de início e término e lucro. Depois de algumas iterações, podemos descobrir se
realizamos Job-A e Job-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. porque nós fazemos isso? É porque
se selecionarmos um trabalho que leva menos tempo para ser concluído, reservaremos mais tempo para escolher outros trabalhos. Nós
ter:

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 vai
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] inicialmente deterá o lucro da realização do i-ésimo
trabalho.

+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_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 a
i-1 e após cada iteração, incrementaremos i em 1, até que i se torne n+1.

j eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

Notas sobre algoritmos do GoalKicker.com 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 do Job[j] é maior que o horário de início do Job[i] , então estes
dois trabalhos não podem ser feitos juntos. Porém, se eles não se sobrepuserem, verificaremos se Acc_Prof[j] + Profit[i] > Acc_Prof[i]. Se
for esse o caso, atualizaremos Acc_Prof[i] = Acc_Prof[j] + Profit[i]. Aquilo é:

if Trabalho[j].finish_time <= Trabalho[i].start_time


if 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 realizar esses dois trabalhos juntos. Vamos verificar
nosso exemplo:

Aqui Job[j] se sobrepõe a Job[i]. Portanto, isso não pode ser feito juntos. Como nosso j é igual a i-1, incrementamos o
valor de i para i+1 que é 3. E fazemos j = 1.

j eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

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

j eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_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,

j eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_Prof | 5 | 6 | 10 | 4 | 11 | 2 |

Notas sobre algoritmos do GoalKicker.com 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 aumente j em 1.

j eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_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
atualize novamente Acc_Prof[i] = 10. Incrementamos j em 1. Obtemos:

j eu

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_Prof | 5 | 6 | 10 | 10 | 11 | 2 |
+--------------+---------+---------+--- ------+---------+---------+------------+

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

+--------------+---------+---------+--- ------+---------+---------+------------+
| Nome | D | A | 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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
| Acc_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 (Job)


classifique o trabalho de acordo com o horário de término em ordem não decrescente
para eu -> 2 para n
para j -> 1 para i-1
if Trabalho[j].finish_time <= Trabalho[i].start_time
if Acc_Prof[j] + Lucro[i] > Acc_Prof[i]
Acc_Prof[i] = Acc_Prof[j] + Lucro[i]

69
Notas sobre algoritmos do GoalKicker.com para profissionais
Machine Translated by Google
fim se
fim se
fim para
fim para

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

A complexidade de preencher a matriz Acc_Prof é O(n2). A travessia da matriz leva O (n). Portanto, a complexidade total deste 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 Acc_Prof
corresponder a maxProfit, colocaremos o nome do trabalho em uma pilha e subtrairemos o Lucro de aquele trabalho de maxProfit. Faremos isso até

que nosso maxProfit > 0 ou alcancemos 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]
S.push(Job[i].name
maxProfit = maxProfit - Job[i].profit endif endfor

A complexidade deste procedimento é: O(n).

Uma coisa a lembrar: se houver vários agendamentos de trabalho que possam nos proporcionar o lucro máximo, só poderemos encontrar um agendamento
de trabalho por meio deste procedimento.

Seção 14.3: Subsequência Comum Mais Longa


Se recebermos as duas strings, teremos que encontrar a subsequência comum mais longa 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


Método gerado automaticamente stub
String str1 = "AGGTAB";
String str2 = "GXTXAYB"; LCS
obj = novo LCS();
System.out.println(obj.lcs(str1, str2, str1.length(), str2.length())); System.out.println(obj.lcs2(str1,
str2));
}

// Função recursiva
public int lcs(String str1, String str2, int m, int n){

Notas sobre algoritmos do GoalKicker.com para profissionais 70


Machine Translated by Google

se(m==0 || n==0)
retornar 0;
if(str1.charAt(m-1) == str2.charAt(n-1))
retornar 1 + lcs(str1, str2, m-1, n-1);
outro
retornar Math.max(lcs(str1, str2, m-1, n), lcs(str1, str2, m, n-1));
}

// Função iterativa
public int lcs2(String str1, String str2){
int lcs[][] = new int[str1.length()+1][str2.length()+1];

for(int i=0;i<=str1.length();i++){
for(int j=0;j<=str2.length();j++){
se(eu==0 || j== 0){
lcs[i][j] = 0;
}
senão if (str1.charAt(i-1) == str2.charAt(j-1)){
lcs[i][j] = 1 + lcs[i-1][j-1];
}outro{
lcs[i][j] = Math.max(lcs[i-1][j], lcs[i][j-1]);
}
}
}

retornar lcs[str1.length()][str2.length()];
}

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)
\
/ fib(1) fib(0)

Subproblemas sobrepostos

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

Implementação

public int fib(int n){


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

Notas sobre algoritmos do GoalKicker.com para profissionais 71


Machine Translated by Google

f[0]=0;f[1]=1;
for(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 substring comum mais longa é "abcd" e tem comprimento 4.

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

A substring comum mais longa é "abcdez" e tem comprimento 6.

Implementação em Java

public int getLongestCommonSubstring(String str1,String str2){


int arr[][] = new int[str2.length()+1][str1.length()+1]; int máx =
Inteiro.MIN_VALUE; for(int
i=1;i<=str2.length();i++){
for(int j=1;j<=str1.length();j++)
{ if(str1.charAt(j-1) == str2.charAt(i-1)){ arr[i][j] = arr
[i-1][j-1]+1; if(arr[i][j]>max) max =
arr[i][j];

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

} retornar máximo;
}

Complexidade de tempo

O(m*n)

Notas sobre algoritmos do GoalKicker.com 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 se repetem. Se você

conseguir identificar um subproblema simples que é calculado repetidamente, é provável que exista uma abordagem de programação dinâmica para o problema.

Como este tópico é intitulado Aplicações de Programação Dinâmica, ele se concentrará mais em 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 principal para programação dinâmica, já que 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 a primeira ramificação recursiva

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

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

ser observado é que o tempo de execução é exponencial, o que significa que o tempo de execução 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)

memo = []
memo.append(1) # f(1) = 1
memo.append(1) # f(2) = 1

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

Notas sobre algoritmos do GoalKicker.com para profissionais 73


Machine Translated by Google

resultado = fibonacci(n-1) + fibonacci(n-2)


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

Com a abordagem memorizada, introduzimos um array que pode ser considerado como todas as chamadas de função anteriores. O
memorando de localização[n] é o resultado da chamada de função fibonacci(n). Isso nos permite trocar a complexidade de espaço de O(n) por
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):


memo.append(memo[i-1] + memo[i-2])

memorando de devolução [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 desse
árvore recursiva como visto acima.

Com esta informação, 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é n.

O principal benefício aqui é que agora eliminamos a pilha recursiva enquanto mantemos 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
principal observação a ser feita para chegar à complexidade do espaço para O(1) (constante) é a mesma observação que fizemos
para a pilha recursiva - só precisamos de fibonacci(n-1) e fibonacci(n-2) para construir fibonacci(n). Isso significa que só precisamos
salvar os resultados de fibonacci(n-1) e fibonacci(n-2) em qualquer ponto de nossa iteração.

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

Eu adiciono os dois índices do array 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 notar que às vezes pode ser melhor encontrar uma solução iterativa memorizada para

Notas sobre algoritmos do GoalKicker.com para profissionais 74


Machine Translated by Google

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

Notas sobre algoritmos do GoalKicker.com para profissionais 75


Machine Translated by Google

Capítulo 16: Algoritmo de Kruskal


Seção 16.1: Implementação ideal baseada em conjunto disjunto
Podemos fazer duas coisas para melhorar os subalgoritmos de conjuntos disjuntos simples e abaixo do ideal:

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

subalgo findSet (v: um nó): se


v.parent ! = v v.parent
= findSet (v.parent) retornar v.parent

2. Heurística de fusão 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 vRoot.height < uRoot.height:


vRoot.parent = uRoot else
if vRoot.height > uRoot.height: uRoot.parent =
vRoot else:

uRoot.parent = vRoot
uRoot.height = uRoot.height + 1

Isso leva ao tempo O(alpha(n)) para cada operação, onde alfa é o inverso da função de Ackermann de crescimento rápido, portanto,
seu 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 do caminho pode reduzir a altura da árvore, portanto, comparar as 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:
vRoot.parent = uRoot
senão:
uRoot.parent = vRoot

Na prática, este algoritmo aleatório junto com a compactação de caminho para a operação findSet resultará em

Notas sobre algoritmos do GoalKicker.com 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 com eficiência com a detecção de ciclo, consideramos cada nó como parte de uma árvore. Ao adicionar uma aresta,
verificamos se seus dois nós componentes fazem parte de árvores distintas. Inicialmente, cada nó forma uma árvore de um nó.

algoritmo kruskalMST (G: um gráfico)


classifica as arestas de G por seu
valor MST = uma floresta de árvores, inicialmente cada árvore é um nó no gráfico para cada
aresta e em G: se a raiz da
árvore à qual e.first pertence é não é o mesmo que a raiz da árvore à qual e.second
pertence: conecte uma das raízes à outra, fundindo assim duas
árvores

devolver o 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 disjunta, que envolve três operações principais:

subalgo makeSet(v: um nó): <- faça


=v uma nova árvore com raiz em v v.parent

subalgo findSet (v: um nó): se


v.parent == v: retornar
v
retornar findSet(v.parent)

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


findSet(v) uRoot =
findSet(u)

uRoot.parent = vRoot

algoritmo kruskalMST (G: um gráfico):


classifique as arestas de G por seu
valor para cada nó n em G:
makeSet(n)
para cada aresta e em G: if
findSet(e.first) != findSet(e.second): unionSet(e.first,
e.second)

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 de classificação, caso não crie um ciclo.

algoritmo kruskalMST (G: um gráfico)


classifica as arestas de G por seu valor
MST = um gráfico vazio
para cada aresta e em G:
se adicionar e ao MST não cria um ciclo:
adicione e ao MST

Notas sobre algoritmos do GoalKicker.com para profissionais 77


Machine Translated by Google

retornar MST

Notas sobre algoritmos do GoalKicker.com para profissionais 78


Machine Translated by Google

Capítulo 17: Algoritmos gananciosos


Seção 17.1: Codificação Humana
Código de Huffman é um tipo específico de código de prefixo ideal comumente usado para compactação de dados sem perdas.
Comprime dados de forma muito eficaz, economizando de 20% a 90% de memória, dependendo das características dos dados que
estão sendo compactados. Consideramos os dados como uma sequência de caracteres. O algoritmo ganancioso de Huffman usa uma
tabela que fornece a frequência com que cada caractere ocorre (ou seja, sua frequência) para construir uma maneira ideal 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 existem
apenas 6 caracteres diferentes nesse arquivo. A frequência dos caracteres é dada por:

+-------------+-----+-----+-----+-----+ -----+-----+
| Personagem | uma | b | c | e | e | f |
+-------------+-----+-----+-----+-----+ -----+-----+
|Frequência (em milhares)| 45 | 13 | 12 | 16 | 9 | 5 |
+-------------+-----+-----+-----+-----+ -----+-----+

Temos muitas opções de como representar esse arquivo de informações. Aqui, consideramos o problema de projetar um código de
caracteres binários no qual cada caractere é representado por uma sequência binária única, que chamamos de palavra-código.

A árvore construída nos fornecerá:

+-------------+-----+-----+-----+-----+ -----+-----+
| Personagem | uma | b | c | e | 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, precisaremos 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 sobre algoritmos do GoalKicker.com 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, fornecendo
palavras-código curtas de caracteres frequentes e palavras-código longas de caracteres raros. 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. Eles são chamados de 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.

Os códigos de prefixo são desejáveis porque simplificam a decodificação. Como nenhuma palavra-código é 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 é
analisado exclusivamente como 0.0.101.1101, que é decodificado para aabe. Resumindo, 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 se deve 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 um array 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 próprio
símbolo, sua frequência e, opcionalmente, um link para seus nós filhos. Por 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 frequência mais baixa quando aberto. O
processo é descrito abaixo:

1. Crie um nó folha para cada símbolo e adicione-o à fila de prioridade.


2. Embora haja 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
frequência dos dois nós.
3. Adicione o novo nó à fila.
3. O nó restante é o nó raiz e a árvore Huffman está completa.

Para nosso exemplo:

Notas sobre algoritmos do GoalKicker.com 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 = tamanho C.
Q = priority_queue() para i =
1 para nn = node(C[i])

Q.push(n)
end for
while Q.size() não é igual a 1 Z = new node()

Z.esquerda = x = Q.pop
Z.direita = y = Q.pop
Z.frequência = x.frequência + y.frequência Q.push(Z) final
enquanto

Retorno Q

Embora o tempo linear forneça entrada classificada, em casos gerais de entrada arbitrária, o uso deste algoritmo requer pré-
classificação. Assim, como a classificaçã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 do 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 em valores de bytes individuais,
geralmente percorrendo 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 da folha representa o desejado

Notas sobre algoritmos do GoalKicker.com para profissionais 81


Machine Translated by Google

personagem. Normalmente a Árvore de Huffman é construída usando dados ajustados estatisticamente em cada ciclo de compressão,
portanto a reconstrução é bastante simples. Caso contrário, as informações para reconstruir a árvore deverão ser enviadas separadamente.
O pseudocódigo:

Procedimento HuffmanDecompression(root, S): // root representa a raiz da árvore Huffman n := S.length // S refere-se ao fluxo de bits
a ser descompactado para i := 1 a n

atual = raiz
enquanto current.left != NULL e current.right != NULL se S[i] for igual a '0'

atual := atual.esquerda
senão
atual := atual.right endif

i := i+1
endwhile
imprime current.symbol endfor

Explicação gananciosa:
a codificação Huffman analisa a ocorrência de cada caractere e o armazena como uma string binária de maneira ideal. 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 baixo para cima, para que
os dois caracteres menos frequentes estejam o mais longe possível da raiz. Desta forma, o caractere mais frequente obtém o
código menor e o caractere menos frequente obtém o código maior.

Referências:

Introdução aos Algoritmos - Charles E. Leiserson, Clifford Stein, Ronald Rivest e Thomas H. Cormen Huffman
Coding - Wikipedia Matemática
Discreta e suas Aplicações - Kenneth H. Rosen

Seção 17.2: Problema de seleção de atividades


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 realizar mais de uma atividade ao mesmo tempo. Sua tarefa é encontrar uma maneira de realizar o número máximo de atividades.

Por exemplo, suponha que você tenha uma seleção de classes para escolher.

Nº da atividade horário de início


horário 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 cursar as aulas 1 e 2 porque
elas compartilham um horário comum, das 10h30 às 11h00. No entanto, você pode cursar as aulas 1 e 3 porque elas não
compartilham um horário comum. Portanto, sua tarefa é fazer o máximo de aulas possível, sem qualquer sobreposição. Como você
pode fazer isso?

Análise

Notas sobre algoritmos do GoalKicker.com para profissionais 82


Machine Translated by Google

Vamos pensar na solução por meio de uma abordagem gananciosa. Primeiro de tudo, escolhemos aleatoriamente alguma abordagem e verificamos se isso irá
trabalhar ou não.

classifique a atividade por horário de início , o que significa que atividade iniciará primeiro, iremos realizá-la primeiro. então leve primeiro para
último da lista classificada e verifique se ele cruzará com a atividade anterior ou não. Se a atividade atual não for
cruzar com a atividade realizada anteriormente, realizaremos a atividade, caso contrário não realizaremos. esse
abordagem funcionará para alguns casos como

Nº da atividade 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á realizada e a atividade 2 será ignorada.
serão realizadas no máximo 3 atividades. Funciona para este tipo de casos. mas falhará em alguns casos. Vamos aplicar
esta abordagem para o caso

Nº da atividade hora de início hora de término


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á realizada, mas a resposta pode ser atividade 1-->3 ou 2-
->3 será executado. Portanto, nossa abordagem não funcionará no caso acima. Vamos tentar outra abordagem

Classifique a atividade por duração, o que significa realizar primeiro a atividade mais curta. que pode resolver o anterior
problema . Embora o problema não esteja completamente resolvido. Ainda existem alguns casos que podem falhar na solução.
aplique esta abordagem no caso abaixo.

Nº da atividade hora de início hora de término


16h00 11h40
2 11h30 12h00
3 23h40 14h00

se classificarmos a atividade por duração, a ordem de classificação será 2 -> 3 ---> 1, . e se realizarmos a atividade nº 2 primeiro, então
nenhuma outra atividade poderá ser executada. Mas a resposta será realizar a atividade 1 e depois realizar 3 no . Então podemos realizar
máximo 2 atividades. Portanto, esta não pode ser uma solução para este problema. Deveríamos tentar uma abordagem diferente.

A solução

Classifique a atividade pelo horário de término , o que significa que a atividade termina primeiro. o algoritmo é dado
abaixo

1. Classifique as atividades pelos horários de término.


2. Caso a atividade a ser realizada não compartilhe um horário comum com as atividades que anteriormente
realizado, execute a atividade.

Vamos analisar o primeiro exemplo

Notas sobre algoritmos do GoalKicker.com para profissionais 83


Machine Translated by Google

Nº da atividade hora de início hora de


término 10h20 11h00 1
2 10h30 11h30
3 11h00 12h00
4 10h00 11h30
5 9h00 11h00

classificar a atividade por seus horários , Portanto, a ordem de classificação será 1 -> 5 -> 2 -> 4 -> 3.. a resposta é 1 -> 3 essas duas atividades
de término será realizada. e essa é a resposta. aqui está o código sudo.

1. classificar: atividades

2. realizar a primeira atividade da lista ordenada de atividades.


3. Definir: Atividade_atual: = primeira atividade
4. definir: horário_final: = horário_final da atividade atual
5. ir para a próxima atividade se existir, se não existir .

encerrar 6. se horário_inicial da atividade atual <= horário_final: realizar a atividade e ir para 4 7.


senão: chegou a 5.

veja aqui para ajuda de codificação http://www.geeksforgeeks.org/greedy-algorithms-set-1-activity-selection-problem/

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 quantia.

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 os cêntimos) forem de 1€, 2€, 5€, 10€, dar a moeda
ou nota mais alta até atingirmos o valor e repetir este procedimento levará ao conjunto mínimo de moedas .

Podemos fazer isso recursivamente com OCaml:

(* assumindo que o sistema monetário está classificado em ordem


decrescente *) deixe change_make
money_system amount = deixe
rec loop dado amount = se

amount = 0 então dado else (* encontramos o primeiro valor menor ou igual ao valor
restante *) let coin = List.find ((>=) quantidade) money_system
em loop (moeda::dado) (quantia - moeda)
em loop [] quantidade

Esses sistemas são feitos para que a mudança seja fácil. O problema fica mais difícil quando se trata de um sistema monetário arbitrário.

Caso Geral. Como dar 99€ com moedas de 10€, 7€ e 5€? Aqui, dar moedas de 10€ até ficarmos com 9€ não leva obviamente a nenhuma
solução. Pior que isso, uma solução pode não existir. Este problema é de fato np-difícil, mas existem soluções aceitáveis que misturam
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 leva a 0.

Aqui está uma função recursiva OCaml correspondente a este método. Retorna None, se não existir solução.

Notas sobre algoritmos do GoalKicker.com para profissionais 84


Machine Translated by Google

(* utilitários de opção *) deixe


optmin xy = combinar
x, y com
| Nenhum, um | a,Nenhum -> a
| Alguns x, Alguns y-> Alguns (min x y)

deixe optsucc = função


| Algum x -> Algum (x+1)
| Nenhum -> Nenhum

(* Problema de mudança *) deixe


change_make money_system amount = deixe rec
loop n = deixe
onepiece acc piece = combine 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*)
None
else
(*procuramos o menor caminho diferente de None com as peças restantes*) optmin (optsucc (loop x)) acc

em

(*chamamos uma peça por todas as peças*)


List.fold_left onepiece Nenhum money_system
em quantidade de loop

Nota: Podemos observar que este procedimento pode calcular várias vezes a mudança definida para o mesmo valor. Na
prática, usar a memoização para evitar essas repetições leva a resultados mais rápidos (muito mais rápidos).

Notas sobre algoritmos do GoalKicker.com para profissionais 85


Machine Translated by Google

Capítulo 18: Aplicações da técnica gananciosa

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 solicitações de itens que devem ter sido colocadas no cache antes de serem processadas. Claro,
se m<=k então apenas colocamos todos os elementos no cache e ele 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 colocar o item solicitado no cache e despejar outro, assumindo que o cache esteja cheio. A Meta é um
cronograma de despejos que minimize o número de despejos.

Existem inúmeras estratégias gananciosas para este 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
recente é removida 3. Última saída recente (LRU): Página despejada cujo acesso mais
recente foi o mais antigo 4 • Solicitada com menos frequência (LFU): Exclui a página que foi solicitada
com menos frequência 5. Distância de encaminhamento mais longa (LFD): Exclui a página do cache que não é solicitada até o futuro mais dist

Atenção: Para os exemplos a seguir, removemos a página com o menor índice, caso mais de uma página possa ser
removida.

Exemplo (FIFO)

Deixe o tamanho do cache ser 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 cache aadebbacfdeafbec 1


aaddddaaadddfffc
cache 2 bbbeeeeccceeebbb
cache 3 ccccbbbbfffaaaee
falta de cache xxxxxxxxxxxx

Treze falhas de cache por dezesseis solicitações não parecem muito ideais, vamos tentar o mesmo exemplo com outra
estratégia:

Exemplo (LFD)

Deixe o tamanho do cache ser 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 cache aadebbacfdeafbec 1


aadeeeeeeeeeeeec
cache 2bbbbbbaaaaaffff _
cache 3 ccccccccfdddbbbb
falta de cache xx xxx xxx

Oito falhas 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 sobre algoritmos do GoalKicker.com para profissionais 86


Machine Translated by Google

O esqueleto é uma aplicação que resolve o problema dependendo da estratégia gananciosa escolhida:

#include <iostream>
#incluir <memória>

usando namespace std;

const int cacheSize const = 3;


int requestLength = 16;

solicitação de const char [] = {'a','a','d','e','b','b','a','c','f','d','e','a', 'f','b','e','c'}; = {'a','b','c'};


cache de char []

// para redefinir
o caractere originalCache[] = {'a','b','c'};

estratégia de classe {

público:
Estratégia(std::string nome) : estratégiaNome(nome) {} virtual
~Estratégia() = padrão;

// 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 nomeestratégia;


};

bool updateCache(int requestIndex, Estratégia* estratégia) {

// calcula onde colocar a solicitação int


cachePlace = strategy->apply(requestIndex);

// prova se é um acerto ou erro de cache bool isMiss =


request[requestIndex] != cache[cachePlace];

// atualiza a estratégia (por exemplo, distâncias de recontagem)


strategy->update(cachePlace, requestIndex, isMiss);

// escreve no cache
cache[cachePlace] = request[requestIndex];

retornar éMiss;
}

int principal()
{
Estratégia* selecionadaEstratégia[] = { novo FIFO, novo LIFO, novo LRU, novo LFU, novo LFD };

for (int camada=0; camada < 5; ++camada) {

//redefinir cache
for (int i=0; i < cacheSize; ++i) cache[i] = originalCache[i];

<<"\nEstratégia: " << estratégia selecionada[strat]->nomedaestratégia << endl; cout

cout << "\nCache inicial: ("; for (int i=0;


i < cacheSize-1; ++i) cout << cache[i] << ",";

Notas sobre algoritmos do GoalKicker.com para profissionais 87


Machine Translated by Google

cout << cache[cacheSize-1] << ")\n\n";

cout << "Solicitação\t"; for


(int i=0; i < cacheSize; ++i) cout << "cache " cout << "cache miss" << << eu << "\t";
endl;

int cntMisses = 0;

for(int i=0; i<requestLength; ++i) {

bool isMiss = updateCache(i, estratégia selecionada[strat]); if (isMiss) +


+cntMisses;

" "
conta << << solicitação[i] << "\t";
" "
for (int l=0; l < cacheSize; ++l) cout << cout << (isMiss ? << cache[l] << "\t";
"x" : "") << endl;
}

"
cout<< "\nTotal de falhas de cache: << cntMisses << endl;
}

for(int i=0; i<5; ++i) excluir estratégia selecionada[i];


}

A ideia básica é simples: para cada solicitação tenho duas chamadas e duas da minha estratégia:

1. aplicar: A estratégia deve informar ao chamador qual página usar. 2.


atualizar: Após o chamador usar o local, ela informa à estratégia se errou ou não. Então a estratégia poderá atualizar os seus dados internos.
A estratégia LFU, por exemplo, precisa atualizar a frequência de acertos das páginas de cache, enquanto a estratégia LFD precisa
recalcular as distâncias das páginas de cache.

Agora vamos ver exemplos de implementações para nossas cinco estratégias:

FIFO

classe FIFO : estratégia pública { público:

FIFO() : Estratégia("FIFO") {

for (int i=0; i<cacheSize; ++i) idade[i] = 0;


}

int apply(int requestIndex) substituir {

int mais antigo = 0;

for(int i=0; i<cacheSize; ++i) {

if(cache[i] == request[requestIndex]) return i;

senão if(idade[i] > idade[mais velha])


mais velho = i;
}

retornar o mais antigo;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituir {

// nada mudou, não precisamos atualizar as idades

Notas sobre algoritmos do GoalKicker.com para profissionais 88


Machine Translated by Google

if(!cacheMiss)
retornar;

// todas as páginas antigas envelhecem, as novas ganham 0


for(int i=0; i<cacheSize; ++i)
{
se (eu ! = cachePos)
idade[i]++;

outro
idade[i] = 0;
}
}

privado:
int idade[cacheSize];
};

O FIFO só precisa da informação de quanto tempo uma página está no cache (e, claro, apenas em relação às outras páginas). Então
a única coisa a fazer é esperar um erro e depois tornar as páginas que não foram despejadas mais antigas. Para nosso exemplo
acima a solução do programa é:

Estratégia: FIFO

Inicial do cache: (a,b,c)

Solicitar cache 0 esconderijo 1 esconderijo 2 falta de cache


a a c
a a c
d d bbb c x
e e c x
e x
bb ddd e bb
a a e x
c a c x
f a c x
d c melhor amigo x
e e f x
a e a x
f df e a x
b a x
e aff e x
c c bbb e x

Perdas totais de cache: 13

Essa é exatamente a solução acima.

LIFO

classe LIFO : estratégia pública {


público:
LIFO() : Estratégia("LIFO")
{
for (int i=0; i<cacheSize; ++i) idade[i] = 0;
}

substituição int apply(int requestIndex)


{
int mais novo = 0;

Notas sobre algoritmos do GoalKicker.com para profissionais 89


Machine Translated by Google

for(int i=0; i<cacheSize; ++i)


{
if(cache[i] == solicitação[requestIndex])
retornar eu;

senão if(idade[i] < idade[mais recente])


mais novo = eu;
}

retornar o mais novo;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituição


{
// nada mudou, não precisamos atualizar as idades
if(!cacheMiss)
retornar;

// todas as páginas antigas envelhecem, as novas ganham 0


for(int i=0; i<cacheSize; ++i)
{
se (eu ! = cachePos)
idade[i]++;

outro
idade[i] = 0;
}
}

privado:
int idade[cacheSize];
};

A implementação do LIFO é mais ou menos igual à do FIFO , mas despejamos a página mais jovem e não a mais antiga. O
os resultados do programa são:

Estratégia: LIFO

Inicial do cache: (a,b,c)

Solicitar cache 0 cache 1b esconderijo 2 falta de cache


a a _ c
a a c
d d c x
e e bbb c x
e c
bb e c
a a bbb c x
c a c
c x
fd fd bbb c x
e e b c x
a a c x
c x
Facebook aff bbb c
e e c x
c e bb c

Perdas totais de cache: 9

LRU

Notas sobre algoritmos do GoalKicker.com para profissionais


90
Machine Translated by Google

classe LRU : estratégia pública {


público:
LRU() : Estratégia("LRU")
{
for (int i=0; i<cacheSize; ++i) idade[i] = 0;
}

// aqui mais antigo significa não usado há mais tempo


substituição int apply(int requestIndex)
{
int mais antigo = 0;

for(int i=0; i<cacheSize; ++i)


{
if(cache[i] == solicitação[requestIndex])
retornar eu;

senão if(idade[i] > idade[mais velha])


mais antigo = eu;
}

retornar o mais antigo;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituição


{
// todas as páginas antigas envelhecem, as usadas ganham 0
for(int i=0; i<cacheSize; ++i)
{
se (eu ! = cachePos)
idade[i]++;

outro
idade[i] = 0;
}
}

privado:
int idade[cacheSize];
};

No caso do LRU a estratégia é independente do que está na página de cache, seu único interesse é a última utilização. O

os resultados do programa são:

Estratégia: LRU

Inicial do cache: (a,b,c)

Solicitar cache 0 esconderijo 1 esconderijo 2 falta de cache


a a c
a a c
d a bbd c x
e a e x
e x
bb bb ddd e

a b a e x
c a c x
a c x
fd melhor amigo d c x
e f e x
a a dd e x

91
Notas sobre algoritmos do GoalKicker.com para profissionais
Machine Translated by Google

f a f e x
b a x
e e aff x
c e c bbb x

Perdas totais de cache: 13

LFU

classe LFU : estratégia pública {


público:
LFU() : Estratégia("LFU")
{
for (int i=0; i<cacheSize; ++i) requestFrequency[i] = 0;
}

substituição int apply(int requestIndex)


{
pelo menos = 0;

for(int i=0; i<cacheSize; ++i)


{
if(cache[i] == solicitação[requestIndex])
retornar eu;

senão if(requestFrequency[i] < requestFrequency[mínimo])


menos = eu;
}

retorne menos;
}

void update(int cachePos, int requestIndex, bool cacheMiss) substituição


{
if(cacheMiss)
requestFrequency[cachePos] = 1;

outro
++requestFrequency[cachePos];
}

privado:

// com que frequência a página foi usada


int requestFrequency[cacheSize];
};

LFU despeja a página usada com menos frequência. Portanto, a estratégia de atualização é apenas contar todos os acessos. Claro que depois de uma falta o

contagem redefinida. Os resultados do programa são:

Estratégia: LFU

Inicial do cache: (a,b,c)

Solicitar cache 0 esconderijo 1 esconderijo 2 falta de cache


a a c
a a bb c
d a d c x
e a e x
a e x
bb a dbb e

a a b e

92
Notas sobre algoritmos do GoalKicker.com para profissionais
Machine Translated by Google
c a b c x
a x
fd a fd x
e a bbb e x
a a e

a x
Facebook a bbb aff
e a e x
c a bb c x

Perdas totais 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
for (int i=0; i<cacheSize; ++i) nextUse[i] = calcNextUse(-1, cache[i]);
}

substituição int apply(int requestIndex)


{
int mais recente = 0;

for(int i=0; i<cacheSize; ++i)


{
if(cache[i] == solicitação[requestIndex])
retornar eu;

senão if(nextUse[i] > nextUse[mais recente])


mais recente = eu;
}

retornar mais tarde;


}

void update(int cachePos, int requestIndex, bool cacheMiss) substituição


{
nextUse[cachePos] = calcNextUse(requestIndex, cache[cachePos]);
}

privado:

int calcNextUse(int requestPosition, char pageItem)


{
for(int i = requestPosition+1; i < requestLength; ++i)
{
if (solicitação[i] == pageItem)
retornar eu;
}

retornar comprimento da solicitação + 1;


}

// próximo uso da página


int nextUse[cacheSize];
};

A estratégia LFD é diferente de todas as anteriores. É a única estratégia que utiliza as solicitações futuras para seu
decisão de quem despejar. A implementação usa a função calcNextUse para obter a página que será o próximo uso

Notas sobre algoritmos do GoalKicker.com para profissionais 93


Machine Translated by Google

mais distante no futuro. A solução do programa é igual à solução manual acima:

Estratégia: LFD

Inicial do cache: (a,b,c)

Solicitar cache 0 cache 1bb esconderijo 2 falta de cache


a a _ c
a a c
d a d x
e a e x
b a bbb e

b a b e

a a b e

c a c e x
f a f e x
d a e x
e a e

a a ddd e

e x
Facebook e x
e fbb ddd e

c c d e x

Perdas totais 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 (ver fontes nos comentários abaixo).

Algoritmo vs Realidade

A estratégia LFD é ótima, mas há um grande problema. É uma solução offline ideal . Na práxis, o cache geralmente é
um problema online , isso significa que a estratégia é inútil porque não podemos agora, na próxima vez que precisarmos de um determinado
item. As outras quatro estratégias também são estratégias online . Para problemas on-line, precisamos de uma solução geral diferente
abordagem.

Seção 18.2: Ticket automático

Primeiro exemplo simples:

Você dispõe de uma máquina de bilhetes que dá troca em moedas com valores 1, 2, 5, 10 e 20. A dispensa do
a troca pode ser vista como uma série de moedas caindo até que o valor certo seja distribuído. Dizemos que uma dispensa é ótima
quando sua contagem de moedas é mínima para seu valor.

Seja M em [1,50] o preço da passagem 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 uma etapa como a diferença entre D e Dc com c a moeda que o automático distribui neste
etapa.

A técnica gananciosa para a troca é a seguinte abordagem pseudo algorítmica:

Etapa 1: enquanto D > 20 , distribua uma moeda de 20 e defina D = D - 20

Etapa 2: enquanto D > 10 , distribua uma moeda de 10 e defina D = D - 10

Etapa 3: enquanto D > 5 , distribua uma moeda de 5 e defina D = D - 5

Etapa 4: enquanto D > 2 , distribua uma moeda de 2 e defina D = D - 2

Etapa 5: enquanto D > 1 , distribua uma moeda de 1 e defina D = D - 1

Notas sobre algoritmos do GoalKicker.com para profissionais 94


Machine Translated by Google

Depois disso, 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 superior.

Agora o ticket automático como programa (em C++):

#include <iostream>
#include <vetor>
#include <string>
#include <algoritmo>

usando namespace std;

// lê alguns valores de moedas, classifica-os em ordem


decrescente, // limpa as cópias e garante que 1 moeda está nela
std::vector<unsigned int> readInCoinValues();

int principal()
{
std::vector<unsigned int> coinValues; // Array de valores de moedas crescentes int ticketPrice;
// M no exemplo
intpaidMoney ; //P no exemplo

// gera valores de moedas


coinValues = readInCoinValues();

cout << "preço do ingresso: "; cin


>> preço do bilhete;

cout << "dinheiro pago: "; cin >>


dinheiro pago;

if(paidMoney <= ticketPrice) {

cout << "Sem dinheiro para troca" << endl; retornar


1;
}

int diffValue = pagoMoney - ticketPrice;

// Aqui começa o ganancioso

// salvamos quantas moedas temos para distribuir


std::vector<unsigned int> coinCount;

for(auto coinValue = coinValues.begin(); coinValue !=


coinValues.end(); ++coinValue)
{
int contagemMoedas = 0;

while (diffValue >= *coinValue) {

diffValue -= *coinValue;
contarMoedas++;
}

coinCount.push_back(contarMoedas);
}

// imprime o resultado cout


<< "a diferença " é paga com: " << paidMoney - ticketPrice << endl;
<< "

Notas sobre algoritmos do GoalKicker.com para profissionais 95


Machine Translated by Google

for(unsigned int i=0; i < coinValues.size(); ++i) {

if(coinCount[i] > 0) cout <<


"
coinCount[i] << moedas com valor " << coinValues[i] << endl;

retornar 0;
}

std::vector<unsigned int> readInCoinValues() {

// valores da moeda
std::vector<unsigned int> coinValues;

// certifique-se de que 1 esteja no vetor


coinValues.push_back(1);

// lê os valores da moeda (atenção: o tratamento de erros é omitido) while(true) {

int coinValor;

cout << "Valor da moeda (<1 até parar): "; cin >>
valor da moeda;

if(coinValue > 0)
coinValues.push_back(coinValue);

senão
quebrar;
}

// classifica os
valores sort(coinValues.begin(), coinValues.end(), std::greater<int>());

// apaga cópias do mesmo valor auto


last = std::unique(coinValues.begin(), coinValues.end()); coinValues.erase(último,
coinValues.end());

// imprime o array
cout << "Valores da moeda: ";

for(auto i : coinValues) " ";


cout << eu <<

cout << endl;

retornar valores de moeda;


}

Esteja ciente de que agora há verificação de entrada para manter o exemplo simples. Um exemplo de saída:

Valor da moeda (<1 até parar): 2


Valor da moeda (<1 até parar): 4
Valor da moeda (<1 até parar): 7
Valor da moeda (<1 até parar): 9
Valor da moeda (<1 até parar): 14
Valor da moeda (<1 até parar): 4
Valor da moeda (<1 para parar): 0

Notas sobre algoritmos do GoalKicker.com 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 moeda com valor 4
1 moeda com valor 1

Contanto que 1 esteja nos valores da moeda, agora o algoritmo será encerrado, porque:

D diminui estritamente 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 é apenas polinomial 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 isto 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 exemplo simples de contador: as moedas são 1,3,4 e D=6. A solução ideal são claramente duas moedas de valor 3 , mas o
ganancioso escolhe 4 na primeira etapa, então tem que escolher 1 nas etapas dois e três. Portanto, não dá uma solução ideal. Um
possível algoritmo ideal para este exemplo é baseado em programação dinâmica.

Seção 18.3: Programação de intervalo


Temos um conjunto de empregos J={a,b,c,d,e,f,g}. Seja j em J um trabalho que começa em sj e termina em fj. Dois trabalhos são
compatíveis se não se sobrepõem. Uma imagem como exemplo:

O objetivo é encontrar o subconjunto máximo de empregos mutuamente compatíveis. Existem várias abordagens gananciosas para
este problema:

1. Primeiro horário de início: Considere os trabalhos em ordem crescente


de sj 2. Primeiro horário de término: 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 empregos conflitantes cj

A questão agora é: qual abordagem é realmente bem-sucedida? Hora de início antecipado definitivamente não, aqui está um contra-exemplo

Notas sobre algoritmos do GoalKicker.com para profissionais 97


Machine Translated by Google

O intervalo mais curto também não é o ideal

e o menor número de conflitos pode realmente parecer ideal, mas aqui está um caso problemático para esta abordagem:

O que nos deixa com o tempo de chegada mais cedo. O pseudocódigo é bastante simples:

1. Classifique os trabalhos por hora de término para 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 , conjunto 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ário de início do
trabalho const int startTimes[] = { 2, 3, 1, 4, 3, 2, 6, 7, 8, 9};

// Horário de término
do trabalho const int endTimes[] = { 4, 4 , 3, 5 , 5, 5, 8, 9, 9, 10};

usando namespace std;

int principal()
{
vetor<par<int,int>> trabalhos;

for(int i=0; i<jobCnt; ++i)


jobs.push_back(make_pair(startTimes[i], endTimes[i]));

// etapa 1: ordenar
sort(jobs.begin(), jobs.end(),[](pair<int,int> p1, pair<int,int> p2) { return p1.second <
p2.second; } );

Notas sobre algoritmos do GoalKicker.com para profissionais 98


Machine Translated by Google

// passo 2: conjunto vazio A


vector<int> A;

// etapa 3:
for(int i=0; i<jobCnt; ++i) {

trabalho automático =
empregos[i]; bool isCompatível = verdadeiro;

for( índice de trabalho automático : A)


{
// testa se o trabalho real e o trabalho de A são incompatíveis if(job.second >=
jobs[jobIndex].first && job.first <= jobs[jobIndex].second)

{
isCompatível = falso;
quebrar;
}
}

if(éCompatível)
A.push_back(i);
}

//etapa 4: imprimir A
cout << "Compatível: ";

for(auto i : A) cout
<< "(" << jobs[i].primeiro << "," << jobs[i].segundo << ") "; cout << endl;

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). Existe uma implementação ÿ(n log n) e o leitor interessado pode continuar
lendo abaixo (Exemplo Java).

Agora temos um algoritmo ganancioso para o problema de escalonamento de intervalos, mas ele é ideal?

Proposição: O algoritmo ganancioso com o primeiro tempo de término é ideal.

Prova:(por contradição)

Suponha que ganancioso não seja ótimo e i1,i2,...,ik denota o conjunto de empregos selecionados por ganancioso. Seja j1,j2,...,jm o
conjunto de tarefas em uma solução ótima com i1=j1,i2=j2,...,ir=jr para o maior valor possível de r.

A tarefa 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 com a maximalidade de r. Isso conclui a prova.

Este segundo exemplo demonstra que normalmente existem 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 java.util.Arrays;

Notas sobre algoritmos do GoalKicker.com para profissionais 99


Machine Translated by Google

importar java.util.Comparator;

classe Trabalho

{
int início, fim, lucro;

Trabalho(int início, int fim, int lucro) {

isto.start = início;
this.finish = terminar;
this.profit = lucro;
}
}

classe JobComparator implementa Comparator<Job> {

public int compare(Trabalho a, Trabalho


b) {
retornar a.finish < b.finish ? -1 : a.terminar == b.terminar ? 0 : 1;
}
}

classe pública WeightedIntervalScheduling {

static public int binarySearch(Job jobs[], índice int ) {

int lo = 0, hi = índice - 1;

while (lo <= oi) {

int médio = (lo + oi) / 2; if


(jobs[mid].finish <= jobs[index].start) {

if (trabalhos[mid + 1].finish <= jobs[index].start) lo = mid + 1;


caso contrário,

retorne no meio;

}
else oi = meio - 1;
}

retornar -1;
}

static public int agendamento(Job jobs[]) {

Arrays.sort(trabalhos, novo JobComparator());

int n =
trabalhos.comprimento; tabela
int [] = novo int[n]; tabela[0] = empregos[0].lucro;

for (int i=1; i<n; i++) {

int inclProf = empregos[i].lucro; int l =


binarySearch(empregos, i); if (l != -1)
inclProf +=
tabela[l];

tabela[i] = Math.max(inclProf, tabela[i-1]);

Notas sobre algoritmos do GoalKicker.com para profissionais 100


Machine Translated by Google

tabela de retorno [n-1];


}

public static void main(String[] args) {

Trabalhos de trabalho[] = {novo Trabalho(1, 2, 50), novo Trabalho(3,


5, 20), novo Trabalho(6, 19, 100), novo Trabalho(2, 100, 200)};

"
System.out.println(" O lucro ideal é + agenda(trabalhos));
}
}

E a saída esperada é:

O lucro ideal é 250

Seção 18.4: Minimizando Atrasos


Existem inúmeros problemas para minimizar atrasos, aqui temos um único recurso que só pode processar um trabalho por vez. A tarefa j
requer tj unidades de tempo de processamento e vence no tempo dj. se j começa no tempo sj terminará no tempo fj=sj+tj. Definimos o
atraso L=max{0,fj-dh} para todo 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
Lj -8 -5 -4 1 7 4

A solução L=7 obviamente não é ótima. Vejamos algumas estratégias gananciosas:

1. Menor tempo de processamento primeiro: agendar trabalhos em ordem crescente e tempo de


processamento j` 2. Prazo mais antigo primeiro: agendar trabalhos em ordem crescente
de prazo dj 3. Menor folga: agendar trabalhos em ordem crescente de folga dj-tj

É fácil ver que o menor tempo de processamento primeiro não é o ideal, um bom exemplo de contador é

12
tj 1 5
DJ 10 5

a solução de menor 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 tarefas por prazo para que d1<=d2<=...<=dn


2. Defina t=0
3. para j=1 a n
Atribuir trabalho j ao intervalo [t,t+tj]

Notas sobre algoritmos do GoalKicker.com para profissionais 101


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
lado, mas na variante online , onde as tarefas aparecem durante a execução.

Notas sobre algoritmos do GoalKicker.com 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 configurar linhas telefônicas entre essas casas. A borda entre as casas representa o custo de
estabelecer uma linha entre duas casas.

Nossa tarefa é montar as linhas de forma que todas as casas estejam conectadas e o custo de montar toda a conexão seja
mínimo. Agora, como descobrimos isso? Podemos usar o Algoritmo de Prim.

Algoritmo de Prim é um algoritmo ganancioso que encontra uma árvore geradora mínima para um gráfico não direcionado ponderado.
Isso significa que ele encontra um subconjunto de arestas que forma uma árvore que inclui todos os nós, onde o peso total de todas
as arestas da á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 examinar primeiro 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 Trees em um gráfico. A árvore geradora mínima de um gráfico 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 árvore geradora mínima,
que é como configurar as linhas telefônicas em nosso gráfico de exemplo de forma que o custo de configuração seja mínimo.

Primeiramente selecionaremos um nó de origem . Digamos que o nó-1 seja nossa fonte. Agora adicionaremos a aresta do nó 1 que
tem o custo mínimo ao nosso subgrafo. Aqui marcamos as arestas que estão no subgráfico usando a cor azul. Aqui 1-5 é

Notas sobre algoritmos do GoalKicker.com para profissionais 105


Machine Translated by Google
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, 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 vantagem 2-4 não nos beneficia. Selecionaremos as arestas de forma que adicione um novo nó em nosso subgrafo. Então nós

selecione a borda 4-8.

Se continuarmos assim, selecionaremos as arestas 8-6, 6-7 e 4-3. Nosso subgráfico ficará assim:

Notas sobre algoritmos do GoalKicker.com para profissionais 107


Machine Translated by Google

Este é o nosso subgrafo desejado, que nos dará a árvore geradora mínima. Se removermos as bordas que não removemos

selecione, obteremos:

Esta é a nossa árvore geradora mínima (MST). Portanto, 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 ligações são mostrados no gráfico. Pode haver vários MST de um
gráfico. Depende do nó de origem que escolhemos.

O pseudocódigo do algoritmo é fornecido abaixo:

Procedimento PrimsMST (Gráfico): // aqui o gráfico é um gráfico ponderado conectado não vazio
Vnovo[] = {x} // Novo subgrafo Vnew com nó fonte x

Notas sobre algoritmos do GoalKicker.com para profissionais 108


Machine Translated by Google

Enew[] = {}
enquanto Vnew não é 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
final enquanto
Devolver Vnew e Enew

Complexidade:

A complexidade de tempo da abordagem ingênua acima é O(V²). Ele usa matriz de adjacência. Podemos reduzir a complexidade usando fila
prioritária. Quando adicionamos um novo nó a Vnew, podemos adicionar suas arestas adjacentes na fila de prioridade. Em seguida, retire a borda
com peso mínimo 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 key[u] :=
inf parent[u] :=
NULL end for

key[source] := 0 Q =
Priority_Queue()
Q=V
enquanto Q não está vazio
u -> Q.pop
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) end if
end for
end while

Aqui key[] armazena o custo mínimo de passagem do nó-v. parent[] é usado para armazenar o nó pai. É útil para percorrer e imprimir a árvore.

Abaixo está um programa simples em Java:

importar java.util.*;

gráfico de classe pública


{
privado estático int infinito = 9999999; int[][] LinkCost;

interno Nnós;
Gráfico(int[][] mat) {

int eu, j;
NNodes = mat.comprimento;
LinkCost = novo int[NNodes][NNodes]; for ( i=0;
i < NNodes; i++) {

for ( j=0; j < NNodos; j++) {

Notas sobre algoritmos do GoalKicker.com para profissionais 109


Machine Translated by Google

LinkCost [ i ][ j ]= mat [ i ][ j ]; if
( LinkCost [ i ] [ j ] == 0 )
LinkCost [ i ][ j ]= infinito ;
}

} for ( i = 0 ; i < NNodes ; i++) {

for ( j = 0 ; j < NNodes ; j++) if


( LinkCost [ i ] [ j ] < infinito )
Sistema .out .print ( " " else + LinkCost [ i ] [ j ] + " " );

Sistema.out.print ("* " ) ;


Sistema.out.println ( ) ;
}

} public int unReached (boolean [ ] r ) {

booleano concluído =
verdadeiro ; for ( int i = 0 ; i < r.length ; i++) if ( r
[ i ] == false ) return i ;
retorno - 1 ;

} public void Prim ( ) {

int eu, j, k, x, y ; booleano


[] Atingido = novo booleano [NNodes ]; int [ ] predNode
= new int [NNodes ] ;
Atingido [ 0 ] =
verdadeiro ; for ( k = 1 ; k < NNodes ; k+
+){
Atingido [ k ] = falso ;

} predNode [ 0 ] = 0 ;
printReachSet ( Alcançado ); for (k
= 1 ; k < NNodes ; k++) {

x = y = 0 ; for
( i = 0 ; i < NNodes ; i++ ) for ( j = 0 ; {
j < NNodos ; j++)

if ( alcançado [ i ] && ! alcançado [ j ] &&


LinkCost [ i ] [ j ] < LinkCost [ x ] [ y ])
{
x = eu ;
y=j;
}
}
System .out .println ("Margem de custo mínimo: (" + + x
+ "," + + y + ")"
+ "custo = +
"
LinkCost [ x ] [ y ] ) ;
predNode [ y ] = x ;
Atingido [ y ] = true ;
printReachSet ( Alcançado );
Sistema.out.println ( ) ;

} int [ ] a = predNode ;
for ( i = 0 ; i < NNodes ; i++ )
" --> "
Sistema .out .println ( a [ i ] + + eu );

} void printReachSet (boolean [] alcançado )

Notas sobre algoritmos do GoalKicker.com para profissionais 110


Machine Translated by Google

{
Sistema .out.print ("ReachSet=" ) ; for (int
i = 0 ; i < Reached.length ; i++) if ( Reached [ i ])

Sistema.out.print ( i + " " ) ; //


System.out.println();
}
público estático void principal (String [ ] args )
{
int [ ] [ ] conexão = { { 0 { 3 , 3 , 0 , 2 , 0 , 0 , 0 , 0 , 4 }, // 0
, 0 , 0 , 0 , 0 , 0 , 0 , 4 , 0 }, // 1
{ 0 , 0 , 0 , 6 , 0 , 1 , 0 , 2 , 0 }, // 2
{ 2 , 0 , 6 , 0 , 1 , 0 , 0 , 0 , 0 }, // 3
{ 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 8 }, // 4
{ 0 , 0 , 1 , 0 , 0 , 0 , 8 , 0 , 0 }, // 5
{ 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 0 }, // 6
{ 0 , 4 , 2 , 0 , 0 , 0 , 0 , 0 , 0 }, // 7 0 } //
, 0, 0, 0, 8, 0, 0, 0, 8
{4};
Gráfico G = novo gráfico (conn );
G.Prim ();
}
}

Compile o código acima usando javac Graph.java

Saída:

$ gráfico java
* 3 * 2 **** 4
3******4*
***6*1*2*2*6*1****

***1****8
**1***8*******8***

*42******
4***8****
ReachSet = 0 Margem de custo mínimo : ( 0 , 3 ) custo = 2
AlcanceSet = 0 3
Margem de custo mínimo : ( 3 , 4 ) custo = 1
AlcanceSet = 0 3 4
0 ), custo = 3
Margem de custo mínimo (: 1
ReachSet = 0 1 3 4 8 )
Margem de custo mínimo : ,
( 0 custo =4
AlcanceSet = 0 1 3 4 8
, 7 ) custo = 4
Margem de custo mínimo : ( 1
AlcanceSet = 0 1 3 4 7 8
2 ,) custo = 2
Margem de custo mínimo (: 7
ReachSet = 0 1 2 3 4 7 8
( 2 , 5 )custo = 1
Margem de custo mínimo :
AlcanceSet = 0 1 2 3 4 5 7 8
6 ,) custo = 8
Margem de custo mínimo (: 5
AlcanceSet = 0 1 2 3 4 5 6 7 8
0 --> 0 0
--> 1
7 -> 2
0 -> 3 3
-> 4
2 -> 5
5 -> 6

Notas sobre algoritmos do GoalKicker.com para profissionais 111


Machine Translated by Google
1 -> 7
0 -> 8

Notas sobre algoritmos do GoalKicker.com para profissionais 112


Machine Translated by Google

Capítulo 20: Algoritmo BellmanFord


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 bordas. 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, funciona nos casos em que
o peso da aresta é negativo e também encontra ciclo de peso negativo no gráfico. O problema com o algoritmo
de Dijkstra é que, se houver um ciclo negativo, você continua repetindo o ciclo continuamente e reduzindo a distância
entre dois vértices.

A ideia deste algoritmo é percorrer todas as arestas deste gráfico, uma por uma, em alguma ordem aleatória. Pode ser qualquer ordem aleatória. Mas você deve

garantir que, se uv (onde uev são dois vértices em um gráfico) for uma de suas ordens, então deve haver uma aresta de u a v . Geralmente, ela é obtida

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 determinada aresta uv indo de u a v a fórmula de
relaxação é:

se distância[u] + custo[u][v] < d[v]


d[v] = d[u] + custo[u][v]

Ou seja, 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 no máximo as arestas (V -1) vezes onde V é o número de arestas do gráfico. Por que (V-1) você pergunta? Explicaremos em

outro exemplo. Também vamos acompanhar o vértice pai de qualquer vértice, ou seja, quando relaxamos uma aresta, definiremos:

pai[v] = você

Isso significa que encontramos outro caminho mais curto para chegar a v através de u. Precisaremos disso mais tarde para imprimir o caminho mais curto da
origem ao vértice de destino.

Vejamos um exemplo. Temos um gráfico:

Notas sobre algoritmos do GoalKicker.com para profissionais 113


Machine Translated by Google

Selecionamos 1 como vértice de origem . Queremos descobrir o caminho mais curto da fonte até todos os outros
vértices.

A princípio, d[1] = 0 porque é a fonte. E o resto é infinito, porque ainda não sabemos a distância deles.

Vamos relaxar 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 desejar. Se relaxarmos as bordas uma vez, o que obteremos? 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] + custo[4][5] = infinito + 7 = infinito. Não podemos atualizar este.


2. d[2] + custo[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 parent[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] + custo[2][3] = infinito. Não podemos atualizar este.

Não foi possível atualizar alguns vértices porque a condição d[u] + cost[u][v] < d[v] não correspondia. Como já dissemos
antes, encontramos 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. Nós temos:

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 sobre algoritmos do GoalKicker.com para profissionais 114


Machine Translated by Google

Nosso gráfico ficará assim:

Nossa terceira iteração atualizará apenas o vértice 5, onde d[5] será 8. Nosso gráfico será semelhante a:

Depois disso, não importa quantas iterações façamos, teremos as mesmas distâncias. Portanto manteremos uma flag que verifica se alguma atualização ocorre

ou não. Caso contrário, simplesmente quebraremos o ciclo. 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] :=
pai infinito[i] :=
NULL final para

d[fonte] := 0 para
i de Sinalizador 1 a
n-1 : = 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 sobre algoritmos do GoalKicker.com para profissionais 115


Machine Translated by Google
fim se
fim para if
flag == false break

fim para
Retorno d

Para acompanhar o ciclo negativo, podemos modificar nosso código usando o procedimento descrito aqui. Nosso pseudocódigo
completo será:

Procedimento Bellman-Ford-With-Negative-Cycle-Detection (Gráfico, fonte): n := número de vértices no


gráfico para i de 1 a n d[i] := pai infinito[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] + cost[u]
[v] < d[v]
d[v] := d[u] + custo[u][v] pai[v] := u
flag := true end if end
for if flag == false
break

fim para
todas as arestas de (u,v) no gráfico se d[u] + custo[u]
[v] < d[v]
Retorna o final do "Ciclo Negativo Detectado" se
terminar
para
Retorno 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] if v == NULL

retornar
PathPrinting(v) imprimir
-> você

Complexidade:

*
Como precisamos relaxar as arestas no máximo (V-1) vezes, a complexidade de tempo deste algoritmo será igual a O(VE) onde E denota
o número de arestas, se usarmos a lista de adjacências para representar o grafo. No entanto, se a matriz de adjacência for usada para
representar o gráfico, a complexidade do tempo será O(V^3). A razão é que podemos iterar por todas as arestas no 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 sobre algoritmos do GoalKicker.com para profissionais 116


Machine Translated by Google

aqui

Usando o algoritmo Bellman-Ford, podemos detectar se existe um ciclo negativo em nosso gráfico. Sabemos que, para descobrir o caminho
mais curto, precisamos relaxar todas as arestas do grafo (V-1) vezes, onde V é o número de vértices de um grafo.
Já vimos que neste exemplo, após (V-1) iterações, 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 a cada iteração,
percorrer 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
a (V-1). Se usássemos o algoritmo de Dijkstra aqui, ficarí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 fonte. Depois de aplicar o algoritmo do caminho mais curto de fonte única de Bellman-Ford ao gráfico,
descobriremos as distâncias da fonte a todos os outros vértices.

É assim que o gráfico fica após (V-1) = 3 iterações. Deve ser o resultado, já que existem 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 diminuindo, significa que há definitivamente um ciclo de peso negativo no
gráfico.

Notas sobre algoritmos do GoalKicker.com 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]. Portanto, podemos concluir que existe
um ciclo negativo em nosso gráfico.

Então, como descobrimos o ciclo negativo? Fazemos uma pequena modificação no procedimento Bellman-Ford:

Procedimento NegativeCycleDetector (Gráfico, fonte): n := número de


vértices no gráfico para i de 1 a n d[i] := fim infinito
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] + cost[u][v] flag := true end


if end for if flag
== false
break

fim para
todas as arestas de (u,v) no gráfico se d[u] + custo[u]
[v] < d[v]
Retorna o final do "Ciclo Negativo Detectado" se

fim para
Retornar "Sem Ciclo Negativo"

É assim que descobrimos se existe um ciclo negativo em um gráfico. Também podemos modificar o algoritmo Bellman-Ford para
acompanhar os 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 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. Este processo é
repetido no máximo (V-1) vezes, onde V é o número de vértices do grafo.

O número de iterações necessárias para descobrir o caminho mais curto da origem até todos os outros vértices depende da ordem
que selecionamos para relaxar as arestas.

Vejamos um exemplo:

Aqui, o vértice fonte é 1. Descobriremos a distância mais curta entre a fonte e todos os outros vértices.
Podemos ver claramente que, para chegar ao vértice 4, no pior caso, serão necessárias arestas (V-1) . 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 sobre algoritmos do GoalKicker.com para profissionais 118


Machine Translated by Google

algoritmo para descobrir o caminho mais curto aqui:

Usaremos esta sequência:

+--------+--------+--------+--------+
| Série | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borda | 3->4 | 2->3 | 1->2 |
+--------+--------+--------+--------+

Para nossa primeira iteração:

1. d[3] + custo[3][4] = infinito. Isso não vai mudar nada.


2. d[2] + custo[2][3] = infinito. Isso não vai mudar nada.
3. d[1] + custo[1][2] = 2 < d[2]. d[2] = 2. pai[2] = 1.

Podemos ver que nosso processo de relaxamento apenas mudou d[2]. Nosso gráfico ficará assim:

Segunda iteração:

1. d[3] + custo[3][4] = infinito. Isso não vai 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 para 4 a partir de 1. Nosso gráfico será semelhante a:

Portanto, foram necessárias 3 iterações para descobrir o caminho mais curto. Depois deste, não importa quantas vezes relaxemos as bordas,

Notas sobre algoritmos do GoalKicker.com 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 origem 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 serão necessárias mais de 3 iterações para descobrir o caminho mais curto da fonte neste
exemplo.

Podemos concluir que, na melhor das hipóteses, será necessária 1 iteração para descobrir o caminho mais curto da fonte. Para o pior
caso, serão necessárias (V-1) iterações, por isso repetimos o processo de relaxamento (V-1) vezes.

Notas sobre algoritmos do GoalKicker.com 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 finais 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 base: O algoritmo de desenho de linha de Bresenham é um algoritmo de geração de linha raster eficiente e preciso desenvolvido
por Bresenham. Envolve apenas cálculo de números inteiros, por isso é preciso e rápido. 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 x e y são aumentados usando o parâmetro de decisão.

Para Inclinação |m|>1:


Qualquer valor de y é aumentado
OU ambos x e y são aumentados usando o parâmetro de decisão.

Algoritmo para inclinação |m|<1:

1. Insira dois pontos finais (x1,y1) e (x2,y2) da linha.

2. Trace o primeiro ponto (x1,y1).

3. Calcule

Delx =| x2 – x1 |
Diariamente = | y2 – y1 |

4. Obtenha o parâmetro de decisão inicial como P = 2 *


dely – delx

5. Para I = 0 para delx na etapa 1

Se p < 0 então
X1 = x1 + 1

Pode(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 sobre algoritmos do GoalKicker.com 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>
#include<conio.h>
#include<graphics.h>
#include<math.h>

int main()

{ int gdriver=DETECTAR,gmode;
int x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\\BGI");

printf("Insira os pontos iniciais: "); scanf("%d",&x1);


scanf("%d",&y1);
printf("Digite os
pontos finais: "); scanf("%d",&x2);
scanf("%d",&y2);

colocarpixel(x1,y1,VERMELHO);

delx=fabs(x2-x1);
dely=fabs(y2-y1);
p=(2*dely)-delx;
for(i=0;i<delx;i++){ if(p<0)

{ x1=x1+1;
colocarpixel(x1,y1,VERMELHO);
p=p+(2*dely); }

senão
{ x1=x1+1;
y1=y1+1;
colocarpixel(x1,y1,VERMELHO);

p=p+(2*dely)-
(2*delx); } }
getch();
fechargráfico(); retornar 0; }

Algoritmo para inclinação |m|>1:

1. Insira dois pontos finais (x1,y1) e (x2,y2) da linha.


2. Trace o primeiro ponto (x1,y1).
3. Calcule

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 na etapa de 1

Se p < 0 então
y1 = y1 + 1
Pode(x1,y1)

Notas sobre algoritmos do GoalKicker.com para profissionais 122


Machine Translated by Google

P = p+ 2delx

Outro

X1 = x1 + 1

Y1 = y1 + 1
Gráfico (x1,y1)

P = p + 2 partes – 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("Insira
os pontos iniciais: "); scanf("%d",&x1);
scanf("%d",&y1);
printf("Digite os
pontos finais: "); scanf("%d",&x2);
scanf("%d",&y2);

colocarpixel(x1,y1,VERMELHO);

delx=fabs(x2-
x1);
dely=fabs(y2-y1); p=(2*delx)-dely; for(i=0;i<delx;i++){ if(p<0) { y1=y1+1; colocarpixel(x1,y1,VERMELHO); p=p+(2*delx); } senão { x1=x1+

Notas sobre algoritmos do GoalKicker.com para profissionais 123


Machine Translated by Google

Capítulo 22: Algoritmo Floyd-Warshall


Seção 22.1: Algoritmo de caminho mais curto para todos os pares

Floyd-Warshall O algoritmo serve para encontrar os caminhos mais curtos em um gráfico 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 detectar ciclos negativos em um gráfico. Floyd-
Warshall é um algoritmo de programação dinâmica.

Vejamos um exemplo. Vamos aplicar o algoritmo de Floyd-Warshall neste gráfico:

A primeira coisa que fazemos é pegar duas matrizes 2D. Estas são matrizes de adjacência. O tamanho das matrizes será
o número total de vértices. Para nosso gráfico, usaremos matrizes 4 * 4 . A Matriz de Distância irá 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, vamos colocar infinito.
A Path Matrix serve 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, colocaremos N lá
indicando que não há caminho disponível agora. As duas tabelas do nosso gráfico serão semelhantes a:

+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| |1|2|3|4| | |1| 2 |3|4|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 1 | 0 | 3 | 6 | 15 | |1|N|1|1|1|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 2 | informações | 0 | -2 | informações | | 2| N| N| 2| N|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 3 | informações | informações | 0 | 2 | | 3 | N| N| N| 3 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 4 | 1 | informações | informações | 0 | | 4 | 4 | N| N| N|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
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 intermediário 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 caminho[i][j] será definido como caminho[k][j], pois é melhor ir de i a k,

Notas sobre algoritmos do GoalKicker.com para profissionais 124


Machine Translated by Google

e então k a j. Todos os vértices serão selecionados como k. Teremos 3 loops aninhados: for 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 é se, para cada par de vértices, obtemos uma distância menor passando por outro
vértice? O número total de operações do nosso gráfico será 4 * 4 * 4 = 64. Isso significa que faremos esta verificação
64 vezes. Vejamos alguns deles:

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
isso 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
preencher ambas as matrizes. O cálculo para cada etapa é mostrado aqui. Depois de fazer as alterações necessárias, nossas matrizes
vai parecer:

+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| |1|2|3|4| | |1| 2 |3|4|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|1|0|3|1|3| |1|N|1|2|3|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 2 | 1 | 0 | -2 | 0 | |2|4|N|2|3|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|3|3|6|0|2| |3|4|1|N|3|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
|4|1|4|2|0| |4|4|1|2|N|
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
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 do vértice
para eu 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 até v, começaremos com path[u][v]. Bem definido
continue mudando v = path[u][v] até encontrarmos path[u][v] = u e colocar todos os valores de path[u][v] em uma pilha. Depois
encontrando você, imprimiremos você e começaremos a retirar itens da pilha e imprimi-los. Isso funciona porque a matriz do 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 sobre algoritmos do GoalKicker.com para profissionais 125


Machine Translated by Google

s = Pilha()
S.push(destino)
enquanto Caminho[fonte][destino] não é igual à origem
S.push(Caminho[fonte][destino]) destino :=
Caminho[fonte][destino] fim enquanto

print -> source


while S não está vazio
print -> S.pop end
while

Encontrando o Ciclo de Borda Negativa:

Para descobrir se existe um ciclo de aresta negativo, precisaremos verificar a diagonal principal da matriz de distância . Se algum valor na diagonal
for negativo, 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 sobre algoritmos do GoalKicker.com para profissionais 126


Machine Translated by Google

Capítulo 23: Algoritmo Numérico 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, muitas vezes envolvendo objetos definidos recursivamente. Os números catalães 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-gon regular pode ser dividido em n-2
triângulos se diferentes orientações forem contadas separadamente?'

Aplicação do algoritmo numérico catalão:

1. O número de maneiras de empilhar moedas em uma linha 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
correspondendo 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 se cruzando, é o ené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 sobre algoritmos do GoalKicker.com para profissionais 127


Machine Translated by Google

Complexidade de tempo: O (n ^ 2)

Notas sobre algoritmos do GoalKicker.com para profissionais 128


Machine Translated by Google

Capítulo 24: Algoritmos Multithread


Exemplos para alguns algoritmos multithread.

Seção 24.1: Multithread de multiplicação de matrizes quadradas

multiplicar-matriz-quadrada-paralela (A, B) n =
A.linhas
C = Matrix(n,n) //cria uma nova matriz n*n paralela para
i=1an
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: Multithread de vetor de matriz de multiplicação

vetor-matriz(A,x) n =
A.linhas y =
Vetor(n) //cria um novo vetor de comprimento n paralelo para i
= 1 a n y[i] = 0 paralelo para i
=1an
para j = 1 a n y[i] = y[i] + A[i]
[j]*x[j] retornar y

Seção 24.3: multithread de classificação por mesclagem

A é uma matriz e os índices p e q da matriz, como você 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 os 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 as duas submatrizes a serem mescladas estão na mesma matriz, mas não assume que sejam adjacentes na matriz. É 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
// verifica se n1>=n2

Notas sobre algoritmos do GoalKicker.com para profissionais 129


Machine Translated by Google

permutar p1 e p2
permutar r1 e r2
permutar n1 e n2 se
n1==0 //ambos vazios?
retorne

senão q1 = piso((p1+r1)/2) q2 =
pesquisa 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)
sincronização

E aqui está a função auxiliar de pesquisa dicotômica.

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 half =
floor((inf+sup)/2) if x<=T[half] sup
= metade senão
inf = metade
+1

retornar _

Notas sobre algoritmos do GoalKicker.com 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 procura ocorrências de uma "palavra" W dentro de uma "string 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. aproveite essas informações para evitar combinar os caracteres que sabemos que corresponderão de qualquer maneira. A
complexidade do pior caso para pesquisar um padrão se reduz a O (n).

Seção 25.1: Exemplo KMP


Algoritmo

Este algoritmo é um processo de duas etapas. Primeiro criamos um array auxiliar lps[] e então usamos esse array para pesquisar o padrão.

Pré-processando :

1. Pré-processamos o padrão e criamos um array auxiliar lps[] que é usado para pular caracteres enquanto
Coincidindo.
2. Aqui lps[] indica o prefixo adequado mais longo, que também é sufixo. Um prefixo adequado é o prefixo no qual a string inteira não está incluída.
“ “
Por exemplo, os prefixos da string ABC são “AB”. Os sufixos da ”, “A”, “AB” e “ABC”. Os prefixos adequados são ”, “A” e

string 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
Coincidindo.

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 adequados. Disto podemos concluir que não precisamos combinar
esses caracteres lps[j-1] com txt[ ij…i-1] porque sabemos que esses caracteres irão corresponder de qualquer maneira.

Implementação em Java

classe pública KMP {

public static void main(String[] args) { // TODO


Método gerado automaticamente stub
String str = "abcabdabc";
Padrão de string = "abc";
KMP obj = novo KMP();
System.out.println(obj.patternExistKMP(str.toCharArray(), pattern.toCharArray()));
}

public int[] computaLPS(char[] str){ int lps[]


= new int[str.length];

lps[0] = 0; int
j = 0; for(int
i =1;i<str.comprimento;i++){ if(str[j] ==
str[i]){ lps[i] = j+1; j++;

Notas sobre algoritmos do GoalKicker.com para profissionais


131
Machine Translated by Google

eu+
+; }
else{ if(j!=0)
{ j = lps[j-1]; }

else{ lps[i] = j+1; eu+


+;
}
}

retornar lps;
}

padrão booleano públicoExistKMP (char[] texto,char[] pat){


int[] lps = computaLPS(pat); int
i=0,j=0;
while(i<texto.comprimento && j<pat.comprimento)
{ if(texto[i] == pat[j]){ i++; j+
+; }

else{ if(j!=0){ j
= lps[j-1]; }
else{ i+
+;
}
}
}

if(j==pat.length)
retorna
verdadeiro; retorna falso;
}

Notas sobre algoritmos do GoalKicker.com 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 em
corda 2
A declaração do problema é como se recebêssemos duas strings str1 e str2, então quantos números mínimos de
as operações podem ser executadas no str1 que é convertido em str2. As operações podem ser:

1. Inserir
2. Remover

3. Substitua

Por exemplo

Entrada: str1 = "geek", str2 = "gesek"


Saída: 1
Precisamos apenas inserir s na primeira string

Entrada: str1 = "março", 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 um array 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 nosso array 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 | | | | | | |
+---+---+---+---+---+---+---+
(de)| 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 pode
fazemos para converter a em ab. Será 1 porque temos que inserir b. Então, após a primeira iteração, nosso array ficará assim

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(de)| 2 | | | | | | |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |

Notas sobre algoritmos do GoalKicker.com para profissionais 133


Machine Translated by Google
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Para iteração 2

Para dp[2][1] temos que verificar se para converter az em a precisamos remover z, portanto dp[2][1] será 1.Semelhante para
dp[2][2] precisamos substituir z por b, portanto dp[2][2] será 1. Portanto, após a 2ª iteração, nosso array dp[] terá a aparência.

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
|0|1|2|3|4|5|6|
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(de)| 2 | 1 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Então nossa fórmula ficará assim

se os caracteres forem iguais


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, 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 |
+---+---+---+---+---+---+---+
(de)| 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

public int getMinConversions(String str1, String str2){


int dp[][] = new int[str1.length()+1][str2.length()+1];
for(int i=0;i<=str1.length();i++){
for(int j=0;j<=str2.length();j++){
se(eu==0)

Notas sobre algoritmos do GoalKicker.com para profissionais 134


Machine Translated by Google

dp[i][j] = j; senão
if(j==0) dp[i][j] =
i; senão
if(str1.charAt(i-1) == str2.charAt(j-1)) dp[i][j] = dp[i-1][j-1];
senão { dp[i][j] = 1 +

Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1 ]));


}
}

} retornar dp[str1.length()][str2.length()];
}

Complexidade de tempo

O(n^2)

Notas sobre algoritmos do GoalKicker.com 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 ÿÿÿÿ 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 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 . É par

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 a primeira) o FWF apresenta uma falta de cache e limpa o cache. isso significa que temos k
páginas vazias. Em cada fase são solicitadas no máximo k páginas diferentes, portanto haverá agora despejo durante a fase.
Portanto, 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 uma página marcada x na fase i é
despejada. Seja ÿt a solicitação na fase i onde x é despejado. Como x está marcado, deve haver uma solicitação anterior ÿt* para x na
mesma fase, então t* < t. Depois que 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 com a definição da fase. Portanto, LRU deve 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, todo algoritmo de marcação é ideal e
o algoritmo off-line ideal não pode ser melhor.
Assumimos 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 despejar mais de k páginas sem despejar uma página marcada.
Agora tentamos mostrar que o algoritmo offline ideal 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á a página x e no máximo k-1 páginas diferentes no cache de
algoritmos off-line ideais. Na subsequência, são k solicitações de página diferentes de x, portanto, o algoritmo off-line ideal deve 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
despejos 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 sobre algoritmos do GoalKicker.com para profissionais 136


Machine Translated by Google

Se não houver uma constante r para a qual um algoritmo online A seja 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 de 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 existem (l-1) solicitações alternadas para as páginas 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 de subsequência (k,k+1)l-1 despeja uma página. Além disso, há falhas de cache k-1 na primeira utilização
das páginas 1-(k-1). Portanto, LFU e LIFO despejam páginas exatas k-1+2(l-1).
Agora devemos mostrar que para toda constante ÿÿÿ e toda constante r ÿ 1 existe um l tal que

que é igual a

Para satisfazer esta desigualdade basta escolher l suficientemente grande. Portanto, 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. Algoritmos Script Online (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 do 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 estes tópicos através de uma série de exemplos, introduzindo
definições ao longo do caminho. A seção de comentários Teoria consistirá em todas as definições, teoremas e proposições para fornecer
todas as informações para uma consulta mais rápida de aspectos específicos.

Notas sobre algoritmos do GoalKicker.com para profissionais 137


Machine Translated by Google

As fontes da seção de comentários consistem no material de base usado para este tópico e informações adicionais para leitura adicional.
Além disso, você encontrará os códigos-fonte completos dos exemplos. Por favor, preste atenção para tornar o código-fonte dos exemplos
mais legível e mais curto, ele evita coisas como tratamento de erros, etc. Ele também transmite alguns recursos específicos da
linguagem que obscureceriam a clareza do exemplo, como o 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áginas que devem ter sido colocadas no cache antes de serem processadas. É claro que
se m<=k então apenas colocamos todos os elementos no cache e ele 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 colocar a página solicitada no cache e despejar outra, presumindo que o cache esteja cheio. A Meta é um
cronograma de despejos 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 recente é removida 3. Menos usada recentemente (LRU): Página removida cujo acesso
mais recente foi o mais antigo 4 • Menos freqüentemente usada (LFU): Retira a página que
foi solicitada com menos frequência 5. Maior distância de avanço (LFD): Retira a página do cache que não é solicitada até o futuro mais dist
6. Liberar quando cheio (FWF): limpe o cache completamente assim que ocorrer uma falha no cache

Existem duas maneiras de abordar esse problema:

1. offline: a sequência de solicitações de página é conhecida antecipadamente


2. online: a sequência de solicitações de página não é conhecida antecipadamente

Abordagem off-line

Para uma primeira abordagem veja o tópico Aplicações da técnica Greedy. Seu terceiro exemplo de cache offline considera as
cinco primeiras estratégias acima e fornece um bom ponto de entrada para as seguintes.

O programa de exemplo foi ampliado com a estratégia FWF :

classe FWF : estratégia pública


{ público:
FWF() : Estratégia("FWF") { }

int apply(int requestIndex) substituir {

for(int i=0; i<cacheSize; ++i) {

if(cache[i] == request[requestIndex]) return i;

// depois da primeira página vazia todas as outras devem estar


vazias else if(cache[i] == emptyPage)
return i;
}

// sem páginas gratuitas

Notas sobre algoritmos do GoalKicker.com para profissionais 138


Machine Translated by Google
retornar 0;
}

void update(int cachePos, int requestIndex, bool cacheMiss) substituição


{

// nenhuma página livre -> miss -> limpar cache


if(cacheMiss && cachePos == 0)
{
for(int i = 1; i < cacheSize; ++i)
cache[i] = página vazia;
}
}
};

O código-fonte completo está disponível aqui. Se reutilizarmos o exemplo do tópico, obteremos a seguinte saída:

Estratégia: FWF

Inicial do cache: (a,b,c)

Solicitar cache 0 cache 1 cache 2 falta de cache


a a b c
a a b c
d X X x
e dd e X
b d e b
b d e b
a a X X x
c a c X
a c f
fd X X x
e dd e X
a e a
X- X x
Facebook b X
e dff b e

c c X X x

Perdas totais de cache: 5

Embora o LFD seja ideal, o FWF apresenta menos perdas de cache. Mas o principal objetivo era minimizar o número de
despejos e para a FWF cinco erros significam 15 despejos, o que torna esta a pior escolha para este exemplo.

Abordagem on-line

Agora queremos abordar o problema online da paginação. Mas primeiro precisamos entender como fazer isso.
Obviamente, um algoritmo online não pode ser melhor que o algoritmo offline ideal. Mas quão pior é? Nós
precisamos 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 sobre algoritmos do GoalKicker.com para profissionais 139


Machine Translated by Google

wA(ÿ) = fÿ(A(ÿ)) ÿ r ÿ OPT(ÿ) + ÿ

para todas as instâncias ÿÿÿÿ. A é chamado de algoritmo online r-competitivo . É par

wA(ÿ) ÿ r ÿ OPT(ÿ)

para todas as instâncias ÿÿÿÿ então A é chamado de algoritmo online estritamente r-competitivo .

Portanto, a questão é quão competitivo é o nosso algoritmo online em comparação com um algoritmo offline ideal. Em seu famoso
livro Allan Borodin e Ran El-Yaniv usaram outro cenário para descrever a situação das paging online:

Existe 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 ao mesmo tempo melhor para o algoritmo offline. o fator competitivo do seu algoritmo é o fator que determina o
desempenho do seu algoritmo em relação ao algoritmo off-line ideal do adversário. Se você quiser tentar ser o adversário, pode
experimentar o Jogo do Adversário (tente vencer as estratégias de paginação).

Algoritmos de Marcação

Em vez de analisar cada algoritmo separadamente, vamos examinar uma família especial de algoritmos online para o problema de
paginação chamada 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é o máximo de k páginas diferentes serem solicitadas
A fase i ÿ 2 é a subsequência máxima de ÿ desde o final da fase i-1 até o máximo k páginas diferentes serem 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. É uma página solicitada durante uma fase em que é marcada. Um algoritmo é um algoritmo de
marcação se nunca remover uma página marcada do cache. Isso significa que as páginas 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 a primeira) o FWF apresenta uma falta de cache e limpa o cache. isso significa que temos k páginas
vazias. Em cada fase são solicitadas no máximo k páginas diferentes, portanto haverá agora despejo durante a fase. Portanto, 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 uma página marcada x na fase i é
despejada. Seja ÿt a solicitação na fase i onde x é despejado. Como x está marcado, deve haver uma solicitação anterior ÿt* para x na
mesma fase, então t* < t. Depois que 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
com a definição da fase. Portanto, LRU deve ser um algoritmo de marcação.

Notas sobre algoritmos do GoalKicker.com 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, todo algoritmo de marcação é ideal e o
algoritmo off-line ideal não pode ser melhor.
Assumimos l ÿ 2. o custo de cada algoritmo de marcação, por exemplo, ÿ é limitado de cima por l ÿ k porque em cada fase um algoritmo de
marcação não pode despejar mais de k páginas sem despejar uma página marcada.
Agora tentamos mostrar que o algoritmo offline ideal 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á a página x e no máximo k-1 páginas diferentes no cache de algoritmos
off-line ideais. Na subsequência, são k solicitações de página diferentes de x, portanto, o algoritmo off-line ideal deve 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 despejos 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.

Se não existe uma constante r para a qual um algoritmo online A seja 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 de 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, existem (l-1) solicitações alternadas para as páginas 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 de subsequência (k,k+1)l-1 despeja uma página. Além disso, há perdas de cache k-1 na primeira utilização das páginas 1-
(k-1). Portanto, LFU e LIFO despejam páginas exatas k-1+2(l-1).
Agora devemos mostrar que para toda constante ÿÿÿ e toda constante r ÿ 1 existe um l tal que

que é igual a

Para satisfazer esta desigualdade basta escolher l suficientemente grande. Portanto, 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 sobre algoritmos do GoalKicker.com para profissionais 141


Machine Translated by Google

A prova para esta última proposição é bastante longa e baseada na afirmação de que o LFD é um algoritmo offline ótimo.
O leitor interessado pode consultar o livro de Borodin e El-Yaniv (ver 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 o seu algoritmo se ele for aleatório.

A paginação aleatória será discutida em um dos próximos exemplos...

Notas sobre algoritmos do GoalKicker.com para profissionais 142


Machine Translated by Google

Capítulo 28: Classificação


Parâmetro Descrição Um

algoritmo de ordenação é estável se preserva a ordem relativa de elementos iguais após a ordenação.
Estabilidade

Um algoritmo de classificação está em vigor se classificar usando apenas memória auxiliar O(1) (sem contar a matriz que precisa ser
No lugar
classificada).

Um algoritmo de classificação tem uma complexidade de tempo de melhor caso de O(T(n)) se seu tempo de execução for pelo
Complexidade do melhor 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 em média 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
Complexidade do pior caso
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 iguais 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 e na matriz não classificada

de entrada.

Considere uma lista de pares:

(1, 2) (9, 7) (3, 4) (8, 6) (9, 3)

Agora ordenaremos 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) também aparece depois de (9, 7) na lista original.

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

Classificação de inserção

raiz da sorte

Tim Classificar

Tipo de bolha

Tipos instáveis bem conhecidos:

Classificação de pilha

Ordenação rápida

Notas sobre algoritmos do GoalKicker.com para profissionais 143


Machine Translated by Google

Capítulo 29: Classificação por Bolhas

Parâmetro Descrição
Estábulo Sim

No lugar Sim

Complexidade do melhor caso Sobre)

Complexidade média do caso O (n ^ 2)

Complexidade de pior caso O (n ^ 2)

Complexidade espacial O(1)

Seção 29.1: Classificação por bolha

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 bolha 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 -> troca
{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 na lista, temos {5,3,1,6,7,2,4,8}. Observe 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á ordenada, 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 de BubbleSort em C++:

void bubbleSort(vetor<int>números) {

for(int i = números.size() - 1; i >= 0; i--) { for(int j = 1; j <=


i; j++) {
if(números[j-1] > números[j]) {
trocar(números[j-1],números(j));

Notas sobre algoritmos do GoalKicker.com para profissionais 144


Machine Translated by Google

}
}
}
}

Implementação C

void bubble_sort( lista longa[], n longo ) {

longo c, d, t;

para (c = 0 ; c < ( n - 1 ); c++) {

para (d = 0 ; d < n - c - 1; d++) {

if (lista[d] > lista[d+1]) {

/ * Trocando */

t = lista[d];
lista[d] = lista[d+1]; lista[d+1]
= t;
}
}
}
}

Classificação por bolha 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++) {

if ( * (lista + d ) > *(lista+d+1)) {

/ * Trocando */

t = * (lista + d ); * (lista
+ d ) = * (lista + d + 1 ); * (lista + d + 1) = t;

}
}
}
}

Seção 29.3: Implementação em C#


A classificação por bolha também é conhecida como classificação por afundamento. É 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 bolha

Notas sobre algoritmos do GoalKicker.com para profissionais 145


Machine Translated by Google

Implementação de classificação por bolha

Usei a linguagem C# para implementar o algoritmo de classificação por bolha

classe pública BubbleSort {

public static void SortBubble(int[] entrada) {

for (var i = entrada.Comprimento - 1; i >= 0; i--) {

for (var j = entrada.Comprimento - 1 - 1; j >= 0; j--) {

if (entrada[j] <= entrada[j + 1]) continuar; var temp


= entrada[j + 1]; entrada[j + 1] =
entrada[j]; entrada[j] =
temperatura;
}
}
}

public static int[] Principal(int[] entrada) {

SortBubble(entrada);
entrada de retorno ;
}
}

Seção 29.4: Implementação Python


#!/ usr/ bin/ python

lista_entrada = [10,1,2,11]

para i no intervalo(len(lista_de_entrada)):
para j no intervalo(i): if
int(lista_de_entrada[j]) > int(lista_de_entrada[j+1]):
lista_de_entrada[j],lista_de_entrada[j+1] = lista_entrada[j+1],lista_entrada[j]

imprimir lista_de_entradas

Notas sobre algoritmos do GoalKicker.com para profissionais 146


Machine Translated by Google

Seção 29.5: Implementação em Java


classe pública MyBubbleSort {

public static void bubble_srt(int array[]) {// lógica principal int n =


array.length; intk ; para
(int m
= n; m >= 0; m--) {
for (int i = 0; i < n - 1; i++) { k = i + 1; if
(matriz[i] >
matriz[k]) {
swapNumbers(i, k, matriz);
}

} printNumbers(matriz);
}
}

private static void swapNumbers(int i, int j, int[] matriz) {

temperatura
interna ; temp =
matriz[i]; matriz[i] =
matriz[j]; matriz[j] = temp;
}

private static void printNumbers(int[] entrada) {

for (int i = 0; i < input.length; i++)


{ System.out.print(input[i] + ", ");
}
System.out.println("\n");
}

public static void main(String[] args) { int[] entrada


= { 4, 2, 9, 6, 23, 12, 34, 0, 1 }; bolha_srt(entrada);

}
}

Seção 29.6: Implementação em Javascript


função bolhaSort(a)
{
var trocado;
faça
{ trocado = falso;
for (var i=0; i < a.length-1; i++) { if (a[i] >
a[i+1]) { var temp = a[i];
uma[i] = uma[i+1];
uma[i+1] =
temperatura;
trocado = verdadeiro;
}

} } while (trocado);
}

var a = [3, 203, 34, 746, 200, 984, 198, 764, 9];

Notas sobre algoritmos do GoalKicker.com para profissionais 147


Machine Translated by Google

bolhaSort(a);
console.log(a); //registros [3, 9, 34, 198, 200, 203, 746, 764, 984]

Notas sobre algoritmos do GoalKicker.com para profissionais 148


Machine Translated by Google

Capítulo 30: Classificação por mesclagem

Seção 30.1: Noções básicas de classificação por mesclagem

Merge Sort é um algoritmo de dividir e conquistar. Ele divide a lista de entrada de comprimento n ao meio sucessivamente até que
haja n listas de tamanho 1. Em seguida, pares de listas são mesclados com o primeiro elemento menor entre o par de listas sendo
adicionado em cada etapa. Através da fusão sucessiva e 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. Cai no caso II do Método Mestre
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 a matriz em duas metades e leva um tempo linear para mesclar as duas metades.

Espaço Auxiliar: O(n)

Paradigma Algorítmico: Dividir e Conquistar

Notas sobre algoritmos do GoalKicker.com 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 { if len(a) <


2{
devolver um

} m := (len(a)) / 2

f := mesclarSort(a[:m]) s :=
mesclarSort(a[m:])

retornar mesclagem (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++

} else if j > lenS-1 && i <= lenF-1 { a[z] = f[i]

eu++

} else if f[i] < s[j] { a[z] = f[i] i+


+ } else { a[z]
=
s[j] j++

}
}

devolver um
}

func main() { a :=
[]int{75, 12, 34, 45, 0, 123 , 32, 56, 32, 99, 123, 11, 86, 33} fmt.Println(a)

fmt.Println( mesclarClassificar(a))
}

Seção 30.3: Implementação de classificação de mesclagem em C e C#

Classificação de mesclagem C

Notas sobre algoritmos do GoalKicker.com 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 armazenar


os dois arrays a serem mesclados int
n1,n2,i,j,k; n1=m-l+1;
n2=hm;

for(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 final de cada array temporário arr2[j]=9999;

eu=0; j=0; for(k=l; k<=h; k++) { //processo de combinação de dois arrays ordenados
if(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 meio;
if(baixo<alto)
{ médio=(baixo+alto)/2;
// Dividir e Conquistar
merge_sort(arr,low,mid);
merge_sort(arr,médio+1,alto);
// Combina
mesclagem(arr,low,mid,high);
}

retornar 0;
}

Classificação de mesclagem C#

classe pública MergeSort {

static void Merge(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];


}

Notas sobre algoritmos do GoalKicker.com para profissionais 151


Machine Translated by Google

para (j = 0; j < n2; j++) {

direita[j] = entrada[m + j + 1];


}

eu = 0;
j = 0;
var k = eu;

enquanto (i < n1 && j < n2) {

if (esquerda[i] <= direita[j]) {

entrada[k] = esquerda[i];
eu++;

}
outro {
entrada[k] = direita[j]; j++;

} k++;
}

enquanto (eu <


n1) {
entrada[k] = esquerda[i];
eu+
+; k++;
}

enquanto (j < n2) {

entrada[k] = direita[j]; j++;


k++;

}
}

static void SortMerge(int[] entrada, int l, int r) {

se (eu < r) {

int m = l + (r - l) / 2;
SortMerge(entrada, l, m);
SortMerge(entrada, m + 1, r);
Mesclar(entrada, l, m, r);
}
}

public static int[] Principal(int[] entrada) {

SortMerge(entrada, 0, entrada.Comprimento - 1);


entrada de retorno ;
}
}

Seção 30.4: Implementação de classificação de mesclagem em Java

Abaixo está a implementação em Java usando uma abordagem genérica. É o mesmo algoritmo apresentado acima.

Notas sobre algoritmos do GoalKicker.com 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 Comparable < T >> implementa InPlaceSort <T> {

@Override
public void sort(T[] elementos) { T[] arr =
(T[]) new Comparable[elements.length]; classificar(elementos, arr,
0, elementos.comprimento - 1);
}

// Verificamos ambos os lados e depois os mesclamos private


void sort(T[] elements, T[] arr, int low, int high) { if (low >= high) return; int médio =
baixo + (alto - baixo) / 2;
classificar(elementos, arr, baixo, médio);
classificar(elementos, arr, médio + 1,
alto); mesclar(elementos, arr, baixo, alto, médio);

private void merge(T[] a, T[] b, int low, int high, int mid) { int i = low; int j = meio + 1;

// Selecionamos o menor elemento dos dois. E então colocamos em b for (int k = low; k <= high;
k++) {

if (i <= médio && j <= alto) { if


(a[i].compareTo(a[j]) >= 0) { b[k] = a[j++]; }
senão { b[k] = a[i+
+];

}
} else if (j > alto && i <= médio) { b[k] = a[i++]; }
else if (i > médio
&& j <= alto) { b[k] = a[j++];

}
}

for (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):
"
mescla duas listas ordenadas "
p1 = p2 = 0 out
= []
enquanto p1 < len(X) e p2 < len(Y): if X[p1] <
Y[p2]: out. anexar(X[p1])
p1 += 1 senão:

out.append(Y[p2]) p2 += 1
out +=
X[p1:] + Y[p2:]

Notas sobre algoritmos do GoalKicker.com para profissionais 153


Machine Translated by Google
retornar _

def mergeSort(A): if len(A)


<= 1: return A if len(A)
== 2: return
sorted(A)

mid = len(A) / 2 return


merge(mergeSort(A[:mid]), mergeSort(A[mid:]))

se __nome__ == "__main__":
# 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 de baixo para cima


classe pública MergeSortBU {
array inteiro estático privado [] = { 4, 3, 1, 8, 9, 15, 20, 2, 5, 6, 30, 70,
60,80,0,9,67,54,51,52,24,54,7 };

public MergeSortBU() { }

private static void merge(Comparable[] arrayToSort, Comparable[] aux, int lo,int mid, int hi) {

for (int índice = 0; índice < arrayToSort.length; índice++) { aux[index] = arrayToSort[index];

int i = lo; int j =


meio + 1; for (int k = lo;
k <= oi; k++) { if (i > mid)

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++];

}
}

public static void sort(Comparable[] arrayToSort, Comparable[] aux, int lo, int hi) {
int N = arrayToSort.comprimento; for (int
sz = 1; sz < N; sz = sz + sz) { for (int low = 0; low < N; low =
low + sz + sz) { System.out.println("Tamanho:"+ sz ); mesclar(arrayToSort,
aux, low, low + sz -1 ,Math.min(low + sz + sz - 1,
N - 1)); imprimir(arrayToSort);

}
}

public static boolean isLess(Comparável a, Comparable b) {


retornar a.compareTo(b) <= 0;

Notas sobre algoritmos do GoalKicker.com para profissionais 154


Machine Translated by Google

private static void print (matriz comparável[] ) {http://


stackoverflow.com/ documentation/ algorithm/ 5732/ merge-sort# StringBuffer buffer
= new StringBuffer();http://
stackoverflow.com/ documentation/ algorithm/ 5732 / merge-sort# for ( valor comparável : array)
{ buffer.append(value); buffer.append(' ');

}
System.out.println(buffer);
}

public static void main(String[] args) { Comparable[]


aux = new Comparable[array.length]; imprimir(matriz);

MergeSortBU.sort(array, aux, 0, array.length - 1);


}
}

Notas sobre algoritmos do GoalKicker.com para profissionais 155


Machine Translated by Google

Capítulo 31: Classificaçã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:insira n xs

Notas sobre algoritmos do GoalKicker.com para profissionais 156


Machine Translated by Google

Capítulo 32: Classificação de Bucket

Seção 32.1: Implementação C#


classe pública BucketSort {

public static void SortBucket(ref int[] entrada) {

int valormin = entrada[0]; int


maxValue = entrada[0]; int k =
0;

for (int i = entrada.Comprimento - 1; i >= 1; i--) {

if (input[i] > maxValue) maxValue = input[i]; if (input[i] <


minValue) minValue = input[i];
}

Lista<int>[] bucket = new Lista<int>[maxValue - minValue + 1];

for (int i = balde.Comprimento - 1; i >= 0; i--) {

balde[i] = new Lista<int>();


}

foreach (int i na entrada) {

bucket[i - minValue].Add(i);
}

foreach (List<int> b no intervalo) {

if (b.Contagem > 0)
{
foreach (int t em b) {

entrada[k] = t; k+
+;
}
}
}
}

public static int[] Principal(int[] entrada) {

SortBucket( entrada de referência);


entrada de retorno ;
}
}

Notas sobre algoritmos do GoalKicker.com para profissionais 157


Machine Translated by Google

Capítulo 33: Classificação rápida

Seção 33.1: Noções básicas do Quicksort

Ordenação rápida é um algoritmo de classificação que escolhe um elemento ("o pivô") e reordena a matriz 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 ordenada.

1. Mecanismo do esquema de partição Lomuto:

Este esquema escolhe um pivô que normalmente é o último elemento da matriz. O algoritmo mantém o índice para colocar o pivô na variável i e cada vez que encontra

um elemento menor ou igual ao pivô, esse índice é incrementado e esse elemento seria colocado antes do pivô.

partição (A, baixa, alta) é pivô :=


A[alta] i := baixa

para j := baixo para alto – 1 faça


se A[j] ÿ pivô então troque
A[i] por A[j]
eu := eu + 1
troque A[i] por A[high]
retorne i

Mecanismo de classificação rápida:

quicksort(A, low, high) é se low <


high then p :=
partição(A, low, high) quicksort(A,
low, p – 1) quicksort(A, p + 1,
high)

Exemplo de classificação rápida:

Notas sobre algoritmos do GoalKicker.com para profissionais 158


Machine Translated by Google

2. Esquema de partição Hoare:

Ele usa dois índices que começam nas extremidades do array que está sendo particionado, depois se movem em direção um ao outro, até
detectarem uma inversão: um par de elementos, um maior ou igual ao pivô, um menor ou igual, que estão no lugar errado. ordem 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 faz três vezes menos trocas em média e cria partições eficientes mesmo quando
todos os valores são iguais.

quicksort(A, lo, hi) é se lo < hi


então p :=
partição(A, lo, hi) quicksort(A,
lo, p) quicksort(A, p + 1,
hi)

Partição:

partição (A, lo, hi) é pivô :=


A[lo] i := lo - 1 j :=
hi + 1 loop
para sempre
faça:

eu := eu + 1

Notas sobre algoritmos do GoalKicker.com para profissionais 159


Machine Translated by Google

enquanto A[i] < pivô do

fazer:

j := j - 1
enquanto A[j] > pivô do

se eu >= j então
retorne j

troque A[i] por A[j]

Seção 33.2: Quicksort em Python


def quicksort(arr): se
len(arr) <= 1:
retorno _
pivô = arr[len(arr) / 2] esquerda
= [x para x em arr se x < pivô] meio = [x para
x em arr se x == pivô] direita = [x para x em arr se
x > pivô ] retornar quicksort (esquerda) + meio
+ quicksort (direita)

imprimir classificação rápida ([3,6,8,10,1,2,1])

Imprime "[1, 1, 2, 3, 6, 8, 10]"

Seção 33.3: Implementação java da partição Lomuto


classe pública Solução {

public static void main(String[] args) { Scanner sc


= new Scanner(System.in); int n = sc.nextInt();
int[] ar = novo int[n]; for(int
i=0; i<n; i++) ar[i] =
sc.nextInt(); quickSort(ar, 0,
ar.comprimento-1);

public static void quickSort(int[] ar, int baixo, int alto) {

if(baixo<alto) {

int p = partição(ar, baixo, alto); quickSort(ar,


0 , p-1); quickSort(ar,p+1,
alto);
}

} partição estática pública int (int[] ar, int l, int r) {

int pivô = ar[r]; int eu


=eu; for(int
j=l; j<r; j++) {

if(ar[j] <= pivô)


{
int t = ar[j]; ar[j] =
ar[i]; ar[i] = t; eu+
+;

Notas sobre algoritmos do GoalKicker.com para profissionais 160


Machine Translated by Google

} int t = ar[i]; ar[i]


= ar[r]; ar[r] = t;

retornar eu;
}

Notas sobre algoritmos do GoalKicker.com para profissionais 161


Machine Translated by Google

Capítulo 34: Classificação por Contagem

Seção 34.1: Contagem de informações básicas de classificação


Classificação de contagem é um algoritmo de classificação inteira para uma coleção de objetos que classifica de acordo com as chaves
dos objetos.

Passos

1. Construa um array funcional C que tenha tamanho igual ao intervalo do array 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 um array onde C[x] se refere ao número de valores ÿ x iterando pelo array,
atribuindo a cada C[x] a soma de seu valor anterior e todos os valores em C que vêm antes dele.
4. Itere de trás para frente através de A, colocando cada valor em uma nova matriz classificada B no índice registrado em C. Isso é feito para um
determinado A[x] atribuindo B[C[A[x]]] a A[x] ], e decrementando C[A[x]] caso houvesse 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 Psuedocódigo


Restrições:

1. Entrada (uma matriz a ser classificada)


2. Número do elemento na entrada (n)
3. Chaves no intervalo de 0..k-1 (k)
4. Contagem (uma matriz de números)

Pseudo-código:

para x na entrada:
contagem[key(x)] +=
1 total = 0
para i no intervalo(k):
oldCount = count[i]
count[i] = total total
+= oldCount

Notas sobre algoritmos do GoalKicker.com para profissionais 162


Machine Translated by Google

para x na entrada:
saída[contagem[chave(x)]] = x
contagem[chave(x)] +=
1 saída de retorno

Notas sobre algoritmos do GoalKicker.com para profissionais 163


Machine Translated by Google

Capítulo 35: Classificação de Heap

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

classe pública HeapSort {

public static void Heapify(int[] entrada, int n, int i) {

int maior = i; int eu


= eu + 1; int r =
eu + 2;

if (l < n && entrada[l] > entrada[maior]) maior = l;

if (r < n && entrada[r] > entrada[maior]) maior = r;

if (maior ! = i) {

var temp = entrada[i];


entrada[i] = entrada[maior];
entrada[maior] = temp;
Heapify(entrada, n, maior);
}
}

public static void SortHeap(int[] entrada, int n) {

para (var i = n - 1; i >= 0; i--) {

Heapify(entrada, n, i);

} for (int j = n - 1; j >= 0; j--) {

var temp = entrada[0];


entrada[0] = entrada[j];
entrada[j] = temperatura;
Heapify(entrada, j, 0);
}
}

public static int[] Principal(int[] entrada) {

SortHeap(entrada, entrada.Comprimento);
entrada de retorno ;
}
}

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 na estrutura de dados heap binários. É 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, repita o
mesmo processo para os demais itens.

Pseudocódigo para classificação de heap:

função heapsort(entrada, contagem)

Notas sobre algoritmos do GoalKicker.com para profissionais 164


Machine Translated by Google

heapify(a,count) end
<- count - 1 while end
-> 0 do swap(a[end],a[0])
end<-end-1 restore(a,
0, end)

função heapify (a, contagem)


start <- parent(count - 1) while start
>= 0 do restore(a, start,
count - 1) start <- start - 1

Exemplo de classificação de heap:

Espaço Auxiliar: O(1)


Complexidade de tempo: O (nlogn)

Notas sobre algoritmos do GoalKicker.com para profissionais 165


Machine Translated by Google

Capítulo 36: Classificação de Ciclo

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 if array[i]
< item: pos += 1 se pos == CycleStart: continuar

while item == array[pos]: pos += 1


array[pos],
item = item, array[pos] escreve += 1 while pos !=
cycleStart: pos
= cycleStart para i de cycleStart + 1
até length(array) - 1 if
array[i] < item: pos += 1 enquanto item == array[pos]: pos += 1
array[pos], item = item,
array[pos]
escreve += 1

retornar _

Notas sobre algoritmos do GoalKicker.com 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, desenvolvido para uso em processadores paralelos com
interconexão local. 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. Em seguida, 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. aplique mesclagem ímpar-par (n/2) 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 do tipo ímpar-par:

Exemplo de classificação ímpar-par:

Notas sobre algoritmos do GoalKicker.com 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 {

private static void SortOddEven (int [] entrada, int n ) {

var preto = falso ;

enquanto ( !classificar )
{
ordenar =
verdadeiro ; para (var i = 1 ; i < n - 1 ; i +=
2){
if (entrada [ i ] <= entrada [i + 1 ]) continuar ; var
temp = entrada [ i ];
entrada [ i ] = entrada [i + 1 ];
entrada [i + 1 ] =
temperatura ; ordenar = falso ;

} for (var i = 0 ; i < n - 1 ; i += 2 ) {

if (entrada [ i ] <= entrada [i + 1 ]) continuar ; var


temp = entrada [ i ];
entrada [ i ] = entrada [i + 1 ];
entrada [i + 1 ] =
temperatura ; ordenar = falso ;
}

Notas sobre algoritmos do GoalKicker.com para profissionais 168


Machine Translated by Google

}
}

public static int[] Principal(int[] entrada) {

SortOddEven(entrada, entrada.Comprimento);
entrada de retorno ;
}
}

Espaço Auxiliar: O(n)


Complexidade de tempo: O(n)

Notas sobre algoritmos do GoalKicker.com para profissionais 169


Machine Translated by Google

Capítulo 38: Classificação por Seleção

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


seleção defmodule fazer

def sort(lista) quando is_list(lista) do_selection (lista, [])


end

def do_selection([head|[]], acc) do acc ++ [head] end

def do_selection(lista, acc) do


min = min(lista)
do_selection(:lists.delete(min, lista), acc ++ [min]) fim

defp min([primeiro|[segundo|[]]]) fazer


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ção.sort([100,4,10,6,9,3])
|> IO.inspect

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


Ordenação por seleção é um algoritmo de classificação, especificamente uma classificação por comparação no local. Possui complexidade de tempo O(n2),
o que o torna ineficiente em listas grandes e geralmente tem desempenho pior do que a classificação de inserção semelhante. A classificação por seleção
é conhecida por sua simplicidade e apresenta vantagens de desempenho em relação a algoritmos mais complicados em determinadas situações,
principalmente quando a memória auxiliar é limitada.

O algoritmo divide a lista de entrada em duas partes: a sublista de itens já ordenados, que é construída da esquerda para a direita na frente (esquerda) da
lista, e a sublista de itens restantes a serem ordenados que ocupam o restante da lista. lista.
Inicialmente, a sublista classificada está vazia e a sublista não classificada é a lista de entrada inteira. O algoritmo prossegue encontrando o menor (ou
maior, dependendo da ordem de classificação) elemento na sublista não classificada, trocando-o (trocando) pelo elemento não classificado mais à esquerda
(colocando-o na ordem de classificação) e movendo os limites da sublista um elemento para a direita .

Pseudocódigo para classificação por seleção:

seleção de função (lista[1..n], k)


para eu de 1 a k

Notas sobre algoritmos do GoalKicker.com para profissionais 170


Machine Translated by Google
minIndex = i
minValue = lista[i] para
j de i+1 a n if list[j] <
minValue minIndex = j
minValue =
list[j] troca lista[i] e
list[minIndex] retorna lista[k]

Visualização da classificação de seleção:

Exemplo de classificação por seleção:

Notas sobre algoritmos do GoalKicker.com para profissionais 171


Machine Translated by Google

Espaço Auxiliar: O(n)

Complexidade de tempo: O (n ^ 2)

Seção 38.3: Implementação da classificação por seleção em C#


Usei a linguagem C# para implementar o algoritmo de classificação por seleção.

classe pública SelectionSort {

private static void SortSelection(int[] entrada, int n) {

for (int i = 0; i < n - 1; i++) {

var minId = i; intj ;


para (j
= i + 1; j < n; j++) {

if (entrada[j] < entrada[minId]) minId = j;

} var temp = input[minId];

Notas sobre algoritmos do GoalKicker.com para profissionais 172


Machine Translated by Google

entrada[minId] = entrada[i];
entrada[i] = temperatura;
}
}

public static int[] Principal(int[] entrada) {

SortSelection(entrada, entrada.Comprimento);
entrada de retorno ;
}
}

Notas sobre algoritmos do GoalKicker.com para profissionais 173


Machine Translated by Google

Capítulo 39: Pesquisando

Seção 39.1: Pesquisa Binária


Introdução

A Pesquisa Binária é um algoritmo de pesquisa Divide and Conquer. Ele usa o 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 reduzindo pela metade o espaço de pesquisa em cada iteração após comparar o valor alvo com o valor médio do
espaço de pesquisa.

Para usar a Pesquisa Binária, o espaço de pesquisa 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
Pesquisa Binária.

Convencionalmente, usamos menos que (<) como função de comparação. Se a < b, retornará verdadeiro. se a não é menor que b e b não é menor
que a, a e b são iguais.

Pergunta de exemplo

Você é um economista, mas um péssimo economista. Você recebe a tarefa de encontrar o preço de equilíbrio (ou seja, o preço onde oferta =
demanda) do arroz.

Lembre-se de que quanto mais alto for definido o preço, maior será a oferta e menor será a demanda.

Como sua empresa é muito eficiente no cálculo das forças de mercado, você pode obter instantaneamente a oferta e a demanda em unidades de
arroz quando o preço do arroz for 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 número inteiro positivo que seja no
máximo 10 ^ 17 e é garantido que haja exatamente 1 solução inteira positiva no intervalo. Então continue com seu trabalho antes de perdê-lo!

Você tem permissão para chamar as funções getSupply(k) e getDemand(k), que farão exatamente o que está declarado no problema.

Exemplo de explicação

Aqui nosso espaço de busca é de 1 a 10^17. Portanto, uma pesquisa linear é inviável.

No entanto, observe que à medida que k aumenta, 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 utilizar a
Busca Binária.

O psuedocódigo a seguir demonstra o uso da pesquisa binária:

alto = 100000000000000000 baixo <- Limite superior do espaço de pesquisa


=1 <- Limite inferior do espaço de pesquisa
enquanto alto - baixo > 1
médio = (alto + baixo) / 2 <- Pegue o valor do meio
oferta = getSupply (médio)
demanda = getDemand (médio)
se oferta > demanda
alta = médio <- A solução está na metade inferior do espaço de pesquisa

Notas sobre algoritmos do GoalKicker.com 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
<- condição de oferta==demanda
médio , senão retornar médio <- Solução encontrada

Este algoritmo é executado em tempo ~O (log 10 ^ 17) . Isso pode ser generalizado para o tempo ~O(log S) onde S é o tamanho
do espaço de busca, já que a cada iteração do loop while , reduzimos pela metade o espaço de busca (de [low:high] para
[low:mid] ou [médio: alto]).

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;

if (x == a[meio]) { retorno
(meio); } else if
(x <
a[mid]) { binsearch(a,
x, low, mid - 1); } else { 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 string que usa hashing para encontrar qualquer um de um conjunto
de strings padrão em um texto. 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),
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){


/*
qum 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;

intq =100;
int
n=texto.comprimento(); int
m=padrão.comprimento(); intt
=0,p=0; inth
=1; int
eu,j; //
função de cálculo do valor hash para
(i=0;i<m-1;i++) h = (h*d)
%q; para
(i=0;i<m;i++){ p = (d*p +
padrão.charAt(i))%q; t = (d*t + texto.charAt(i))
%q; }

// procura o padrão

Notas sobre algoritmos do GoalKicker.com 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(text .charAt(j+i)!
=pattern.charAt(j)) quebra; if(j==m && i>=início)

System.out.println(" Correspondência de padrão encontrada no índice "+i);

} if(i<end-m){ t
=(d*(t - text.charAt(i)*h) + text.charAt(i+m))%q; se(t<0) t=t+q;

}
}
}

Ao calcular o valor do hash, estamos dividindo-o por um número primo para evitar colisão. 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 do hash possa ser o mesmo para duas strings, então quando Se
conseguirmos uma correspondência, temos que verificá-la caractere por caractere para ter certeza de que obtivemos uma correspondência
adequada.

t =(d*(t - text.charAt(i)*h) + text.charAt(i+m))%q;

Isso serve para recalcular o valor 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 pesquisa linear (piores, médios e


melhores casos)
Podemos ter três casos para analisar um algoritmo:

1. Pior caso

2. Caso Médio

3. Melhor caso

#include <stdio.h>

// Pesquisa linearmente x em arr[]. Se x estiver presente, retorne o índice,

// caso contrário, retorne -1


int search(int arr[], int n, int x) {

int eu;
para (i=0; i<n; i++) {

if (arr[i] == x) retornar
i;
}

retornar -1;
}

/* Programa driver para testar as funções acima*/

int principal()

Notas sobre algoritmos do GoalKicker.com para profissionais 176


Machine Translated by Google

{
int arr[] = {1, 10, 30, 15}; interno x =
30; int n =
tamanhode(arr)/tamanho(arr[0]); printf("%d
está presente no índice %d", x, search(arr, n, x));

getchar();
retornar 0;
}

Análise do pior caso (geralmente feita)

Na análise do pior caso, calculamos o limite superior do tempo de execução de um algoritmo. Devemos conhecer o caso que faz com que
o número máximo de operações seja executado. Para Pesquisa 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, o pior caso de complexidade de tempo da pesquisa linear seria ÿ(n)

Análise média de caso (às vezes feita)

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 conhecer (ou prever) a distribuição dos casos. Para
o problema de busca linear, vamos supor que todos os casos estão distribuídos uniformemente (incluindo o caso de x não estar presente
no array). Então somamos todos os casos e dividimos a soma por (n+1). A seguir está o valor da complexidade média do tempo do caso.

Melhor análise de caso (falso)

Na análise do melhor caso, calculamos o limite inferior do tempo de execução de um algoritmo. Devemos conhecer 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 na
primeira localização. O número de operações no melhor caso é constante (não depende de n). Portanto, a complexidade de tempo no
melhor caso seria ÿ (1). Na maioria das vezes, fazemos análises de pior caso para analisar algoritmos. Na pior análise, garantimos um
limite superior para o tempo de execução de um algoritmo que é uma boa informação. A análise de casos médios não é fácil de fazer na
maioria dos casos práticos e raramente é feita. Na análise do caso médio, devemos conhecer (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 dos casos, um algoritmo pode levar anos para ser executado.

Para alguns algoritmos, todos os casos são assintoticamente iguais, ou seja, não existem piores e melhores casos. Por exemplo,
classificação por mesclagem. Merge Sort realiza operações ÿ(nLogn) em todos os casos. A maioria dos outros algoritmos de classificação
tem o pior e o melhor caso. Por exemplo, na implementação típica de 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 pivôs sempre dividem o array em
duas metades. Para classificação por inserção, o pior caso ocorre quando a matriz é classificada inversamente e o melhor caso

Notas sobre algoritmos do GoalKicker.com para profissionais 177


Machine Translated by Google

ocorre quando a matriz é classificada na mesma ordem da saída.

Seção 39.4: Pesquisa binária: em números classificados


É mais fácil mostrar uma pesquisa binária de números usando pseudocódigo

int array[1000] = { lista ordenada de números }; interno N =


100; // número de entradas no espaço de busca; int alto, baixo,
médio; // nossos temporários int x; //valor a ser
procurado

baixo = 0;
alto = N -1;
while(baixo < alto) {

médio = (baixo + alto)/2;


if(array[médio] < x) baixo
= médio + 1; outro

alto = médio;

} if(array[low] == x) //
encontrado, índice é baixo

else // não encontrado

Não tente retornar mais cedo comparando array[mid] com x para igualdade. A comparação extra só pode retardar o código. Observe
que você precisa adicionar um ao menor para evitar ficar preso pela divisão inteira sempre arredondando para baixo.

Curiosamente, a versão acima da pesquisa binária permite encontrar a menor ocorrência de x na matriz. Se a matriz contém duplicatas
de x, o algoritmo pode ser ligeiramente modificado para retornar a maior ocorrência de x simplesmente adicionando ao if condicional:

while(baixo < alto) {

médio = baixo + ((alto - baixo) / 2);


if(array[mid] < x || (array[mid] == x && array[mid + 1] == x))
baixo = médio + 1;
outro
alto = médio;
}

Observe que em vez de fazer mid = (low + high) / 2, também pode ser uma boa ideia tentar mid = low + ((high - low) / 2) para
implementações como implementações Java para diminuir o risco de obter um overflow para entradas realmente grandes.

Seção 39.5: Pesquisa linear

A pesquisa linear é um algoritmo simples. Ele percorre os 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)? Na pior das hipóteses, você terá que passar por todos os n itens.

Pode ser comparado a procurar um livro em uma pilha de livros - você examina todos eles até encontrar o que deseja.

Abaixo está uma implementação Python:

Notas sobre algoritmos do GoalKicker.com 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
retorna falso

linear_search(['maçã', 'banana', 'cenoura', 'figo', 'alho'], 'figo') #returns True

Notas sobre algoritmos do GoalKicker.com 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 | uma | b | c | b | c | g | eu | x |
+-------+---+---+---+---+---+---+---+---+

+---------+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 |
+---------+---+---+---+---+
| Padrão | b | c | g | eu |
+---------+---+---+---+---+

Esse padrão existe no texto. Portanto, nossa pesquisa de substring deve retornar 3, o índice da posição a partir da qual esse padrão
começa. Então, como funciona nosso procedimento de pesquisa de substring de força bruta?

O que normalmente fazemos é: partimos do 0º índice do texto e do 0º índice do nosso *pattern e comparamos Text[0] com Pattern[0].
Como não correspondem, vamos para o próximo índice do nosso texto e comparamos Text[1] com Pattern[0]. Como se trata de
uma correspondência, incrementamos o índice do nosso padrão e também o índice do Texto . Comparamos Text[2] com
Pattern[1]. Eles também combinam. Seguindo o mesmo procedimento indicado anteriormente, agora comparamos Text[3] com
Pattern[2]. Como não coincidem, partimos da próxima posição onde começamos a encontrar a correspondência. Esse é o índice 2 do
Texto. Comparamos Text[2] com Pattern[0]. Eles não combinam. Em seguida, incrementando o índice do Texto, comparamos o
Texto[3] com o Padrão[0]. Eles combinam. 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 final do nosso Padrão, agora retornamos o índice a partir do qual
nossa correspondência começou, que é 3. Se nosso padrão fosse: bcgll, isso significa que se o padrão não existisse em nosso texto,
nossa pesquisa deveria retornar exceção ou -1 ou qualquer outro valor predefinido. Podemos ver claramente que, no pior caso, este
algoritmo levaria um tempo O(mn) , onde m é o comprimento do Texto en é o comprimento do Padrão. Como podemos reduzir essa
complexidade de tempo? É aqui que o algoritmo de pesquisa de substring KMP entra em cena.

O algoritmo de pesquisa de strings Knuth-Morris-Pratt ou o Algoritmo KMP procura 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 correspondências
anteriores personagens. 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 Text and Pattern 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 sobre algoritmos do GoalKicker.com para profissionais 180


Machine Translated by Google
+---------+---+---+---+---+---+---+---+---+
| Padrão | uma | b | c | e | uma | b | c | você |
+---------+---+---+---+---+---+---+---+---+

A princípio, nosso Texto e Padrão correspondem até o índice 2. Texto[3] e Padrão[3] não correspondem. Portanto, nosso objetivo é não
retroceder neste Texto, ou seja, em caso de incompatibilidade, não queremos que nosso emparelhamento recomece a partir da posição em
que iniciamos o emparelhamento. Para conseguir isso, procuraremos um sufixo em nosso Padrão logo antes de ocorrer nossa
incompatibilidade (substring abc), que também é um prefixo da substring de nosso Padrão. Para o nosso exemplo, como todos os
caracteres são únicos, não há sufixo, esse é o prefixo da nossa substring correspondente. Isso significa que nossa próxima comparação
começará no índice 0. Espere um pouco, você entenderá por que fizemos isso. A seguir, comparamos Text[3] com Pattern[0] e ele não
corresponde. Depois disso, para Texto do índice 4 ao índice 9 e para Padrão do índice 0 ao índice 5, encontramos uma correspondência.
Encontramos uma incompatibilidade em Text[10] e Pattern[6]. Portanto, pegamos a substring do Padrão logo antes do ponto onde ocorre a
incompatibilidade (substring abcdabc), verificamos se há um sufixo, que também é um prefixo dessa substring. Podemos ver aqui que ab é o
sufixo e o prefixo desta substring. O que isso significa é que, como combinamos 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 tivemos que olhar para todo o Texto, podemos começar
diretamente de onde ocorreu a 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
combinam. Depois disso, para Texto do índice 11 ao índice 17 e para Padrão do índice 0 ao índice 6. Encontramos uma incompatibilidade
em Texto[18] e Padrão[7]. Então, novamente, verificamos a substring antes da incompatibilidade (substring abcdabc) e descobrimos
que abc é o sufixo e o prefixo. Então, como combinamos até o Padrão[7], abc deve estar antes do Texto[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 é o nosso índice inicial da partida. É assim que nossa pesquisa de substring KMP funciona usando informações de sufixo
e prefixo.

Agora, como podemos calcular com eficiência se o sufixo é igual ao prefixo e em que ponto iniciar a verificação se há uma incompatibilidade
de caracteres entre Texto e Padrão. Vejamos um exemplo:

+---------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Padrão | uma | b | c | e | uma | b | c | uma |
+---------+---+---+---+---+---+---+---+---+

Geraremos um array contendo as informações necessárias. Vamos chamar o array de S. O tamanho do array será igual ao comprimento do
padrão. Como a primeira letra do Padrão não pode ser o sufixo de nenhum prefixo, colocaremos S[0] = 0. Tomamos i = 1 e j = 0 primeiro. Em
cada etapa comparamos Padrão[i] e Padrão[j] e incrementamos i. Se houver correspondência colocamos S[i] = j + 1 e incrementamos j, se
houver incompatibilidade, verificamos a posição do valor anterior de j (se disponível) e definimos j = S[j-1] (se j não é 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 :

j eu

+---------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Padrão | uma | b | c | e | uma | b | c | uma |
+---------+---+---+---+---+---+---+---+---+

Padrão[j] e Padrão[i] não correspondem, então incrementamos i e como j é 0, não verificamos o valor anterior e colocamos Padrão[i] = 0. Se
continuarmos incrementando i, pois i = 4, obteremos uma correspondência, então colocamos S[i] = S[4] = j + 1 = 0 + 1 = 1 e

Notas sobre algoritmos do GoalKicker.com para profissionais 181


Machine Translated by Google

incrementar j e eu. Nossa matriz ficará assim:

j eu

+---------+---+---+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Padrão | uma | b | c | e | uma | b | c | uma |
+---------+---+---+---+---+---+---+---+---+
| S |0|0|0|0|1| | | |
+---------+---+---+---+---+---+---+---+---+

Como Padrão[1] e Padrão[5] são correspondentes, 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 vamos comparar 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á na posição S[i] + 1 do Padrão. Nosso algoritmo para gerar
o array ficaria assim:

Procedimento GenerateSuffixArray(Pattern): i := 1 j := 0
n :=

Pattern.length enquanto i é
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 do tempo para construir esta matriz é O(n) e a complexidade do espaço também é O(n). Para ter certeza de que você
entendeu completamente o algoritmo, tente gerar um array 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 | uma | b | x | uma | b | c | uma | b | c | uma | b | você |
+---------+---+---+---+---+---+---+---+---+---+--- +---+---+

+---------+---+---+---+---+---+---+
| Índice | 0 | 1 | 2 | 3 | 4 | 5 |

Notas sobre algoritmos do GoalKicker.com para profissionais 182


Machine Translated by Google
+---------+---+---+---+---+---+---+
| Padrão | uma | b | c | uma | b | você |
+---------+---+---+---+---+---+---+
| S |0|0|0|1|2|0|
+---------+---+---+---+---+---+---+

Temos um Texto, um Padrão e um array S pré-calculado usando nossa lógica definida anteriormente. Comparamos Text[0] e
Pattern[0] e eles são iguais. Texto[1] e Padrão[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 igual ao prefixo em nossa substring e nossa comparação
começa na posição S[1], que é 0. Portanto, 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. Isso significa que há um prefixo
de comprimento 2 que também é o sufixo desta substring (abcab) que é ab. Isso também significa que existe um ab antes do Text[8].
Portanto, podemos ignorar com segurança Pattern[0] e Pattern[1] e iniciar nossa próxima comparação a partir de Pattern[2] e Text[8].
Se continuarmos, encontraremos o Padrão no Texto. Nosso procedimento ficará assim:

Procedimento KMP (Texto, Padrão)


GenerateSuffixArray(Padrão) m :=
Text.Length n :=
Padrão.Length i := 0

j := 0
enquanto i é menor que m
se Padrão[j] for igual a Texto[i] j := j + 1 i :=
i+1

se j é igual a n
Return (ji) else
se i < m e Pattern[j] não for igual t Text[i] se j não for igual a 0 j =
S[j-1] else

i := i + 1 fim
se fim
se
terminar enquanto

Retorno -1

A complexidade de tempo deste algoritmo, além do cálculo da matriz de sufixos, é O (m). Como GenerateSuffixArray leva O(n), a
complexidade de tempo total do algoritmo KMP é: O(m+n).

PS: Se você deseja encontrar múltiplas ocorrências de Padrão no Texto, em vez de retornar o valor, imprima/armazene 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 de acordo.

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 hashing para
encontrar qualquer um de um conjunto de strings de padrão 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 dada.

No algoritmo Rabin-Karp, geraremos um hash do nosso padrão que estamos procurando e verificaremos se o hash contínuo 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 sobre algoritmos do GoalKicker.com 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 hash rolante, precisaremos usar um número primo. Pode ser qualquer número primo. Vamos considerar primo = 11 para
este exemplo. Determinaremos o valor do hash usando esta fórmula:

(1ª letra) X (principal) + (2ª letra) X (principal)¹ + (3ª letra) X (principal)² X + ......

Vamos denotar:

uma -> g -> 7h m -> 13 s -> 19 anos -> 25


1 b -> 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 -> k -> 11 q -> 17 w -> 23
6 eu -> 12 r -> 18 x -> 24

O valor hash de nsa será:

14 X 11ÿ + 19 X 11¹ + 1 X 11² = 344

Agora encontramos o hash rolante do nosso texto. Se o hash rolante corresponder ao valor hash do nosso padrão, verificaremos se
as strings correspondem ou não. Como nosso padrão tem 3 letras, pegaremos as 3 primeiras letras do nosso texto e calcularemos
valor hash. Nós temos:

25 X 11ÿ + 5 X 11¹ + 13 X 11² = 1653

Este valor não corresponde ao valor hash do nosso padrão. Portanto, 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 de hash atual. Neste caso, você. Nós
obtenha, 1653 - 25 = 1628.
Dividimos a diferença pelo nosso primo, que é 11 neste exemplo. Obtemos 1628/11 = 148 . _
Adicionamos a nova letra X (primo)ÿ¹, onde m é o comprimento do padrão, com o quociente, que é i = 9.
obtenha 148 + 9 X 11² = 1237.

O novo valor de hash não é igual ao valor de hash de nossos padrões. Seguindo em frente, para n obtemos:

String Anterior : emi


Primeira letra da string anterior: e(5)
Nova Carta: n(14)
Nova string: "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 String: "ins"
1806 - 13 = 1793
1793/11 = 163 _ _

Notas sobre algoritmos do GoalKicker.com para profissionais 184


Machine Translated by Google

163 + 19 X 11² = 2462

Não corresponde. A seguir, para a, obtemos:

String Anterior : ins


Primeira letra da string anterior: i(9)
Nova Carta: a(1)
Nova string : "nsa"
2462 - 9 = 2453
2453/11 = 223
223 + 1 X 11² = 344

É uma combinação! 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 Calcula-Hash(String, Prime, x): hash := 0


// Aqui x denota o comprimento a ser considerado para m
de 1 a x // para encontrar o valor hash
hash := hash + (Valor de String[m])ÿ¹ fim para

Hash de retorno

Recálculo de hash:

Procedimento Recalcular-Hash (String, Curr, Prime, Hash): Hash := Hash


- Valor da String[Curr] //aqui Curr denota a primeira letra da string anterior Hash := Hash / Prime m := String.length
Novo := Curr + m - 1

Hash := Hash + (Valor da String[Novo])ÿ¹


Hash de retorno

Correspondência de cordas:

Procedimento String-Match(Texto, Padrão, m): para i


de m a Comprimento do padrão + m - 1
se Text[i] não for igual a Pattern[i]
Retorna fim
falso
se for final
Retornar verdadeiro

Rabin Karp:

Procedimento Rabin-Karp(Texto, Padrão, Prime): m :=


Pattern.Length
HashValue := Calcular-Hash(Padrão, Prime, m)
CurrValue : = Calcular-Hash (Padrão, Prime, m) para i de 1 a
Text.length - m
se HashValue == CurrValue e String-Match(Text, Pattern, i) for verdadeiro
Volte eu
fim se
CurrValue := Recalcular-Hash(String, i+1, Prime, CurrValue) fim para

Notas sobre algoritmos do GoalKicker.com para profissionais 185


Machine Translated by Google
Retorno -1

Se o algoritmo não encontrar nenhuma correspondência, ele simplesmente retornará -1.

Este algoritmo é usado na detecção de plágio. Dado o material de origem, o algoritmo pode pesquisar rapidamente em um artigo por instâncias
de sentenças do material de origem, ignorando detalhes como maiúsculas e minúsculas e pontuação. Devido à abundância de strings procuradas,
algoritmos de busca de string única são impraticáveis aqui. Novamente, o algoritmo Knuth-Morris-Pratt ou o algoritmo Boyer-Moore
String Search é um algoritmo de pesquisa de string de padrão único mais rápido do que Rabin-Karp. No entanto, é um algoritmo de
escolha para pesquisa de múltiplos padrões. 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 de Rabin-Karp.

Para padrões de texto de comprimento n e p de comprimento combinado m, seu tempo de execução médio e de 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


Haystack: A string na qual determinado padrão 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ária para construir a tabela de prefixos onde m é o comprimento
de a agulha.

Portanto, a complexidade geral de tempo para KMP é O(n+m)


Complexidade do espaço: O(m) por causa da tabela de prefixos na agulha.

Nota: 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
delimeter = 1
while(delimeter<n):

prefix_set.add(needle[:delimeter]) j = 1 while(j

<delimeter+1): se
agulha[j:delimeter+1] em prefix_set:
prefix_table[delimeter] = delimitador - j + 1 quebra

j += 1
delimitador += 1
return prefix_table

def strstr(palheiro, agulha):


# m: denotando a posição dentro de S onde a correspondência prospectiva para W começa # i:
denotando o índice do caracter atualmente considerado em W. haystack_len =
len(haystack) Needle_len =
len(needle) if (needle_len >
haystack_len) ou (not haystack_len) ou (não Needle_len):
return -1
prefix_table = get_prefix_table(needle) m = i = 0

while((i<needle_len) and (m<haystack_len)): if haystack[m]


== agulha[i]: i += 1

Notas sobre algoritmos do GoalKicker.com para profissionais 186


Machine Translated by Google
m+=1
outro:
se eu ! = 0:
i = prefix_table[i-1] senão:

m += 1
se i==lenço_agulha e palheiro[m-1] == agulha[i-1]: return m - lenço_agulha
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[] = "PAIPAI" pat[] = "PAI"

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>

#include<string.h>
#include<stdlib.h>

void computaLPSArray(char *pat, int M, int *lps);

void KMPSearch(char *pat, char *txt) {

int M = strlen(pat); int N =


strlen(txt);

// cria lps[] que conterá o sufixo de prefixo mais longo

Notas sobre algoritmos do GoalKicker.com para profissionais 187


Machine Translated by Google

// valores para o padrão int


*lps = (int *)malloc(sizeof(int)*M); int j = 0; //índice
para pat[]

// Pré-processa o padrão (calcula o array lps[])


computeLPSArray(pat, M, lps);

int eu = 0; //índice para txt[] while (i <


N) {

if (pat[j] == txt[i]) {

j++;
eu++;
}

se (j == M) {

printf(" Padrão encontrado no índice %d \n", ij); j = lps[j-1];

// incompatibilidade após j
correspondências else if (i < N && pat[j] != txt[i])
{
// Não correspondem aos caracteres lps[0..lps[j-1]], // eles
corresponderão de qualquer
maneira if (j !
= 0) j = lps[j-1];
outro
eu = eu+1;
}

} grátis(lps); //para evitar vazamento de memória


}

void computaLPSArray(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 para M-1 while (i < M) {

if (pat[i] == pat[len]) {

lente++;
lps[i] = comprimento;
eu++;

} else // (pat[i] != pat[len]) {

if (len ! = 0) {

// Isso é complicado. Considere o exemplo //


AAACAAAA e i = 7. len =
lps[len-1];

// Além disso, observe que não incrementamos i aqui

Notas sobre algoritmos do GoalKicker.com para profissionais 188


Machine Translated by Google

} else // if (len == 0) {

lps[i] = 0; eu+
+;
}
}
}
}

// Programa 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:

http://www.geeksforgeeks.org/searching-for-patterns-set-2-kmp-algorithm/

Notas sobre algoritmos do GoalKicker.com para profissionais 189


Machine Translated by Google

Capítulo 41: Pesquisa Ampla


Seção 41.1: Encontrando o caminho mais curto da origem para
outros nós
Pesquisa ampla (BFS) é um algoritmo para percorrer ou pesquisar estruturas de dados em árvores ou gráficos. Ele começa na raiz da árvore (ou
em algum nó arbitrário de um gráfico, às vezes chamado de 'chave de pesquisa') e explora primeiro os nós vizinhos, antes de passar para os
vizinhos do próximo nível. O BFS foi inventado no final da década de 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 partimos 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 do nó de origem.
nós de nível 1 são nós de nível 2 e assim por diante.

4. O nível indica a distância do caminho mais curto da fonte.

Vejamos um exemplo:

Vamos supor que este gráfico represente a conexão entre várias cidades, onde cada nó denota uma cidade e uma aresta entre dois nós indica
que há uma estrada que os liga. Queremos ir do nó 1 ao nó 10. Portanto, 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 a partir daqui. Portanto, eles serão nós de nível (0+1) = nível 1 . Agora vamos marcá-los como visitados e
trabalhar com eles.

Notas sobre algoritmos do GoalKicker.com 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 sobre algoritmos do GoalKicker.com para profissionais 191


Machine Translated by Google

Se você não percebeu, o nível dos nós simplesmente indica a distância mais curta do caminho da origem. Por exemplo:
encontramos o nó 8 no nível 2. Portanto, a distância da fonte ao nó 8 é 2.

Ainda não alcançamos 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 sobre algoritmos do GoalKicker.com 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 encontrou o caminho mais curto. Agora vamos apagar as bordas que não usamos:

Notas sobre algoritmos do GoalKicker.com para profissionais 193


Machine Translated by Google

Depois de remover as arestas que não usamos, obtemos uma árvore chamada árvore BFS. Esta árvore mostra o caminho mais curto da
origem até todos os outros nós.

Portanto, nossa tarefa será ir dos nós de origem aos nós de nível 1 . Depois, dos nós de nível 1 para o nível 2 e assim por diante até
chegarmos ao nosso destino. Podemos usar queue para armazenar os nós que iremos processar. Ou seja, para cada nó com o qual
trabalharemos, enviaremos todos os outros nós que podem ser percorridos diretamente e ainda não percorridos 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. nível[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. Alcançamos esses nós a partir do nó 1. Então nível[4] = nível[3] = nível[2] = nível[1] +
1 = 1. Agora os marcamos como visitados e os colocamos na fila.

frente
+-----+ +-----+ +-----+
|2| |3| |4|
+-----+ +-----+ +-----+

Notas sobre algoritmos do GoalKicker.com para profissionais 194


Machine Translated by Google

Agora abrimos o nó 4 e trabalhamos com ele. Podemos ir para o nó 7 a partir do nó 4. nível[7] = nível[4] + 1 = 2. Marcamos o nó 7
como visitado e coloque-o na fila.

frente
+-----+ +-----+ +-----+
|7| |2| |3|
+-----+ +-----+ +-----+

Do nó 3, podemos ir para o nó 7 e para o nó 8. Como já marcamos o nó 7 como visitado, marcamos o nó 8 como


visitado, alteramos nível[8] = nível[3] + 1 = 2. Colocamos o nó 8 na fila.

frente
+-----+ +-----+ +-----+
|6| |7| |2|
+-----+ +-----+ +-----+

Este processo continuará até chegarmos ao nosso destino ou a fila ficar vazia. A matriz de níveis nos fornecerá
com a distância do caminho mais curto da fonte. Podemos inicializar o array de níveis 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
Q.push(fonte)
enquanto Q não está vazio
você -> Q.pop()
para todas as arestas de u a v na lista de adjacências
se nível[v] == infinito
nível[v] := nível[u] + 1
Q.push(v)
fim se
fim para
terminar enquanto

Nível de retorno

Ao iterar pela matriz 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, poderemos precisar imprimir não apenas a distância mais curta, mas também o caminho pelo qual podemos chegar ao nosso destino.
nó destinado da origem. Para isso precisamos manter um array pai . pai[fonte] será NULO. Para cada
update na matriz de nível , simplesmente adicionaremos parent[v] := u em nosso pseudocódigo dentro do loop for. Depois de terminar o BFS,
para encontrar o caminho, percorreremos de volta o array pai até chegarmos à fonte , que será denotada pelo valor NULL.
O pseudocódigo será:

Procedimento PrintPath(u): //recursivo se pai[u] | Procedimento PrintPath(u): // iterativo


não for igual a nulo | S = Pilha()
PrintPath(pai[u]) fim se | enquanto pai[u] não é igual a null S.push(u)
imprimir
-> u | você := pai[u]
| | terminar enquanto
| enquanto S não está vazio
| imprimir -> S.pop
| terminar enquanto

Notas sobre algoritmos do GoalKicker.com para profissionais 195


Machine Translated by Google

Complexidade:

Visitamos todos os nós uma vez e todas as arestas uma vez. Portanto, 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, precisaremos descobrir o caminho mais curto de uma única fonte para todos os outros nós ou para um nó específico em um gráfico
2D. Digamos por exemplo: queremos descobrir quantos movimentos são necessários para um cavalo chegar a uma determinada casa em um
tabuleiro de xadrez, ou temos um array onde algumas células estão bloqueadas, temos que descobrir o caminho mais curto de uma célula para outra .
Só podemos nos mover horizontalmente e verticalmente. Até movimentos diagonais também podem ser possíveis.
Para estes casos, podemos converter os quadrados ou células em nós e resolver estes problemas facilmente usando BFS. Agora nossos visitados, pai
e nível serão arrays 2D. Para cada nó, consideraremos todos os movimentos possíveis. Para saber a distância até um nó específico, também verificaremos
se chegamos ao nosso destino.

Haverá uma coisa adicional chamada matriz de direção. Isso simplesmente armazenará todas as combinações possíveis de direções que podemos
seguir. Digamos que, para movimentos horizontais e verticais, nossas matrizes de direção serão:

+----+-----+-----+-----+-----+
| dx | 1 | -1 | 0 | 0 |
+----+-----+-----+-----+-----+
| você | 0 | 0 | 1 | -1 |
+----+-----+-----+-----+-----+

Aqui dx representa o movimento no eixo x e dy representa o 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 de direção. Pode haver mais e até 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 todos os movimentos possíveis, verificaremos se a célula está bloqueada ou não.
Também verificaremos se ultrapassamos os limites, ou seja, ultrapassamos os limites do array.
O número de linhas e colunas será fornecido.

Nosso pseudocódigo será:

Procedimento BFS2D(Gráfico, sinal de bloco, linha,


coluna): para i de 1 a
linha para j de 1 a coluna
visitado[i][j] := fim falso para

fim para
visitado[source.x][source.y] := nível
verdadeiro [source.x][source.y] := 0
Q = fila()
Q.push (source)
m := dx.size
enquanto Q não está
vazio top :=
Q.pop 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 ao blocksign visitado[temp.x]
[temp.y] := true level[temp.x][temp.y] :=
level[ top.x][top.y] + 1 Q.push(temp)

Notas sobre algoritmos do GoalKicker.com para profissionais 196


Machine Translated by Google

fim se
fim por fim
enquanto
Nível de retorno

Como discutimos anteriormente, o BFS funciona apenas 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, este algoritmo é um
algoritmo de caminho mais curto de fonte única. Se precisarmos descobrir a distância de cada nó a todos os outros nós, precisaremos
do algoritmo de Floyd-Warshall.

Seção 41.3: Componentes conectados de gráfico 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
fornecido está conectado ou não. Nossa discussão subsequente pressupõe que estamos lidando com gráficos não direcionados. A definição
de um gráfico conectado é:

Um grafo é conectado se existe um caminho entre cada par de vértices.

A seguir está um gráfico conectado.

O gráfico a seguir não está conectado e possui 2 componentes conectados:

1. Componente conectado 1: {a,b,c,d,e}


2. Componente Conectado 2: {f}

Notas sobre algoritmos do GoalKicker.com para profissionais 197


Machine Translated by Google

BFS é um algoritmo de passagem de gráfico. Portanto, começando a partir de um nó de origem aleatório, se ao término do algoritmo todos os
nós forem visitados, então o grafo está conectado, caso contrário, não está conectado.

Pseudocódigo para o algoritmo.

boolean isConnected(Graph g) { BFS(v)//

v é um nó de origem aleatório. if(todosvisitados(g))


{

retornar

verdadeiro; } senão
retorna falso; }

Implementação C para descobrir se um gráfico não direcionado está conectado ou não:

#include<stdio.h>
#include<stdlib.h> #define
MAXVERTICES 100

void enfileiramento(int);
int deque(); int
isConnected(char **gráfico,int noOfVertices); void BFS(char
**gráfico,int vértice,int noOfVertices); contagem interna = 0; //O nó da fila
representa um único
elemento da fila //NÃO é um nó gráfico. nó de estrutura {

na TV;
nó de estrutura *próximo;
};

typedef struct nó Nó; nó de estrutura


typedef *Nodeptr;

Nodeptr Qfront = NULL; Nodeptr


Qrear = NULO; char *visited; //
array que rastreia os vértices visitados.

int principal()

Notas sobre algoritmos do GoalKicker.com para profissionais 198


Machine Translated by Google

{
int n,e;//n é o número de vértices, e é o número de arestas. int eu,j; char **gráfico; //
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 número inteiro positivo válido de 1 a %d",MAXVERTICES); retornar -1; }

gráfico = malloc(n * sizeof(char *)); visitado =


malloc(n*sizeof(char));

for(i = 0;i < n;++i) {

gráfico[i] = malloc(n*sizeof(int)); visitado[i] = 'N';//


inicialmente todos os vértices não são visitados. for(j = 0;j < n;++j) gráfico[i][j] = 0;

printf("insira o número de arestas e depois insira-as em pares:"); scanf("%d",&e);

for(i = 0;i < e;++i) {

int você,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 enfileiramento(int vértice) {

if(Qfront == NULO) {

Qfront = malloc(tamanho(Nó));
Qfront->v = vértice;
Qfront->próximo = NULO;
Qtraseiro = Qfrontal;

} outro
{
Nodeptr newNode = malloc(sizeof(Node)); novoNode->v
= vértice; novoNode->próximo
= NULL;
Qrear->próximo = novoNode;
Qrear = novoNode;
}
}

int deque() {

Notas sobre algoritmos do GoalKicker.com para profissionais 199


Machine Translated by Google

if(Qfront == NULO) {

printf("Q está vazio, retornando -1\n"); retornar -1;

} outro
{
int v = Qfront->v;
Nodeptr temp = Qfront;
if(Qfront == Qtraseiro) {

Qfront = Qfront->próximo;
Qrear = NULO;

} outro
Qfront = Qfront->próximo;

grátis(temperatura);
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;

return 1;//1 implica verdadeiro;


}

void BFS(char **gráfico,int v,int noOfVertices) {

int i,vértice;
visitado[v] = 'Y';
enfileirar(v);
while((vértice = deque()) != -1) {

for(i = 0;i < noOfVertices;++i) if(graph[vertex]


[i] == 1 && visitado[i] == 'N') {

enfileirar(i);
visitado[i] = 'Y';
}
}
}

Para encontrar todos os componentes conectados de um gráfico 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 em 0 // adiciona isto como
primeira linha à função BFS

Notas sobre algoritmos do GoalKicker.com para profissionais 200


Machine Translated by Google

printf("%d ",vértice+1);
adicione isso como primeira linha do loop while no BFS

e definimos a seguinte função:

void listConnectedComponents(char **gráfico,int noOfVertices) {

int eu;
for(i = 0;i < noOfVertices;++i) {

if(visitado[i] == 'N')
BFS(gráfico,i,noOfVertices);

}
}

Notas sobre algoritmos do GoalKicker.com para profissionais 201


Machine Translated by Google

Capítulo 42: Primeira Pesquisa em Profundidade

Seção 42.1: Introdução à pesquisa em profundidade


Pesquisa em profundidade é um algoritmo para percorrer ou pesquisar estruturas de dados em árvores ou gráficos. Começa-se
pela raiz e explora-se o máximo possível ao longo de cada ramo antes de voltar atrás. Uma versão da pesquisa em
profundidade foi investigada no século 19 pelo matemático francês Charles Pierre Trémaux como uma estratégia para resolver labirintos.

A pesquisa em profundidade é uma forma sistemática de encontrar todos os vértices acessíveis a partir de um vértice de origem. Assim como
a pesquisa em largura, o DFS percorre um componente conectado de um determinado gráfico e define uma árvore geradora. A ideia básica da
pesquisa em profundidade é explorar metodicamente cada aresta. Recomeçamos a partir de vértices diferentes, 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 possa explorar
a partir dele mais tarde).

Vejamos um exemplo. Percorreremos 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 em preto.

Vejamos passo a passo:

Notas sobre algoritmos do GoalKicker.com para profissionais 202


Machine Translated by Google

Notas sobre algoritmos do GoalKicker.com para profissionais 203


Machine Translated by Google

Notas sobre algoritmos do GoalKicker.com para profissionais 204


Machine Translated by Google

Podemos ver uma palavra-chave importante. Isso é backedge. Você pode ver. 5-1 é chamado de backedge. Isso ocorre porque ainda não terminamos o nó 1,

então passar 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 possui um ciclo. Esta é uma das formas de detectar 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 fossemos primeiro para 5 a partir de 1 , teríamos descoberto 2-1 como backedge.

A aresta que tomamos para ir do nó cinza ao nó branco é chamada de aresta da árvore. Se mantivermos apenas as arestas da árvore e removermos outras,

obteremos a árvore DFS.

No grafo não direcionado, se pudermos visitar um nó já visitado, isso deve ser uma backedge. Mas para gráficos direcionados, devemos verificar as cores. Se,

e somente se, pudermos ir de um nó cinza para outro nó cinza, isso será chamado de backedge.

No DFS, também podemos manter carimbos de data/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 sobre algoritmos do GoalKicker.com 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 pesudo-código ficará assim:

Procedimento DFS(G):
para cada nó u em V[G] color[u] :=
branco pai[u] := NULL

fim do
tempo : = 0
para cada nó u em V [G]
se cor[u] == branco
Visita DFS (u)
termina se
fim para

Procedimento DFS-Visit(u):
color[u] := tempo cinza :=
tempo + 1 d[u] := tempo
para cada nó v
adjacente a u if color[v] == branco pai[v] := u

Visita DFS (v) end


if end for
color[u] :=
black time := time + 1
f[u] := time

Complexidade:

Cada nó e aresta são visitados uma vez. Portanto, 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 primeira pesquisa em profundidade:

Encontrando o caminho mais curto de todos os pares em um gráfico não direcionado.

Detectando ciclo em um gráfico.

Encontrando o caminho.

Classificação topológica.

Testando se um gráfico é bipartido.

Encontrando Componentes Fortemente Conectados.

Resolvendo quebra-cabeças com uma solução.

Notas sobre algoritmos do GoalKicker.com para profissionais 206


Machine Translated by Google

Capítulo 43: Funções Hash


Seção 43.1: Códigos hash para tipos comuns em C#
Os códigos hash produzidos pelo método GetHashCode() para integração e tipos C# comuns do namespace System são
mostrados abaixo.

boleano

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);


Caracteres

(int)m_valor ^ ((int)m_valor << 16);


Int16

((int)((ushort)m_valor) ^ (((int)m_valor) << 16));


Int64, Dobro

Xor entre 32 bits inferiores e superiores de um número de 64 bits

(desmarcado((int)((long)m_value)) ^ (int)(m_value >> 32));


UInt64, Data hora, Intervalo de
tempo ((int)m_valor) ^ (int)(m_valor >> 32);
Decimal

((((int *)&dbl)[0]) & 0xFFFFFFF0) ^ ((int *)&dbl)[1];


Objeto

RuntimeHelpers.GetHashCode(este);

A implementação padrão é usada no índice do bloco de sincronização.

Corda

O cálculo do código hash depende do tipo de plataforma (Win32 ou Win64), recurso de uso de hashing de string aleatório, modo de
depuração/liberação. No caso da plataforma Win64:

int hash1 = 5381; int


hash2 = hash1;
interno
c; char *s = fonte;
enquanto ((c = s[0]) != 0)
{ hash1 = ((hash1 << 5) + hash1) ^ c = c;
s[1]; se (c
== 0)

quebrar; hash2 = ((hash2 << 5) + hash2) c;


^ s += 2;
}

Notas sobre algoritmos do GoalKicker.com para profissionais 207


Machine Translated by Google

retornar hash1 + (hash2 * 1566083941);

Tipo de valor

O primeiro campo não estático é procurado e obtido seu código hash. Se o tipo não tiver campos não estáticos, o código hash do tipo será
retornado. O código hash 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
terminará em um loop infinito.

Anulável<T>

retornar temValor ? valor.GetHashCode() : 0;

Variedade

int ret = 0; for


(int i = (Comprimento >= 8 ? Comprimento - 8 : 0); i < Comprimento; i++) {

ret = ((ret << 5) + ret) ^ comparar.GetHashCode(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 mapeou dados x ÿ X de tamanho arbitrário para o 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 determinado x

cálculo rápido (tem tempo de execução O (1))

Em geral, o tamanho da função hash é menor que o tamanho dos dados de entrada: |y| < |x|. Funções hash não são reversíveis ou em
outras palavras pode haver colisão: ÿ x1, x2 ÿ X, x1 ÿ x2: h(x1) = h(x2). X pode ser um conjunto finito ou infinito e Y é um conjunto
finito.

As 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.

Freqüentemente, 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 dados possui implementações próprias ou definidas pelo usuário.

Métodos hash

Existem várias abordagens para determinar a função hash. Sem perda de generalidade, sejam x ÿ X = {z ÿ ÿ: z ÿ 0} números inteiros
positivos. Muitas vezes 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 hash

Funções hash usadas em tabelas hash para calcular índices em uma matriz de slots. A tabela hash é uma estrutura de dados para

Notas sobre algoritmos do GoalKicker.com para profissionais 208


Machine Translated by Google

implementação de dicionários (estrutura de valores-chave). 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ão:

1. Encadeamento: 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 teste necessárias para o 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
Hashing 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 auxiliares positivas
constantes.

Exemplos

Seja x ÿ U{1, 1000}, h = x mod m. A próxima tabela mostra os valores de hash no caso de não primo e primo. 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 hash

Wolfram MathWorld - Função Hash

Notas sobre algoritmos do GoalKicker.com 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 por cada vértice exatamente uma vez, podemos aplicar força bruta em cada um dos N! permutações dos números de 1 a N.

Psuedocódigo

mínimo = INF para


todas as permutações P

corrente = 0

para eu 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 <- Atualize 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, este algoritmo leva O(N * N!) tempo para produzir
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 ao vértice 2 e ao vértice 3 permanece o mesmo, então por que 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 na 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 sobre algoritmos do GoalKicker.com para profissionais 210


Machine Translated by Google

Assim podemos ter o seguinte algoritmo (implementação em C++):

custo interno [N][N]; // Ajuste o valor de N se necessário


memorando interno [1 << // Define tudo aqui como -1
N][N]; int TSP(int máscara de bits, int pos){
custo interno = INF;
if (bitmask == ((1 << N) - 1)){ custo de // Todos os vértices foram explorados
retorno [pos][0]; // Custo para voltar
}
if (memo[bitmask][pos] != -1){ return // Se isso já foi calculado
memo[bitmask][pos]; // Basta retornar o valor, não há necessidade de recalcular
}
for (int i = 0; i < N; ++i){ // Para cada vértice
if ((bitmask & (1 << i)) == 0){ //Se o vértice não foi visitado
custo = min(custo,TSP(bitmask | (1 << i) , i) + custo[pos][i]); //Visite o vértice
}
}
memo[bitmask][pos] = custo; custo // Salva o resultado
de devolução ;
}
// Chama o 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, máscara de bits | (1 << i) define o i-ésimo bit da máscara de bits como 1, o que representa que o i-ésimo vértice foi visitado. O
i depois da vírgula representa o novo pos 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 atualiza o valor do custo para o valor mínimo possível de viajar para todos os outros vértices que
ainda não foi visitado.

Complexidade de tempo

A função TSP(bitmask,pos) possui 2^N valores para bitmask e N valores para pos. Cada função leva O(N) tempo para

execute (o loop for ). Portanto, esta implementação leva tempo O(N^2 * 2^N) para gerar a resposta exata.

Notas sobre algoritmos do GoalKicker.com para profissionais 211


Machine Translated by Google

Capítulo 45: Problema da Mochila


Seção 45.1: Noções básicas do problema da mochila
O problema: Dado um conjunto de itens onde cada item contém um peso e um valor, determine o número de cada um a
incluir em uma coleção de modo que o peso total seja menor ou igual a um determinado limite e o valor total seja o maior
possível.

Pseudocódigo para 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 mochila(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 c==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] peso
= [10, 20, 30]
W = 50
n = len(val)
print(mochila(W, wt, val, n))

Executando o código: Salve em um arquivo chamado knapSack.py

$python knapSack.py 220

Complexidade temporal 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 Problema da mochila


{

Notas sobre algoritmos do GoalKicker.com para profissionais 212


Machine Translated by Google

private static int Mochila(int w, int[] peso, int[] valor, int n) {

int eu;
int[,] k = novo int[n + 1, w + 1]; para (eu
= 0; eu <= n; eu++) {

intb ;
para (b = 0; b <= w; b++) {

se (i==0 || b==0) {

k[eu, b] = 0;

} else if (peso[i - 1] <= b) {

k[i, b] = Math.Max(valor[i - 1] + k[i - 1, b - peso[i - 1]], k[i - 1, b]);

}
outro {
k[eu, b] = k[eu - 1, b];
}
}

} retornar k[n, w];


}

public static int Main(int nItems, int[] pesos, int[] valores) {

int n = valores.Comprimento;
return Mochila(nItens, pesos, valores, n);
}
}

Notas sobre algoritmos do GoalKicker.com 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 resolvemos diretamente de uma equação.

2. Método Iterativo: Métodos Iterativos ou Indiretos, começam com uma estimativa da solução e depois refinam repetidamente a solução
até que um determinado critério de convergência seja alcançado. Os métodos iterativos são geralmente menos eficientes que os
métodos diretos porque são necessários um grande número de operações. 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 das
variáveis int rootFound=0; //bandeira

int eu, j;
while(!rootFound){ for(i=0;
i<n; i++){ Nx[i]=b[i]; // Cálculo

for(j=0; j<n; j++){ if(i!=j) Nx[i]


= Nx[i]-a[i][j]*x[j];
}
Nx[i] = Nx[i] / a[i][i];
}

raizEncontrada=1; // verificação
for(i=0; i<n; i++){ if(!( (Nx[i]-
x[i])/x[i] > -0,000001 && (Nx[i]-x[i])/x [eu] < 0,000001 )){
raizEncontrada=0;
quebrar;
}
}

for(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 das variáveis
int rootFound=0; //bandeira

int eu, j;
for(i=0; i<n; i++){ // inicialização
Nx[i]=x[i];
}

Notas sobre algoritmos do GoalKicker.com para profissionais 214


Machine Translated by Google

while(!rootFound){ for(i=0;
i<n; i++){ Nx[i]=b[i]; // Cálculo

for(j=0; j<n; j++){ if(i!=j) Nx[i]


= Nx[i]-a[i][j]*Nx[j];
}
Nx[i] = Nx[i] / a[i][i];
}

raizEncontrada=1; // verificação
for(i=0; i<n; i++){ if(!( (Nx[i]-
x[i])/x[i] > -0,000001 && (Nx[i]-x[i])/x [eu] < 0,000001 )){
raizEncontrada=0;
quebrar;
}
}

for(i=0; i<n; i++){ x[i]=Nx[i]; // avaliação

}
}

retornar ;
}

// Imprime 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;

for(i=0; i<n; i++){ x[i]=0; // inicialização

}
JacobisMethod(n, x, b, a); imprimir(n, x);

for(i=0; i<n; i++){ // inicialização

Notas sobre algoritmos do GoalKicker.com para profissionais 215


Machine Translated by Google

x[eu]=0;
}
GaussSeidalMethod(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 resolverem um problema.
equação. Baseia-se no conceito de aproximação sucessiva. No Método Iterativo, existem duas maneiras de resolver uma equação-

Método de colchetes: pegamos dois pontos iniciais onde a raiz está entre eles. Método de exemplo-bissecção, método
de posição falsa.

Método Open End: Tomamos um ou dois valores iniciais onde a raiz pode estar em qualquer lugar. Método Exemplo-Newton-
Raphson, Método de Aproximação Sucessiva, Método Secante.

Implementação em C:

/// Aqui defina diferentes funções para trabalhar #define f(x)


( ((x)*(x)*(x)) - (x) - 2 ) #define f2(x) ( (3*(x) *(x)) - 1 )
#define g(x) ( cbrt( (x) + 2 ) )

/ **
* Toma 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 loopContador=0;
if(f(a)*f(b) < 0){ while(1)
{ loopCounter+
+; c=(a+b)/2;

if(f(c)<0,00001 && f(c)>-0,00001){


raiz=c;
quebrar;
}

if((f(a))*(f(c)) < 0){ b=c; }outro{

uma=c;

Notas sobre algoritmos do GoalKicker.com para profissionais 216


Machine Translated by Google

} printf("Foram necessários %d loops.\n", loopCounter);

retornar raiz;
}

/ **
* Pega dois valores iniciais e encurta a distância em um único lado. **/ double FalsePosition()

{ double root=0;

duplo a=1, b=2; duplo


c=0;

int loopContador=0;
if(f(a)*f(b) < 0){ while(1)
{ loopCounter+
+;

c=(a*f(b) - b*f(a)) / (f(b) - f(a));

/ */ printf("%lf\t %lf \n", c, f(c));/ **//// teste if(f(c)<0.00001 &&


f(c)>-0.00001){ raiz=c; quebrar;

if((f(a))*(f(c)) < 0){ b=c; }

else{ a=c;

}
}

} printf("Foram necessários %d loops.\n", loopCounter);

retornar raiz;
}

/ **
* Usa um valor inicial e gradualmente aproxima esse valor do valor real. **/ double NewtonRaphson()

{ double root=0;

duplo x1=1;
duplo x2=0;

int loopContador=0;
while(1)
{ loopContador++;

x2 = x1 - (f(x1)/f2(x1)); /*/ printf("%lf


\t %lf \n", x2, f(x2));/ **//// teste

if(f(x2)<0,00001 && f(x2)>-0,00001){ raiz=x2;


quebrar;

Notas sobre algoritmos do GoalKicker.com 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 valor real. **/ double Ponto Fixo(){ double

root=0; duplo x=1;

int loopContador=0;
while(1)
{ loopContador++;

if( (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 ambas as abordagens de valor para a raiz. **/ double

Secante(){ double
root=0;

duplo x0=1;
duplo x1=2;
duplo x2=0;

int loopContador=0;
while(1)
{ loopContador++;

/ */ printf("%lf \t %lf \t %lf \n", x0, x1, f(x1));/ **//// teste

if(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 sobre algoritmos do GoalKicker.com para profissionais 218


Machine Translated by Google

int principal(){
raiz dupla ;

raiz = BisectionMethod();
printf("Usando o método de bissecção a raiz é: %lf \n\n", root);

raiz = FalsePosition();
printf("Usando o Método de Posição Falsa a raiz é: %lf \n\n", root);

raiz = NewtonRaphson();
printf("Usando o Método Newton-Raphson a raiz é: %lf \n\n", root);

raiz = PontoFixo();
printf("Usando o Método de Ponto Fixo a raiz é: %lf \n\n", root);

raiz = Secante();
printf("Usando o Método Secante a raiz é: %lf \n\n", root);

retornar 0;
}

Notas sobre algoritmos do GoalKicker.com para profissionais 219


Machine Translated by Google

Capítulo 47: Mais Longo Comum


Subsequência
Seção 47.1: Explicação da Subsequência Comum Mais Longa
Uma das implementações mais importantes da Programação Dinâmica é descobrir o Maior Tempo Comum
Subsequência. Vamos definir algumas das terminologias básicas primeiro.

Subsequência:

Uma subsequência é uma sequência que pode ser derivada de outra sequência excluindo 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á um
subsequência. Para descobrir a subsequência, para cada caractere de uma string, temos duas opções - ou pegamos o
personagem, ou não. Portanto, se o comprimento da string for n, existem 2n subsequências dessa string.

Subsequência comum mais longa:

Como o nome sugere, de todas as subsequências comuns entre duas strings, a subsequência comum mais longa (LCS)
é aquele com comprimento máximo. Por exemplo: As subsequências comuns entre "HELLOM" e "HMLD"
são "H", "HL", "HM" etc. Aqui "HLL" é a subsequência comum mais longa que tem comprimento 3.

Método de força bruta:

Podemos gerar todas as subsequências de duas strings usando retrocesso. Então podemos compará-los para descobrir o
subsequências comuns. Depois precisaremos descobrir aquele com o comprimento máximo. Já vimos isso,
existem 2n subsequências de uma string de comprimento n. Levaria anos para resolver o problema se nosso n ultrapassasse 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. Portanto, a maior subsequência comum dessas duas strings será "abcf", que tem comprimento 4.
Mais uma vez, lembro a você que as 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 (um array 2D) com 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. Vamos percorrer a mesa da esquerda para a direita
para cada linha. Nossa tabela ficará assim:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | uma | b | c | e | uma | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | uma | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Notas sobre algoritmos do GoalKicker.com para profissionais 220


Machine Translated by Google

5|f| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Aqui, cada linha e coluna representa o comprimento da maior subsequência comum entre duas strings se
pegue os caracteres dessa linha e coluna e adicione ao prefixo anterior. Por exemplo: Tabela[2][3] representa o
comprimento da maior subsequência comum entre "ac" e "abc".

A 0ª coluna representa a subsequência vazia de s1. Da mesma forma, a linha 0 representa o vazio
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 for 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. Nós temos:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | uma | b | c | e | uma | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | uma | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|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 subsequência comum mais longa 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 restante do
os valores também serão 1 para a primeira linha, pois considera apenas a string a com abcd, abcda, abcdaf. Então nossa mesa ficará
como:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | uma | b | c | e | uma | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | uma | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Para a linha 2, que agora incluirá c. Para Table[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 a entre duas substrings. E daí
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

Notas sobre algoritmos do GoalKicker.com para profissionais 221


Machine Translated by Google

LCS na parte superior ou à esquerda. Tomar o comprimento do LCS no topo denota que não consideramos a corrente
personagem de s2. Da mesma forma, tomar o comprimento do LCS à esquerda indica que não consideramos a corrente
caractere de s1 para criar o LCS. Nós temos:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | uma | b | c | e | uma | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | uma | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0|1| | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Portanto, 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

Continuando, para Table[2][2] temos as strings ab e ac. Como c e b não são iguais, colocamos o máximo do topo ou
saiu daqui. Neste caso, é novamente 1. Depois disso, para Tabela[2][3] temos as strings abc e ac. Desta vez, os valores atuais de
linha e coluna 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 da diagonal, que representa a melhor correspondência
entre ab e a. A partir deste estado, para os valores atuais, adicionamos mais um caractere a s1 e s2 que

aconteceu de ser o mesmo. Portanto, é claro que a duração do LCS aumentará. Colocaremos 1 + 1 = 2 na Tabela[2][3]. Nós temos,

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | uma | b | c | e | uma | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | uma | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0|1| 1 |2| | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|0| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Portanto, 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 sobre algoritmos do GoalKicker.com para profissionais 222


Machine Translated by Google

Definimos ambos os casos. Usando essas duas fórmulas, podemos preencher a tabela inteira. Depois de preencher o
tabela, ficará assim:

0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| chÿ | | uma | b | c | e | uma | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0| |0|0|0|0|0|0|0|
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | uma | 0 | 1 | 1 |1| 1 |1| 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2|c|0|1| 1 |2|2|2|2|
+-----+-----+-----+-----+-----+-----+-----+-----+
3|b|0|1|2|2|2|2|2|
+-----+-----+-----+-----+-----+-----+-----+-----+
4|c|0|1|2|3|3|3|3|
+-----+-----+-----+-----+-----+-----+-----+-----+
5|f|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 s1.length
Tabela[0][i] = 0
fim para
para i de 1 a s2.length
Tabela[i][0] = 0
fim para
para i de 1 a s2.length
para j de 1 a s1.length
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
Tabela de retorno [s2.length][s1.length]

A complexidade de tempo para este algoritmo é: O(mn) onde m e n denotam o comprimento de cada string.

Como descobrimos a subsequência comum mais longa? Começaremos do canto inferior direito. Iremos verificar
de onde vem o valor. Se o valor vier da diagonal, isto é, se Table[i-1][j-1] for igual a
Tabela[i][j] - 1, pressionamos s2[i] ou s1[j] (ambos são iguais) e nos movemos na diagonal. Se o valor vier de cima,
isso significa que, se Tabela[i-1][j] for igual a Tabela[i][j], passamos para o topo. Se o valor vier da esquerda, isso significa que, se
Tabela[i][j-1] é igual a Tabela[i][j], movemos para a esquerda. Quando alcançamos a coluna mais à esquerda ou superior, nossa pesquisa
termina. Em seguida, retiramos os valores da pilha e os imprimimos. O pseudocódigo:

Procedimento PrintLCS(LCSlength, s1, s2)


temp := comprimento LCS
S = pilha()
eu := s2.comprimento
j := s1.comprimento
enquanto i não é igual a 0 e j não é igual a 0
if Tabela[i-1][j-1] == Tabela[i][j] - 1 e s1[j]==s2[i]

Notas sobre algoritmos do GoalKicker.com para profissionais 223


Machine Translated by Google

S.push(s1[j]) //ou S.push(s2[i]) i := i - 1 j := j -


1 else if
Tabela[i-1][j]
== Tabela[i] [j]
eu := i-1
outro
j := j-1
endif
endwhile
enquanto S não está vazio
print(S.pop)
endwhile

Ponto a ser observado: se a Tabela[i-1][j] e a Tabela[i][j-1] forem iguais à Tabela[i][j] e a Tabela[i-1][j-1] não for igual à
Tabela[i][j] - 1, pode haver dois LCS para aquele momento. Este pseudocódigo não considera esta 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 sobre algoritmos do GoalKicker.com 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 subsequência
crescente mais longa

A subsequência crescente mais longa O problema é encontrar a 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 subsequência crescente mais longa:

Algoritmos como Subsequência Crescente Mais Longa, Subsequência Comum Mais Longa são usados em sistemas de controle de
versão como Git e etc.

Forma simples de algoritmo:

1. Encontre linhas exclusivas 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 correspondente mais longa
de linhas, uma correspondência entre as linhas de dois documentos.
4. Recurse o algoritmo em cada intervalo de linhas entre os já correspondentes.

Agora consideremos um exemplo mais simples do problema LCS. Aqui, a entrada é apenas uma sequência de inteiros distintos
a1,a2,...,an., e queremos encontrar a subsequência crescente mais longa nela. Por exemplo, se a entrada for 7,3,8,4,2,6 , a subsequência
crescente mais longa será 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ê observar o array resultante, notará que muitos valores são iguais e o array parece muito repetitivo. Isto
sugere que o problema LIS (maior subsequência crescente) pode ser resolvido com algoritmo de programação dinâmica usando apenas
array unidimensional.

Pseudo-código:

1. Descreva uma matriz de valores que queremos calcular.


Para 1 <= i <= n, seja A(i) o comprimento de uma sequência crescente de entrada mais longa. Observe que o
comprimento que nos interessa é 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-a na ordem inversa. Este programa é executado no tempo O(n), então todo o algoritmo é executado no tempo O(n^2).

Parte 1:

mÿ1
para i : 2..n
se A(i) > A(m) então

Notas sobre algoritmos do GoalKicker.com para profissionais 225


Machine Translated by Google

m ÿ eu
fim se
fim para

Parte 2:

coloque
um while A(m) > 1 do i ÿ
mÿ1

enquanto not(ai < am e A(i) = A(m)ÿ1) faça


eu ÿ euÿ1
terminar enquanto

m ÿ eu

pôr um
fim 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 = máx(m, 1 +
LIS(A[1..(n ÿ 1)], A[n]))

Saída m

PRINCIPAL(A[1..n]):
retornar 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]):
retornar LIS(A[1..i])

Notas sobre algoritmos do GoalKicker.com 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 a
n do L[i] = 1 para j = 1
a i ÿ 1 do

se (A[j] < A[i]) faça


eu[i] = máx(eu[i], 1 + eu[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 . Portanto, a subsequência crescente mais longa para a entrada
fornecida é {0, 2, 6, 9, 11, 15}.

Notas sobre algoritmos do GoalKicker.com 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 anagrama. Eu usei javascript aqui.

Criaremos um hash de str1 e aumentaremos a contagem +1. Faremos um loop na 2ª string e verificaremos se todos os caracteres estão presentes
no hash e diminuiremos o valor da chave hash. Verifique se todos os valores da chave hash são zero e serão um anagrama.

Seção 49.1: Exemplo de entrada e saída


Ex1:

deixe str1 = 'stackoverflow'; deixe


str2 = 'flowerovstack';

Essas strings são anagramas.

// Cria Hash 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 hashKey 'o' contém o valor 2 porque o é 2 vezes na string.

Agora faça um loop sobre 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 sobre o objeto hashMap e verifique se todos os valores são zero na chave do hashMap.

Notas sobre algoritmos do GoalKicker.com 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) {

if(str1.length !== str2.length){ retornar falso;

// Cria um mapa hash do caractere str1 e aumenta o valor um (+1).


createStr1HashMap(str1);

// Verifique se o caractere str2 é chave no mapa hash e diminui o valor em um (-1); var valorExist
= createStr2HashMap(str2);

// Verifique se todos os valores das chaves hashMap são zero, então será um anagrama.
return isStringsAnagram(valorExist);
}

função createStr1HashMap (str1)


{ [].map.call(str1, function(valor, índice, array){ hashMap[valor] =
valor em hashMap ? (hashMap[valor] + 1): 1; valor de retorno ;

});
}

função createStr2HashMap (str2) {


var valorExist = [].every.call(str2, function(valor, índice, array){
if(valor em hashMap)
{ hashMap[valor] = hashMap[valor] - 1;

} valor de retorno em hashMap;


});
valor de retornoExist ;
}

função isStringsAnagram (valorExist) { if(!valueExist)


{ return valorExist; }
else { var isAnagram;
for(var i
no hashMap) {

if(hashMap[i] !== 0)
{ isAnagram = false;

quebrar; }
else { isAnagram = true;
}
}

return éAnagrama;
}
}

isAnagram('stackoverflow', 'flowerovstack'); // true


isAnagram('stackoverflow', 'flowervvstack'); // falso

Notas sobre algoritmos do GoalKicker.com para profissionais 229


Machine Translated by Google

})();

Complexidade de tempo: 3n, ou seja, O(n).

Notas sobre algoritmos do GoalKicker.com 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;
for(i=1; i<=linhas; ++i) {

for(espaço=1; espaço <= linhas-i; ++espaço) {

printf(" "); +
+contar;
}

enquanto(k != 2*i-1) {

if (contagem <= linhas-1)


{
printf("%d ", i+k); +
+contar;

}
outro {
++conta1;
printf("%d ", (i+k-2*contagem1));

} ++k;

} contagem1 = contagem = k = 0;

printf("\n");
}

Saída

1232
34543
456765456
7898765

Notas sobre algoritmos do GoalKicker.com para profissionais 231


Machine Translated by Google

Capítulo 51: Algo: - Imprima a matriz am*n em quadrado

Verifique o exemplo de entrada e saída abaixo.

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:
imprima o 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 imprima o
í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


function noOfLooping(m,n) { if(m >
n) { menorValor
= n; } else { menorValor
= m;

if(menorValor % 2 == 0) { return
menorValor/2; } else { return

(menorValor+1)/2;
}
}

função squarePrint(m,n) { var


looping = noOfLooping(m,n); for(var i =
0; i < looping; i++) {
for(var j = i; j < m - 1 - i; j++) { console.log(i+''+j);

} for(var k = i; k < n - 1 - i; k++)


{ console.log(k+''+j);

} for(var l = j; l > i; l--)


{ console.log(k+''+l);

} for(var x = k; x > i; x--)


{ console.log(x+''+l);
}
}
}

quadradoPrint(6,4);

Notas sobre algoritmos do GoalKicker.com para profissionais 232


Machine Translated by Google

Capítulo 52: Exponenciação da Matriz

Seção 52.1: Exemplo de exponenciação de matriz para resolver


Problemas
Encontre f (n): enésimo número de Fibonacci. O problema é bastante 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
uma e outra vez. 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 podemos resolver 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 seu produto. 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:

Primeiramente precisamos de uma relação recursiva e queremos encontrar uma matriz M que possa nos levar ao estado desejado a partir de um

conjunto de estados já conhecidos. Vamos supor que conhecemos os k estados de uma determinada 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) |
| ...... | | ...... |
| merda (nk) | |f(nk+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 sabemos f(n) e f(n-1); Queremos descobrir f(n+1).
A partir da situação apresentada 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 todos os estados dos quais f(n+1) depende estarão presentes]
Agora, precisamos projetar uma matriz 2X2 M tal que satisfaça MXA = B conforme declarado acima.
O primeiro elemento de B é f(n+1) que na verdade é f(n) + f(n-1). Para conseguir isso, da matriz A, precisamos de 1 X f (n) e 1
Xf(n-1). Portanto, a primeira linha de M será [1 1].

| 11 | | X | f(n) | = | f(n+1) |
----- | | f(n-1) | | ------ |

Notas sobre algoritmos do GoalKicker.com para profissionais 233


Machine Translated by Google

[Nota: ----- significa que não estamos preocupados com este valor.]
Da mesma forma, o segundo item de B é f(n) , que pode ser obtido simplesmente pegando 1 X f(n) de A, então a segunda linha de M é [1 0].

| ----- | X | f(n) | = | ------ |


|1 0| | f(n-1) | | f(n) |

Então obtemos nossa matriz 2 X 2 M desejada.

|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 complexo: encontre f(n) = a X f(n-1) + b X f(n-2), onde a e b são constantes.
Isso nos diz, f(n+1) = a X f(n) + b X f(n-1).
Até aqui, deve ficar claro que a dimensão das matrizes será igual ao número de dependências, ou seja
neste exemplo específico, novamente 2. Portanto, para A e B, podemos construir duas matrizes de tamanho 2 X 1:

Matriz A | Matriz B
f(n) | | 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 objetiva M. E para a segunda
item em B, ou seja, f(n) já temos isso na matriz A, então pegamos apenas aquele, que leva, a 2ª linha da matriz M
para [1 0]. Desta vez obtemos:

| b | X | f(n) | = | f(n+1) |
uma | 1 0| | f(n-1) | | f(n) |

Muito simples, hein?

Tipo 3:

Se você sobreviveu até esse estágio, ficou muito mais velho, agora vamos encarar uma relação um pouco complexa: encontre f(n) =
aXf (n-1) + cXf (n-3)?
Ops! Há alguns minutos, tudo o que vimos foram estados contíguos, mas aqui falta o estado f(n-2) . 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 esta é na verdade uma forma
descrito no Tipo 2. Portanto, aqui a matriz objetiva M será 3 X 3 e os elementos são:

|a0c| | f(n) | | f(n+1) |


| 1 0 0 | X | f(n-1) | = | f(n) |
| 0 1 0 | | f(n-2) | | f(n-1) |

São calculados da mesma forma que o tipo 2, se achar difícil experimente com caneta e papel.

Tipo 4:

A vida está ficando complexa como o inferno, e o Sr., o Problema agora pede que você encontre f(n) = f(n-1) + f(n-2) + c onde c é qualquer
constante.

Agora, este é novo e tudo o que vimos no passado, após a multiplicação, cada estado em A se transforma no próximo

Notas sobre algoritmos do GoalKicker.com 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 resumir: 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 corresponde à sua solução. Encontre também a matriz A e
B.

| a 0 cd 1 |
|10000|
|01000|
|00100|
|00001|

Tipo 6:

Às vezes a recorrência é dada assim:

f(n) = f(n-1) -> se n é í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 pares ímpares e manter 2 matrizes diferentes para ambas e calcular
eles separadamente.

Tipo 7:

Sentindo-se um pouco confiante demais? Bom para você. Às vezes podemos precisar manter mais de uma recorrência, onde
Eles estão interessados. Por exemplo, seja uma recorrência re;atopm:

g(n) = 2g(n-1) + 2g(n-2) + f(n)

Aqui, a recorrência g(n) depende de f(n) e isso pode ser calculado na mesma matriz, mas com aumento
dimensões. A partir delas, vamos primeiro projetar as matrizes A e B.

Notas sobre algoritmos do GoalKicker.com para profissionais 235


Machine Translated by Google

Matriz A | Matriz B
g(n) | | g(n-1) | g(n+1) |
| | f(n+1) | | | g(n) |
f(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 indicados acima,
podemos encontrar a matriz objetiva M como sendo:

|2210|
|1000|
|0022|
|0010|

Portanto, essas são as categorias básicas de relações de recorrência que são usadas para resolver por meio desta técnica simples.

Notas sobre algoritmos do GoalKicker.com 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 mínimo de cobertura de vértices de saída C

Definir C <- new Set<Vertex>()

Definir X <- novo Conjunto<Vértice>()

X <- G.getAllVerticiesArrangedDescendenteByDegree()

para v em X faça
Lista<Vértice> adjacenteVertices1 <- G.getAdjacent(v)

se !C contiver algum dos adjacenteVertices1 então

C.adicionar(v)

para vértice em C faça

Lista<vértice> adjacenteVertices2 <- G.adjacentVertecies(vértice)

se C contém algum dos adjacenteVertices2 então

C.remove(vértice)

retornar C

C é a cobertura mínima de vértices do gráfico G

podemos usar a classificação por balde para classificar os vértices de acordo com seu grau porque o valor máximo dos graus é (n-1), onde
n é o número de vértices, então a complexidade de tempo da classificação será O (n)

Notas sobre algoritmos do GoalKicker.com para profissionais 237


Machine Translated by Google

Capítulo 54: Distorção Dinâmica do Tempo

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


Distorção dinâmica do tempo (DTW) é um algoritmo para medir a similaridade entre duas sequências temporais que podem
variar em velocidade. Por exemplo, semelhanças na caminhada poderiam ser detectadas usando DTW, mesmo que uma pessoa estivesse andando
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 exemplo de comando de voz com outro comando, 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 - de fato,
quaisquer dados que possam ser transformados em uma sequência linear podem ser analisados com DTW.

Em geral, DTW é um método que calcula uma correspondência ótima entre duas sequências dadas 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 refere-se 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 assumir:

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.

A princípio, definimos a distância entre dois pontos, d(x, y) onde xey representam os dois pontos. Deixar,

d(x, y) = |x - y| // diferença absoluta

Vamos criar uma tabela matricial 2D usando essas duas sequências. Calcularemos as distâncias entre cada ponto de

Faça uma amostra de 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 ideal entre duas sequências se considerarmos a sequência até

Amostra[i] e Teste[j], considerando todas as distâncias ótimas que observamos anteriormente.

Para a primeira linha, se não tomarmos valores de Amostra, a distância entre esta e Teste será infinita. Então colocamos

infinito na primeira linha. O mesmo vale para a primeira coluna. Se não tomarmos valores de Teste, a distância entre este

um e Amostra também serão infinitos. E a distância entre 0 e 0 será simplesmente 0. Obtemos,

Notas sobre algoritmos do GoalKicker.com 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 somaremos com o mínimo
distância que encontramos até agora. Isso nos dará a distância ideal 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. Portanto, o valor de Table[1][1] será 0 + 0 = 0. Para
o segundo, d(1, 2) = 0. Tabela[1][1] representa o mínimo. O valor será: Tabela[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 | informações | 15 | 15 | 10 | 10 | 6 | 0 |
+------+------+------+------+------+------+------+ ------+
| 6 | informações | 20 | 20 | 14 | 14 | 9 | 1|
+------+------+------+------+------+------+------+ ------+

O valor na Tabela[7][6] representa a distância máxima entre essas duas sequências fornecidas. Aqui 1 representa
a distância máxima entre a amostra e o teste é 1.

Agora, se voltarmos do último ponto, até o ponto inicial (0, 0) , obteremos uma longa linha que
move-se horizontalmente, verticalmente e diagonalmente. Nosso procedimento de retrocesso será:

if Tabela[i-1][j-1] <= Tabela[i-1][j] e Tabela[i-1][j-1] <= Tabela[i][j-1]

Notas sobre algoritmos do GoalKicker.com 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 isso 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 testes acelerou durante esse intervalo.
Um movimento vertical representa a inserção. Isso significa que a sequência de testes foi desacelerada durante esse intervalo.
Um movimento diagonal representa a partida. Durante este período, o Teste e a Amostra foram iguais.

Nosso pseudocódigo será:

Procedimento DTW (Amostra, Teste): n :=


Sample.length m :=
Test.length 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
Tabela de retorno [n + 1][m + 1]

Também podemos adicionar uma restrição de localidade. Ou seja, exigimos que se Sample[i] corresponder a Test[j], então |i - j| não é maior que w, um
parâmetro de janela.

Notas sobre algoritmos do GoalKicker.com para profissionais 240


Machine Translated by Google

Complexidade:

*
A complexidade da computação DTW é O(m). n) onde m e n representam o comprimento de cada sequência. Mais rápido
As técnicas para computação DTW incluem PrunedDTW, SparseDTW e FastDTW.

Formulários:

Reconhecimento de palavras faladas

Análise de poder de correlação

Notas sobre algoritmos do GoalKicker.com para profissionais 241


Machine Translated by Google

Capítulo 55: Transformada Rápida de Fourier

A forma Real e Complexa de DFT (Transformadas Discretas de Fourier ) pode ser usada para realizar análise ou síntese de
frequência 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: Base 2 FFT

O método mais simples e talvez mais conhecido para calcular a FFT é o algoritmo Radix-2 Decimation in Time.
O Radix-2 FFT 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 da matriz de dados no 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
por 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, esta matriz de pontos
únicos decompostos no domínio do tempo não requer transformação para se tornar uma matriz de pontos no domínio da
frequência. Os N pontos únicos; no entanto, precisa ser reconstruído em um espectro de frequência de N pontos. A reconstrução
ideal do espectro de frequência completo é realizada usando cálculos borboleta. Cada estágio de reconstrução no Radix-2 FFT executa
uma série de borboletas de dois pontos, usando um conjunto semelhante de funções de ponderação exponencial, Wn^R.

Notas sobre algoritmos do GoalKicker.com 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 nos estágios log2(N) dos cálculos borboleta, fornecendo X[K]; os dados do domínio da frequência
real e imaginário em forma retangular. Para converter em 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 um Radix 2 FFT 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 sobre algoritmos do GoalKicker.com para profissionais 243


Machine Translated by Google

A FFT normalmente opera com 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], porém muitas otimizações são possíveis envolvendo a
transformação de dados apenas reais. Os valores de Wn^R usados ao longo da 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 da corrente
dentro de uma borboleta específica.

Exemplo de código (C/C++)

Um exemplo de código AC/C++ para calcular o Radix 2 FFT 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 lenta que a implementação FFTw mais
rápida, mas ainda é uma base muito boa para otimização futura ou para aprender como esse algoritmo funciona.

#include <matemática.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.32192
80948873623478703194294948 //1/Log10(2)

// estrutura variável complexa (precisão dupla) struct complex { public:


double Re, Im;

// Não é tão complicado afinal


};

// 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 sobre algoritmos do GoalKicker.com para profissionais 244


Machine Translated by Google

if ((NN != N) || (NN == 0)) // Verifique se N é uma potência de 2.


retorna falso;

retornar verdadeiro;
}

void rad2FFT(int N, complexo *x, complexo *DFT)


{
interno M = 0;

// Verifica se é potência de dois. Caso contrário, 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


Blargura; interno // BWidth é o espaçamento de memória das extremidades opostas da borboleta
P; intj ; // P é o número de Wn's semelhantes a serem usados naquela etapa
estágio // j é usado em um loop para realizar todos os cálculos em cada estágio
interno = 1; (1 a M). // estágio é o número do estágio da FFT. Existem M estágios no total

int HiIndex; // HiIndex é o índice do array DFT para o valor superior de cada
cálculo de borboleta
unsigned int iaddr; intii ; // máscara de bits para reversão de bits
interno // Campo de bits inteiro para reversão de bits (Decimation in Time)
MM1 = M - 1;

não assinado int i;


você é o;
int não assinado nMax = ( int não assinado)N;

// Variáveis de Precisão Dupla


duplo DoisPi_N = DOISPI / (duplo)N; duplo DoisPi_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 complexa; // TEMP é usado para salvar o cálculo no cálculo borboleta
complexo *pDFT = DFT; // Ponteiro para os primeiros elementos do array DFT
complexo *pLo; // Ponteiro para valor lo/hi dos cálculos borboleta
complexo *pHi;
complexo *pX; // Ponteiro para x[n]

// Dizimação em 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 = eu; // Copie i para manipulações
for (l = 0; l < M; l++) // Bit inverte i e armazena em ii...
{
if (iaddr & 0x01) ii += (1 //Determina o bit menos significativo
<< (MM1 - l)); // Incrementa ii em 2^(M-1-l) se lsb fosse 1
iaddr >>= 1; // shift para a direita iaddr para testar o próximo bit. Usar lógica
operações para aumento de velocidade
se (!iaddr)
quebrar;
}
DFT = pDFT + ii; índice // Calcula o DFT[n] atual a partir do endereço base *pDFT e bit
revertido ii

Notas sobre algoritmos do GoalKicker.com para profissionais 245


Machine Translated by Google

DFT->Re = pX->Re; //Atualiza o array complexo com sinal de domínio de tempo classificado por endereço
x[n]
DFT->Im = pX->Im; // NB: Imaginário é sempre zero
}

// Computação FFT por cálculo borboleta


for (stage = 1; stage <= M; stage++) // Loop para M estágios, onde 2^M = N
{
BSep = (int)(pow(2, estágio)); //Separação entre borboletas = 2^estágio
P = N / BSep; // Wn's semelhantes nesta etapa = N/Bsep
BLargura = BSep / 2; // Largura da borboleta (espaçamento entre pontos opostos) = Separação /
2.

DoisPi_NP = DoisPi_N*P;

for (j = 0; j < BWidth; j++) // Loop para cálculos de j por borboleta


{
se (j ! = 0) { // Economize no cálculo se R = 0, pois WN^0 = (1 + j0)

//WN.Re = cos(TwoPi_NP*j)
WN.Re = cos(DoisPi_N*P*j); // Calcula Wn (Real e Imaginário)
WN.Im = -sin(DoisPi_N*P*j);
}

for (HiIndex = j; HiIndex < N; HiIndex += BSep) // Loop para HiIndex Step BSep
borboletas por fase
{
pHi = pDFT + HiIndex; pLo // Aponta para valor maior
= pHi + BLargura; para // Aponta para valor inferior (Nota VC++ ajusta
espaçamento entre elementos)

se (j ! = 0) { // Se a potência exponencial não for zero...

//CMult(pLo, &WN, &TEMP); // Realiza multiplicação complexa de Lovalue


com Wn
TEMP.Re = (pLo->Re * WN.Re) - (pLo->Im * WN.Im);
TEMP.Im = (pLo->Re * WN.Im) + (pLo->Im * WN.Re);

//CSub (pHi, &TEMP, pLo);


pLo->Re = pHi->Re - TEMP.Re; pLo- //Encontre o novo Lovalue (subtração complexa)
>Im = pHi->Im - TEMP.Im;

//CAdd(pHi, &TEMP, pHi); pHi- // Encontra novo Hivalue (adição complexa)


>Re = (pHi->Re + TEMP.Re);
pHi->Im = (pHi->Im + TEMP.Im);
}
outro
{
TEMP.Re = pLo->Re;
TEMP.Im = pLo->Im;

//CSub (pHi, &TEMP, pLo);


pLo->Re = pHi->Re - TEMP.Re; pLo- //Encontre o novo Lovalue (subtração complexa)
>Im = pHi->Im - TEMP.Im;

//CAdd(pHi, &TEMP, pHi); pHi- // Encontra novo Hivalue (adição complexa)


>Re = (pHi->Re + TEMP.Re);
pHi->Im = (pHi->Im + TEMP.Im);
}
}
}
}

Notas sobre algoritmos do GoalKicker.com para profissionais 246


Machine Translated by Google

pLo = 0; // Nula todos os ponteiros


pHi = 0;
PDFT = 0;
DFT = 0;
pX = 0;
}

Seção 55.2: FFT Inversa da Radix 2

Devido à forte dualidade da Transformada de Fourier, ajustar a saída de uma transformada direta pode produzir a FFT inversa. Os dados
no domínio da frequência podem ser convertidos para o domínio do tempo pelo seguinte método:

1. Encontre o conjugado complexo dos dados no domínio da frequência invertendo a componente imaginária para todos
instâncias de K.

2. Execute a FFT direta nos dados conjugados no domínio da frequência.


3. Divida cada saída do resultado desta FFT por N para obter o valor verdadeiro no domínio do tempo.
4. Encontre o conjugado complexo da saída invertendo o componente imaginário dos dados no domínio do tempo para
todas as instâncias de n.

Nota: os dados no domínio da frequência e do tempo são variáveis complexas. Normalmente, o componente imaginário do sinal no
domínio do 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 long double de 128 bits reduz significativamente os erros de arredondamento produzidos por
várias operações FFT consecutivas.

Exemplo de código (C/C++)

#include <matemática.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.32192
80948873623478703194294948 //1/Log10(2)

// estrutura variável complexa (precisão dupla) struct complex { public:


double Re, Im;

// Não é tão complicado afinal


};

void rad2InverseFFT(int N, complexo *x, complexo *DFT) {

// M é o número de etapas a serem executadas. 2^M = N duplo


Mx = (log10((duplo)N) / log10((duplo)2)); int a = (int)(ceil(pow(2.0, Mx)));
estado interno = 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 Radix 2 Inverse FFT";
}

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

247
Notas sobre algoritmos do GoalKicker.com para profissionais
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 Freq


rad2FFT(N, DFT, x); // Calcula a FFT direta com variáveis comutadas (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 escalar amplitude correta
>Im *= -1; //Muda o sinal do ImX
}
}

Notas sobre algoritmos do GoalKicker.com para profissionais 248


Machine Translated by Google

Apêndice A: Pseudocódigo
Seção A.1: Aectações variáveis

Você poderia descrever a afetação variável de diferentes maneiras.

Digitado
intuma = 1 _
int a := 1 deixe
int a = 1
interno um <- 1

Nenhum tipo

a=1
a := 1
seja a = 1
um <- 1

Seção A.2: Funções

Contanto que o nome da função, a instrução de retorno e os parâmetros estejam claros, você está bem.

def incremento substantivo, masculino

retornar n + 1

ou

deixe incr (n) = n + 1

ou

função incr (n) retornar n


+1

são todos bastante claros, então você pode usá-los. Tente não ser ambíguo com uma afetação variável

Notas sobre algoritmos do GoalKicker.com para profissionais 249


Machine Translated by Google

Créditos
Muito obrigado a todas as pessoas da Stack Overflow Documentation que ajudaram a fornecer este conteúdo,
mais alterações podem ser enviadas para web@petercv.com para que novo conteúdo seja publicado ou atualizado

Abdul Karim Capítulo 1


afeldspato Capítulo 43
Ahmed Faiyaz Capítulo 28
Alberto Tadrous Capítulo 53
Anagh Hegde Capítulos 29 e 39
Andriy Artamonov Capítulo 27
AnukuL 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
bris 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 Capítulo 21
Dr. ABT Capítulo 55
EsmaeelE Capítulos 2, 29, 30, 39 e 50
Filip Allberg Capítulos 1 e 9
ghilesZ Capítulo 17
Goeddek Capítulos 18 e 27
grande lobo capítulo 5
Ijaz Khan Capítulo 29
invisível Capítulo 31
Isha Agarwal Capítulos 4, 5, 6, 7 e 8
Você era Mehta capítulo 5
IVad Capítulos 16 e 28
Deixar 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
Luv Agarwal Capítulo 30
Linfato Capítulo 31
MS Hossain Capítulo 17
Malavo Capítulo 33
Malcolm McLean Capítulos 4 e 39
Martin 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 sobre algoritmos do GoalKicker.com para profissionais 250


Machine Translated by Google

Nick, o codificador Capítulo 3


um otimista Capítulos 29 e 33
Pedro K. Capítulo 2
Rashik Hasnat Capítulo 40
Roberto Fernández Capítulo 12
Samgak Capítulo 29
Samuel Pedro Capítulo 3
Santiago Gil Capítulo 30
Sayakiss Capítulos 9 e 14
SHARMA Capítulo 30
ShreePool Capítulo 39
Dúvida Capítulo 16
Sumeet Singh Capítulos 20 e 41
Muitas riquezas 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
WitVault Capítulo 3
xenteros Capítulos 17, 29 e 39
Yair Twito Capítulo 2
rendimento1 Capítulo 4
enquanto come
Capítulos 16 e 20
JovemHobbit Capítulo 29

Notas sobre algoritmos do GoalKicker.com para profissionais 251


Machine Translated by Google

você pode gostar

Você também pode gostar