Algorithms Notes For Professionals - Traduzido
Algorithms Notes For Professionals - Traduzido
Algoritmos
Conteúdo
Sobre .................................................. .................................................. .................................................. ............................. 1
2.2: Comparação das notações assintóticas Seção 2.3: Notação .................................................. ........................................... 6
Seção 4.3: Para verificar se duas árvores binárias são iguais ou não .................................................. ................................... 15
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
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
Seção 9.5: Detectando um ciclo em um gráfico direcionado usando Depth First Traversal Seção .................................................. 40
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
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
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
Merge Sort Seção 30.2: Implementação de Merge Sort .................................................. .................................................. 150
Seção 30.5: Implementação de Merge Sort em Python .......... .................................................. .................................. 153
Seção 30.6: Implementação Java de baixo para cima .......................................... .................................................. ....... 154
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 39.3: Análise de pesquisa linear (piores, médios e melhores casos) ................................... ..................... 176
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 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
Seção 47.1: Explicação da Subsequência Comum Mais Longa ........................................... ........................................ 220
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
Capítulo 51: Algo: - Imprima a matriz am*n em quadrado .................................................. ........................... 232
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
Sobre
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
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 }.
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?
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:
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
}
}
Ó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.
}
}
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
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!
}
}
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.
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
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.
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!
ÿ(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:
A definição é igual a:
se limite(x->infinito) f(x)/g(x) = c ÿ (0,ÿ) ou seja, o limite existe e é positivo, então f(x) = ÿ(g(x))
Linear 10 100
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
Ligações
Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introdução aos Algoritmos.
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:
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)).
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)).
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".
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.
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.
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.
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.
} retornar máximo;
}
Essas duas atribuições são feitas apenas uma vez, ou seja, 2 operações. As operações em loop são:
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!
}
}
} 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 .
}
}
} 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.
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.
tamanho do problema = 1
2. Mas sabemos que na k-ésima etapa, o tamanho do nosso problema deve ser:
3. De 1 e 2:
n/2k = 1 ou
n = 2k
loge n = k loge2
ou
k = loge n / loge 2
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)
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)
k = log2 256
k=8
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; }
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
uma = 0
b = n-1
enquanto Verdadeiro:
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".
Conclusão
Ao se deparar com divisões sucessivas (seja por dois ou por qualquer número), lembre-se que a complexidade é logarítmica.
Capítulo 4: Árvores
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;
}
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;
}
}
}
printree_r(raiz, 0);
}
Árvores são um subtipo da estrutura de dados gráfica mais geral da borda do nó.
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.
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)
b)
Exemplo:2
Se as entradas forem:
a)
b)
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
root.r_child = nó
outro:
inserir (root.r_child, nó)
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.
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.
nó de estrutura
{
dados
internos ; nó *esquerda, *direita;
};
outro
{
if(root->left == nullptr && root->right == nullptr) // Caso 1 {
grátis(raiz);
raiz = nullptr;
} // Caso 3
outro {
nó* temp = root->direita;
root->dados = temp->dados;
root->direita = delete_node(root->direita, temp->dados);
}
} retornar raiz;
}
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;
retornar raiz;
}
else
{ return lowerCommonAncestor(root->right, node1, node2);
}
}
self.insert(root.l_child, node)
raiz de retorno
retornar Nenhum
outro:
self.in_order_place(root.l_child) imprimir root.val
self.in_order_place(root.r_child)
imprimir root.val
self.pre_order_place(root.l_child)
self.pre_order_place(root.r_child)
retornar Nenhum
outro:
self.post_order_place(root.l_child)
self.post_order_place(root.r_child) imprimir root.val
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))
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) .
is_BST(raiz): se
raiz == NULO:
retornar verdadeiro
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(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
Por exemplo
se a entrada for:
Se a entrada for:
1234567
Código:
#include<iostream>
#include<queue>
#include<malloc.h>
nó de estrutura{
dados
internos ; nó
*esquerdo; nó *direita;
};
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();
retorno(nó);
}
int principal(){
retornar 0;
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ó.
1245367
A travessia em ordem (raiz) está percorrendo a subárvore esquerda do nó, depois o nó e depois a subárvore direita do
nó.
4251637
A travessia pós-ordem (raiz) atravessa a subárvore esquerda do nó, depois a subárvore direita e depois o nó.
4526731
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}.
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
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
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.
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.
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.
importar java.util.Scanner;
vértices = v;
matriz_adjacência = novo int[vértices + 1][vértices + 1];
}
tentar
matriz_adjacência[to][de] = borda;
tentar
retornar matriz_adjacência[para][de];
} retornar -1;
}
para = sc.nextInt(); de
= sc.nextInt();
} catch (Exceção E) {
sc.close();
}
}
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
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.
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
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.
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.
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.
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
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.
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
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á:
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:
A partir deste, podemos descobrir facilmente o número total de nós conectados a qualquer nó e quais são esses nós.
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.
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 é:
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.
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:
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));
Implementação C++:
#include <iostream>
#include <lista>
#define NUM_V 4
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. */
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;
} retorna falso;
} /*
Função de motorista
*/
int principal() {
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.
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
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).
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.
retornar;
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,
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ó
na fila e em vez de atualizá-lo com visitado, relaxamos ou atualizamos a nova borda. Vejamos um exemplo:
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:
terminar enquanto
Distância de retorno
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.
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 {
min = dist[v];
índice_mín = v;
}
retornar índice_minuto;
}
dist[i] = Inteiro.MAX_VALUE;
sptSet[i] = falso;
}
dist[src] = 0;
sptSet[u] = verdadeiro;
printSolução(dist, V);
}
t.dijkstra(gráfico, 0);
}
}
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.
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*-
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:
Para escolher para qual quadrado passar, precisamos levar em consideração 2 heurísticas:
Para calcular essas heurísticas, esta é a fórmula que usaremos: distância = abs(from.x - to.x) + abs(from.y - to.y)
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
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”:
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,
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:
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:
Finalmente, podemos ver que temos uma casa vencedora ao nosso lado, então avançamos para lá e terminamos.
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
Estado final:
123
456
78 _
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
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
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 +
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.
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.
13 _ 123
(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:
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.
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.
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
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:
Para calcular essas heurísticas, esta é a fórmula que usaremos: distância = abs(from.x - to.x) + abs(from.y - to.y)
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
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”:
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,
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:
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:
Finalmente, podemos ver que temos uma casa vencedora ao nosso lado, então avançamos para lá e terminamos.
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
} retornar dp[str1.length()][str2.length()];
}
Saída
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?
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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
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 é:
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 |
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 |
+--------------+---------+---------+--- ------+---------+---------+------------+
Se iterarmos pelo array Acc_Prof, podemos descobrir que o lucro máximo é 17! O pseudocódigo:
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:
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.
Exemplo
Implementação em Java
// Função recursiva
public int lcs(String str1, String str2, int m, int n){
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
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) / \
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
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)
Exemplos
Implementação em Java
}
senão arr[i][j] = 0;
}
} retornar máximo;
}
Complexidade de tempo
O(m*n)
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.
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
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)
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]
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
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
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
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.
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;
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.
se vRoot == uRoot:
retornar
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:
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
uRoot.parent = vRoot
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.
retornar MST
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.
+-------------+-----+-----+-----+-----+ -----+-----+
| 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?
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:
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
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
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.
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
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
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
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.
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
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
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 .
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.
em
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).
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.
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 :
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 :
Autoteste: Faça o exemplo para LIFO, LFU, RFU e veja o que aconteceu.
O esqueleto é uma aplicação que resolve o problema dependendo da estratégia gananciosa escolhida:
#include <iostream>
#incluir <memória>
// 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;
// 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 };
//redefinir cache
for (int i=0; i < cacheSize; ++i) cache[i] = originalCache[i];
int cntMisses = 0;
" "
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;
}
A ideia básica é simples: para cada solicitação tenho duas chamadas e duas da minha estratégia:
FIFO
FIFO() : Estratégia("FIFO") {
if(!cacheMiss)
retornar;
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
LIFO
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
LRU
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
Estratégia: LRU
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
LFU
retorne menos;
}
outro
++requestFrequency[cachePos];
}
privado:
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
Estratégia: LFU
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
LFD
privado:
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
Estratégia: LFD
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
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.
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.
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.
#include <iostream>
#include <vetor>
#include <string>
#include <algoritmo>
int principal()
{
std::vector<unsigned int> coinValues; // Array de valores de moedas crescentes int ticketPrice;
// M no exemplo
intpaidMoney ; //P no exemplo
diffValue -= *coinValue;
contarMoedas++;
}
coinCount.push_back(contarMoedas);
}
retornar 0;
}
// valores da moeda
std::vector<unsigned int> coinValues;
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>());
// imprime o array
cout << "Valores da moeda: ";
Esteja ciente de que agora há verificação de entrada para manter o exemplo simples. Um exemplo de saída:
Contanto que 1 esteja nos valores da moeda, agora o algoritmo será encerrado, porque:
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.
O objetivo é encontrar o subconjunto máximo de empregos mutuamente compatíveis. Existem várias abordagens gananciosas para
este problema:
A questão agora é: qual abordagem é realmente bem-sucedida? Hora de início antecipado definitivamente não, aqui está um contra-exemplo
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:
para j=1 a n se j for compatível com todos os trabalhos em A , conjunto A=A+ {j}
#include <iostream>
#include <utilitário>
#include <tupla>
#include <vetor>
#include <algoritmo>
// 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};
int principal()
{
vetor<par<int,int>> trabalhos;
// etapa 1: ordenar
sort(jobs.begin(), jobs.end(),[](pair<int,int> p1, pair<int,int> p2) { return p1.second <
p2.second; } );
// etapa 3:
for(int i=0; i<jobCnt; ++i) {
trabalho automático =
empregos[i]; bool isCompatível = verdadeiro;
{
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 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?
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.
importar java.util.Arrays;
importar java.util.Comparator;
classe Trabalho
{
int início, fim, lucro;
isto.start = início;
this.finish = terminar;
this.profit = lucro;
}
}
int lo = 0, hi = índice - 1;
retorne no meio;
}
else oi = meio - 1;
}
retornar -1;
}
int n =
trabalhos.comprimento; tabela
int [] = novo int[n]; tabela[0] = empregos[0].lucro;
"
System.out.println(" O lucro ideal é + agenda(trabalhos));
}
}
E a saída esperada é:
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
É 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
12
tj 1 5
DJ 3 5
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.
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:
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 é
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
Se continuarmos assim, selecionaremos as arestas 8-6, 6-7 e 4-3. Nosso subgráfico ficará assim:
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.
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
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).
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.
importar java.util.*;
interno Nnós;
Gráfico(int[][] mat) {
int eu, j;
NNodes = mat.comprimento;
LinkCost = novo int[NNodes][NNodes]; for ( i=0;
i < NNodes; i++) {
LinkCost [ i ][ j ]= mat [ i ][ j ]; if
( LinkCost [ i ] [ j ] == 0 )
LinkCost [ i ][ j ]= infinito ;
}
booleano concluído =
verdadeiro ; for ( int i = 0 ; i < r.length ; i++) if ( r
[ i ] == false ) return i ;
retorno - 1 ;
} 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++)
} int [ ] a = predNode ;
for ( i = 0 ; i < NNodes ; i++ )
" --> "
Sistema .out .println ( a [ i ] + + eu );
{
Sistema .out.print ("ReachSet=" ) ; for (int
i = 0 ; i < Reached.length ; i++) if ( Reached [ i ])
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
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
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 é:
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.
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.
+--------+--------+--------+--------+--------+---- ----+--------+
| 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:
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:
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
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
fim para
Retorno d
Para acompanhar o ciclo negativo, podemos modificar nosso código usando o procedimento descrito aqui. Nosso pseudocódigo
completo será:
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.
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 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.
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:
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.
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
+--------+--------+--------+--------+
| Série | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borda | 3->4 | 2->3 | 1->2 |
+--------+--------+--------+--------+
Podemos ver que nosso processo de relaxamento apenas mudou d[2]. Nosso gráfico ficará assim:
Segunda iteração:
Desta vez o processo de relaxamento mudou d[3]. Nosso gráfico ficará assim:
Terceira iteração:
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,
+--------+--------+--------+--------+
| Série | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Borda | 1->2 | 2->3 | 3->4 |
+--------+--------+--------+--------+
Teríamos:
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.
3. Calcule
Delx =| x2 – x1 |
Diariamente = | y2 – y1 |
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:
int main()
{ int gdriver=DETECTAR,gmode;
int x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\\BGI");
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; }
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)
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:
{ 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+
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.
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,
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:
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á:
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á:
s = Pilha()
S.push(destino)
enquanto Caminho[fonte][destino] não é igual à origem
S.push(Caminho[fonte][destino]) destino :=
Caminho[fonte][destino] fim enquanto
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:
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?'
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.
Complexidade de tempo: O (n ^ 2)
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
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
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.
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)
p-merge(T,p1,r1,p2,r2,A,p3) n1 = r1-
p1+1 n2 = r2-
p2+1 se n1<n2
// verifica se n1>=n2
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
retornar _
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
lps[0] = 0; int
j = 0; for(int
i =1;i<str.comprimento;i++){ if(str[j] ==
str[i]){ lps[i] = j+1; j++;
eu+
+; }
else{ if(j!=0)
{ j = lps[j-1]; }
retornar lps;
}
else{ if(j!=0){ j
= lps[j-1]; }
else{ i+
+;
}
}
}
if(j==pat.length)
retorna
verdadeiro; retorna falso;
}
1. Inserir
2. Remover
3. Substitua
Por exemplo
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].
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
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.
Implementação em Java
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 +
} retornar dp[str1.length()][str2.length()];
}
Complexidade de tempo
O(n^2)
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(ÿ) ÿ r ÿ OPT(&sigma)
para todas as instâncias ÿÿÿÿ então A é chamado de algoritmo online estritamente r-competitivo .
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.
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
Se não houver uma constante r para a qual um algoritmo online A seja r-competitivo, chamamos A de não competitivo.
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
Leitura adicional
Código fonte
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.
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.
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 código-fonte completo está disponível aqui. Se reutilizarmos o exemplo do tópico, obteremos a seguinte saída:
Estratégia: FWF
c c X X x
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
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
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.
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.
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
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
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.
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.
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).
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.
A classificação instável pode gerar a mesma saída que a classificação estável, mas nem sempre.
Mesclar classificação
Classificação de inserção
raiz da sorte
Tim Classificar
Tipo de bolha
Classificação de pilha
Ordenação rápida
Parâmetro Descrição
Estábulo Sim
No lugar Sim
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:
void bubbleSort(vetor<int>números) {
}
}
}
}
Implementação C
longo c, d, t;
/ * Trocando */
t = lista[d];
lista[d] = lista[d+1]; lista[d+1]
= t;
}
}
}
}
longo c, d, t;
/ * Trocando */
t = * (lista + d ); * (lista
+ d ) = * (lista + d + 1 ); * (lista + d + 1) = t;
}
}
}
}
lista a ser classificada, compara cada par de itens adjacentes e os troca se estiverem na ordem errada.
SortBubble(entrada);
entrada de retorno ;
}
}
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
} printNumbers(matriz);
}
}
temperatura
interna ; temp =
matriz[i]; matriz[i] =
matriz[j]; matriz[j] = temp;
}
}
}
} } while (trocado);
}
var a = [3, 203, 34, 746, 200, 984, 198, 764, 9];
bolhaSort(a);
console.log(a); //registros [3, 9, 34, 198, 200, 203, 746, 764, 984]
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:
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.
Estável: Sim
pacote principal
importar "fmt"
} m := (len(a)) / 2
f := mesclarSort(a[:m]) s :=
mesclarSort(a[m:])
eu++
}
}
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))
}
Classificação de mesclagem C
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 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#
int eu, j;
var n1 = m - l + 1; var n2
= r - m;
eu = 0;
j = 0;
var k = eu;
entrada[k] = esquerda[i];
eu++;
}
outro {
entrada[k] = direita[j]; j++;
} k++;
}
}
}
se (eu < r) {
int m = l + (r - l) / 2;
SortMerge(entrada, l, m);
SortMerge(entrada, m + 1, r);
Mesclar(entrada, l, m, r);
}
}
Abaixo está a implementação em Java usando uma abordagem genérica. É o mesmo algoritmo apresentado acima.
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);
}
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++) {
}
} else if (j > alto && i <= médio) { b[k] = a[i++]; }
else if (i > médio
&& j <= alto) { b[k] = a[j++];
}
}
}}}
out.append(Y[p2]) p2 += 1
out +=
X[p1:] + Y[p2:]
se __nome__ == "__main__":
# Gere 20 números aleatórios e classifique-os
A = [randint(1, 100) para i em xrange(20)] imprimir mergeSort(A)
public MergeSortBU() { }
private static void merge(Comparable[] arrayToSort, Comparable[] aux, int lo,int mid, int hi) {
}
}
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);
}
}
}
System.out.println(buffer);
}
bucket[i - minValue].Add(i);
}
if (b.Contagem > 0)
{
foreach (int t em b) {
entrada[k] = t; k+
+;
}
}
}
}
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.
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ô.
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.
Partição:
eu := eu + 1
fazer:
j := j - 1
enquanto A[j] > pivô do
se eu >= j então
retorne j
if(baixo<alto) {
retornar eu;
}
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.
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
para x na entrada:
saída[contagem[chave(x)]] = x
contagem[chave(x)] +=
1 saída de retorno
if (maior ! = i) {
Heapify(entrada, n, i);
SortHeap(entrada, entrada.Comprimento);
entrada de retorno ;
}
}
heapify(a,count) end
<- count - 1 while end
-> 0 do swap(a[end],a[0])
end<-end-1 restore(a,
0, end)
retornar _
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.
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]
Implementação:
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 ;
}
}
SortOddEven(entrada, entrada.Comprimento);
entrada de retorno ;
}
}
defp min([primeiro|[segundo|cauda]]) do
min([menor(primeiro, segundo)|cauda]) fim
Seleção.sort([100,4,10,6,9,3])
|> IO.inspect
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 .
Complexidade de tempo: O (n ^ 2)
entrada[minId] = entrada[i];
entrada[i] = temperatura;
}
}
SortSelection(entrada, entrada.Comprimento);
entrada de retorno ;
}
}
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.
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]).
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);
}
}
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
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)
} 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.
Isso serve para recalcular o valor hash do padrão, primeiro removendo o caractere mais à esquerda e depois adicionando o novo caractere do
texto.
1. Pior caso
2. Caso Médio
3. Melhor caso
#include <stdio.h>
int eu;
para (i=0; i<n; i++) {
if (arr[i] == x) retornar
i;
}
retornar -1;
}
int principal()
{
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;
}
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)
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.
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
baixo = 0;
alto = N -1;
while(baixo < alto) {
alto = médio;
} if(array[low] == x) //
encontrado, índice é baixo
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:
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.
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.
+-------+---+---+---+---+---+---+---+---+
| Í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 |
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
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
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.
+---------+---+---+---+---+---+---+---+---+---+--- +---+---+
| Í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 |
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:
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.
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.
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:
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:
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:
É 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:
Hash de retorno
Recálculo de hash:
Correspondência de cordas:
Rabin Karp:
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).
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.
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.
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
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)
Exemplos:
Entrada:
saída:
Entrada:
saída:
Implementação da linguagem C:
#include<string.h>
#include<stdlib.h>
if (pat[j] == txt[i]) {
j++;
eu++;
}
se (j == M) {
// 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;
}
if (pat[i] == pat[len]) {
lente++;
lps[i] = comprimento;
eu++;
if (len ! = 0) {
} else // if (len == 0) {
lps[i] = 0; eu+
+;
}
}
}
}
Saída:
Referência:
http://www.geeksforgeeks.org/searching-for-patterns-set-2-kmp-algorithm/
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.
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.
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.
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:
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.
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|
+-----+ +-----+ +-----+
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|
+-----+ +-----+ +-----+
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á:
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á:
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.
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.
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.
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)
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.
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.
retornar
verdadeiro; } senão
retorna falso; }
#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;
};
int principal()
{
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
{ fprintf(stderr, "Por favor, insira um número inteiro positivo válido de 1 a %d",MAXVERTICES); retornar -1; }
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");
}
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() {
if(Qfront == NULO) {
} 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 eu;
int i,vértice;
visitado[v] = 'Y';
enfileirar(v);
while((vértice = deque()) != -1) {
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.
printf("%d ",vértice+1);
adicione isso como primeira linha do loop while no BFS
int eu;
for(i = 0;i < noOfVertices;++i) {
if(visitado[i] == 'N')
BFS(gráfico,i,noOfVertices);
}
}
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).
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,
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).
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
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.
Encontrando o caminho.
Classificação topológica.
boleano
SByte
RuntimeHelpers.GetHashCode(este);
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:
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>
Variedade
Referências
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 são determinísticas. h(x) deve sempre retornar o mesmo valor para um determinado x
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).
Funções hash usadas em tabelas hash para calcular índices em uma matriz de slots. A tabela hash é uma estrutura de dados para
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
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.
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.
Psuedocódigo
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
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.
(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.
Esta linha pode ser um pouco confusa, então vamos analisá-la lentamente:
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.
Dado:
1. Valores (matriz v)
2. Pesos (matriz w)
3. Número de itens distintos(n)
4. Capacidade (W)
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))
Complexidade temporal do código acima: O(nW) onde n é o número de itens e W é a capacidade da mochila.
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;
}
outro {
k[eu, b] = k[eu - 1, b];
}
}
int n = valores.Comprimento;
return Mochila(nItens, pesos, valores, n);
}
}
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-
int eu, j;
while(!rootFound){ for(i=0;
i<n; i++){ Nx[i]=b[i]; // Cálculo
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;
}
}
}
}
retornar ;
}
int eu, j;
for(i=0; i<n; i++){ // inicialização
Nx[i]=x[i];
}
while(!rootFound){ for(i=0;
i<n; i++){ Nx[i]=b[i]; // Cálculo
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;
}
}
}
}
retornar ;
}
} printf("\n\n");
retornar ;
}
int main(){ //
inicialização da equação // número de
variáveis int n=3;
int eu;
}
JacobisMethod(n, x, b, a); imprimir(n, x);
x[eu]=0;
}
GaussSeidalMethod(n, x, b, a);
imprimir(n, x);
retornar 0;
}
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:
/ **
* Toma dois valores iniciais e encurta a distância em ambos os lados. **/ double
int loopContador=0;
if(f(a)*f(b) < 0){ while(1)
{ loopCounter+
+; c=(a+b)/2;
uma=c;
retornar raiz;
}
/ **
* Pega dois valores iniciais e encurta a distância em um único lado. **/ double FalsePosition()
{ double root=0;
int loopContador=0;
if(f(a)*f(b) < 0){ while(1)
{ loopCounter+
+;
else{ a=c;
}
}
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++;
x1=x2;
retornar raiz;
}
/ **
* Usa um valor inicial e gradualmente aproxima esse valor do valor real. **/ double Ponto Fixo(){ double
int loopContador=0;
while(1)
{ loopContador++;
x=g(x);
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++;
x2 = ((x0*f(x1))-(x1*f(x0))) / (f(x1)-f(x0));
x0=x1;
x1=x2;
retornar raiz;
}
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;
}
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.
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.
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.
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| | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
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
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| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
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| | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
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|
+-----+-----+-----+-----+-----+-----+-----+-----+
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:
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 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.
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.
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:
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
m ÿ eu
fim se
fim para
Parte 2:
coloque
um while A(m) > 1 do i ÿ
mÿ1
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
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], ÿ)
Abordagem 3:
LIS(A[1..n]): se (n =
0) retornar 0 m = 1
PRINCIPAL(A[1..n]):
retornar LIS(A[1..i])
Algoritmo Iterativo:
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
MAIN(A[1..n]): L =
LIS(A[1..n]) retorna
o valor máximo em L
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}.
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.
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.
// 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);
}
});
}
if(hashMap[i] !== 0)
{ isAnagram = false;
quebrar; }
else { isAnagram = true;
}
}
return éAnagrama;
}
}
})();
printf(" "); +
+contar;
}
enquanto(k != 2*i-1) {
}
outro {
++conta1;
printf("%d ", (i+k-2*contagem1));
} ++k;
} contagem1 = contagem = k = 0;
printf("\n");
}
Saída
1232
34543
456765456
7898765
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
if(menorValor % 2 == 0) { return
menorValor/2; } else { return
(menorValor+1)/2;
}
}
quadradoPrint(6,4);
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
[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) | | ------ |
[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].
|1 1 | X | f(n) | = | f(n+1) |
|1 0| | f(n-1) | | f(n) |
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) |
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:
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
estado em B.
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:
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:
Resumidamente:
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:
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.
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.
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)
X <- G.getAllVerticiesArrangedDescendenteByDegree()
para v em X faça
Lista<Vértice> adjacenteVertices1 <- G.getAdjacent(v)
C.adicionar(v)
C.remove(vértice)
retornar C
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)
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}
A princípio, definimos a distância entre dois pontos, d(x, y) onde xey representam os dois pontos. Deixar,
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é
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
+------+------+------+------+------+------+------+ ------+
| | 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,
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á:
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.
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
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.
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:
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.
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.
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.
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.
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>
*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);
retornar verdadeiro;
}
// Variáveis Inteiras
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;
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
}
DoisPi_NP = DoisPi_N*P;
//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)
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.
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.
#include <matemática.h>
x = 0;
DFT = 0;
throw "rad2InverseFFT(): N deve ser uma potência de 2 para Radix 2 Inverse FFT";
}
247
Notas sobre algoritmos do GoalKicker.com para profissionais
Machine Translated by Google
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
}
}
Apêndice A: Pseudocódigo
Seção A.1: Aectações variáveis
Digitado
intuma = 1 _
int a := 1 deixe
int a = 1
interno um <- 1
Nenhum tipo
a=1
a := 1
seja a = 1
um <- 1
Contanto que o nome da função, a instrução de retorno e os parâmetros estejam claros, você está bem.
retornar n + 1
ou
ou
são todos bastante claros, então você pode usá-los. Tente não ser ambíguo com uma afetação variável
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