Introduction aux algorithmes et exemples
Introduction aux algorithmes et exemples
#algorithm
Table des matières
À propos 1
Remarques 2
Examples 2
Introduction 6
Examples 6
Chapitre 3: A * Pathfinding 14
Examples 14
Introduction à A * 14
Introduction 24
Examples 24
Exemple d'échantillon 24
Remarques 26
Examples 26
Algorithme du chemin le plus court à source unique (étant donné qu'il y a un cycle négatif 26
Pourquoi avons-nous besoin de détendre tous les bords au maximum (V-1) fois 31
Examples 36
Examples 38
Introduction 41
Examples 41
Examples 45
Examples 48
Implémentation C # 49
Examples 51
Implémentation C # 53
Examples 54
Introduction 57
Examples 57
Exemple KMP 57
Chapitre 14: algorithme lié à un temps polynomial pour la couverture de vertex minimum 59
Introduction 59
Paramètres 59
Remarques 59
Examples 59
Pseudo-code d'algorithme 59
Entrée connectée G 59
Remarques 61
Théorie 61
Sources 63
Matériel de base 63
Lectures complémentaires 63
Code source 63
Examples 63
Préface 63
Pagination 63
Approche en ligne 65
Algorithmes de marquage 66
Remarques 69
Examples 69
Huffman Codage 69
Problème de changement 73
Le problème 75
Une analyse 75
La solution 77
Introduction 78
Syntaxe 78
Examples 78
fusionner-trier multithread 78
Introduction 80
Examples 80
Remarques 81
Sources 81
Examples 81
Automate à billet 81
Planification d'intervalle 84
Exemple (FIFO) 91
Exemple (LFD) 92
FIFO 94
LIFO 95
LRU 96
LFU 97
LFD 99
Introduction 101
Remarques 101
Définitions 101
Examples 101
Remarques 104
Chapitre 21: Arbres de recherche binaire 105
Introduction 105
Examples 105
Remarques 112
Travail 113
Envergure 113
Examples 114
Remarques 115
Liens 117
Examples 119
Implémentation C # 120
Examples 121
Examples 126
Examples 132
introduction 132
Examples 136
Examples 141
Exemples 142
Liens 143
Booléen 143
SByte 143
Carboniser 143
Int16 144
Décimal 144
Objet 144
Chaîne 144
Tableau 145
Remarques 146
Examples 146
Détecter un cycle dans un graphe orienté à l'aide de la méthode Traject First Traversal 148
Examples 162
Remarques 168
Examples 168
Examples 171
Examples 179
Examples 185
Examples 188
Remarques 191
Examples 192
introduction 194
Dichotomie 194
Explication 195
Conclusion 195
Examples 198
Implémentation C # 200
Remarques 202
Examples 202
Examples 204
Introduction 207
Remarques 207
Examples 207
Remarques 217
Examples 217
Dactylographié 217
Examples 219
introduction 219
Examples 226
Examples 237
Examples 250
Examples 256
Implémentation C # 256
Examples 258
Examples 262
Implémentation C # 263
Examples 264
Introduction 267
Examples 267
Examples 274
Examples 277
Paramètres 278
Examples 278
Paramètres 280
Examples 280
Implémentation en C # 281
Examples 285
Implémentation C # 287
Examples 288
Examples 290
Implémentation C # 291
Chapitre 58: Tri du tas 292
Examples 292
Implémentation C # 293
Examples 294
Remarques 301
Examples 302
Implémentation C # 304
Remarques 305
Examples 305
Implémentation C # 307
Examples 310
Remarques 313
Examples 313
Chapitre 64: Vérifier que deux chaînes sont des anagrammes 316
Introduction 316
Examples 316
Examples 319
Si une arborescence donnée suit la propriété de l'arborescence de recherche binaire ou non 319
Crédits 321
À propos
You can share this PDF with anyone you feel could benefit from it, downloaded the latest version
from: algorithm
It is an unofficial and free algorithm ebook created for educational purposes. All the content is
extracted from Stack Overflow Documentation, which is written by many hardworking individuals at
Stack Overflow. It is neither affiliated with Stack Overflow nor official algorithm.
The content is released under Creative Commons BY-SA, and the list of contributors to each
chapter are provided in the credits section at the end of this book. Images may be copyright of
their respective owners unless otherwise specified. All trademarks and registered trademarks are
the property of their respective company owners.
Use the content presented in this book at your own risk; it is not guaranteed to be correct nor
accurate, please send your feedback and corrections to info@[Link]
[Link] 1
Chapitre 1: Commencer avec l'algorithme
Remarques
Qu'est-ce qu'un algorithme? Informellement, un algorithme est une procédure pour accomplir une
tâche spécifique. [Skiena: 2008: ADM: 1410219] Spécifiquement, un algorithme est une procédure
de calcul bien définie , qui prend une valeur (ou un ensemble de valeurs) comme entrée et produit
une valeur ou un ensemble de valeurs en sortie . Un algorithme est donc une suite d'étapes de
calcul qui transforment l'entrée en sortie. Cormen et. Al. n'indique pas explicitement qu'un
algorithme ne nécessite pas nécessairement une entrée. [Cormen: 2001: IA: 580470]
Formellement, un algorithme doit satisfaire à cinq caractéristiques: [Knuth: 1997: ACP: 260999]
Une procédure qui manque de finitude mais satisfait à toutes les autres caractéristiques d'un
algorithme peut être appelée méthode de calcul . [Knuth: 1997: ACP: 260999]
Examples
Un exemple de problème algorithmique
Un problème algorithmique est spécifié en décrivant l'ensemble complet des instances sur
lesquelles il doit travailler et de sa sortie après avoir été exécuté sur l'une de ces instances. Cette
distinction entre un problème et une instance de problème est fondamentale. Le problème
algorithmique connu sous le nom de tri est défini comme suit: [Skiena: 2008: ADM: 1410219]
[Link] 2
• Problème: Tri
• Entrée: Une séquence de n clés, a_1, a_2, ..., a_n .
• Sortie: La réorganisation de la séquence d'entrée telle que a'_1 <= a'_2 <= ... <= a'_{n-1}
<= a'_n
Une instance de tri peut être un tableau de chaînes, tel que { Haskell, Emacs } ou une séquence
de nombres tels que { 154, 245, 1337 } .
Pour ceux d'entre vous qui sont nouveaux dans la programmation de Swift et ceux qui viennent de
différentes bases de programmation, telles que Python ou Java, cet article devrait être très utile.
Dans cet article, nous discuterons d'une solution simple pour implémenter des algorithmes
rapides.
Fizz Buzz
Vous avez peut-être vu Fizz Buzz écrit comme Fizz Buzz, FizzBuzz ou Fizz-Buzz; ils font tous
référence à la même chose. Cette "chose" est le sujet principal de discussion aujourd'hui. Tout
d'abord, qu'est-ce que FizzBuzz?
C'est une question courante qui se pose dans les entretiens d'embauche.
1 2 3 4 5 6 7 8 9 10
Pour travailler sur ce problème, ouvrez Xcode pour créer un nouveau terrain de jeu et initialiser un
tableau comme ci-dessous:
// for example
let number = [1,2,3,4,5]
// here 3 is fizz and 5 is buzz
Pour trouver tous les effets et les buzz, il faut parcourir le tableau et vérifier quels nombres sont
fizz et quels sont les buzz. Pour ce faire, créez une boucle for pour parcourir le tableau que nous
avons initialisé:
Après cela, nous pouvons simplement utiliser la condition "if else" et l'opérateur de module dans
swift ie -% pour localiser le fizz et le buzz
[Link] 3
for num in number {
if num % 3 == 0 {
print("\(num) fizz")
} else {
print(num)
}
}
Génial! Vous pouvez aller à la console de débogage dans la cour de jeu Xcode pour voir la sortie.
Vous constaterez que les "fizzes" ont été triés dans votre tableau.
Pour la partie Buzz, nous utiliserons la même technique. Essayons avant de faire défiler l'article -
vous pouvez vérifier vos résultats contre cet article une fois que vous avez terminé.
Vérifiez la sortie!
C'est plutôt simple - vous avez divisé le nombre par 3, fizz et divisé le nombre par 5, buzz.
Maintenant, augmentez les nombres dans le tableau
Voici la solution:
Attendez ... ce n'est pas fini! Le but de l'algorithme est de personnaliser le runtime correctement.
Imaginez si la plage augmente de 1-15 à 1-100. Le compilateur vérifiera chaque nombre pour
déterminer s'il est divisible par 3 ou 5. Il relèverait alors les nombres pour vérifier si les nombres
sont divisibles par 3 et 5. Le code devrait essentiellement parcourir chaque nombre du tableau.
[Link] 4
deux fois - il devrait d'abord exécuter les nombres par 3 et ensuite l'exécuter par 5. Pour accélérer
le processus, nous pouvons simplement dire à notre code de diviser les nombres directement par
15.
Aussi simple que cela, vous pouvez utiliser n'importe quelle langue de votre choix et commencer
Profitez du codage
[Link] 5
Chapitre 2: A * Algorithme Pathfinding
Introduction
Ce sujet va se concentrer sur l'algorithme A * Pathfinding, son utilisation et son fonctionnement.
Note aux futurs contributeurs: J'ai ajouté un exemple pour A * Pathfinding sans aucun obstacle sur
une grille 4x4. Un exemple avec des obstacles est toujours nécessaire.
Examples
Exemple simple de A * Pathfinding: Un labyrinthe sans obstacles
[Link] 6
Supposons que ce soit un labyrinthe . Il n'y a pas de murs / obstacles, cependant. Nous avons
seulement un point de départ (le carré vert) et un point final (le carré rouge). Supposons
également que pour passer du vert au rouge, nous ne pouvons pas nous déplacer en diagonale.
Donc, en partant du carré vert, voyons les carrés vers lesquels nous pouvons aller et les
surlignons en bleu:
[Link] 7
Pour choisir la case à déplacer, nous devons prendre en compte 2 heuristiques:
Pour calculer ces heuristiques, voici la formule que nous allons utiliser: distance = abs(from.x -
to.x) + abs(from.y - to.y)
Calculons la valeur "g" pour le carré bleu immédiatement à gauche du carré vert: abs(3 - 2) +
abs(2 - 2) = 1
Génial! Nous avons la valeur: 1. Maintenant, essayons de calculer la valeur "h": abs(2 - 0) + abs(2
- 0) = 4
Faisons la même chose pour tous les autres carrés bleus. Le grand nombre au centre de chaque
carré est la valeur "f", tandis que le nombre en haut à gauche est la valeur "g" et le nombre en
haut à droite est la valeur "h":
[Link] 8
Nous avons calculé les valeurs g, h et f pour tous les nœuds bleus. Maintenant, que choisissons-
nous?
Cependant, dans ce cas, nous avons 2 nœuds avec la même valeur f, 5. Comment les
choisissons-nous?
Simplement, vous pouvez en choisir un au hasard ou définir une priorité. Je préfère généralement
avoir une priorité comme celle-ci: "Right> Up> Down> Left"
L'un des nœuds avec la valeur f de 5 nous emmène dans la direction "Down", et l'autre nous
prend "Left". Puisque Down est plus prioritaire que Left, nous choisissons le carré qui nous
emmène "Down".
Je marque maintenant les noeuds pour lesquels nous avons calculé les heuristiques, mais ne les
a pas déplacés en orange, et le noeud que nous avons choisi comme cyan:
[Link] 9
Bon, maintenant, calculons la même heuristique pour les nœuds autour du nœud cyan:
Encore une fois, nous choisissons le nœud descendant du nœud cyan, car toutes les options ont
la même valeur f:
[Link] 10
Calculons l'heuristique pour le seul voisin que le noeud cyan a:
D'accord, puisque nous allons suivre le même schéma que nous avons suivi:
[Link] 11
Encore une fois, calculons l'heuristique pour le voisin du noeud:
Allons là-bas:
[Link] 12
Enfin, nous pouvons voir que nous avons une place gagnante à côté de nous, alors nous allons
là-bas et nous avons terminé.
[Link] 13
Chapitre 3: A * Pathfinding
Examples
Introduction à A *
A * (Une étoile) est un algorithme de recherche utilisé pour trouver un chemin d'un nœud à un
autre. Donc, il peut être comparé avec Breadth First Search , ou l'algorithme de Dijkstra , ou
Depth First Search , ou Best First Search. Un algorithme * est largement utilisé dans la recherche
de graphes pour améliorer l'efficacité et la précision, le prétraitement de graphe n'étant pas une
option.
A * est une spécialisation de Best First Search, dans laquelle la fonction d'évaluation f est définie
d'une manière particulière.
f (n) = g (n) + h (n) est le coût minimum depuis le nœud initial jusqu'aux objectifs conditionnés
pour aller au nœud de pensée n .
A * est un algorithme de recherche informé et garantit toujours de trouver le plus petit chemin
(chemin avec un coût minimum) dans le temps le plus court possible (si utilise une heuristique
admissible ). Il est donc à la fois complet et optimal . L'animation suivante montre A * search-
Définition du problème :
Un puzzle 8 est un jeu simple constitué d'une grille de 3 x 3 (contenant 9 carrés). L'un des carrés
est vide. L’objet est de déplacer les carrés dans différentes positions et d’afficher les chiffres dans
l’état «objectif».
[Link] 14
Étant donné un état initial de jeu de 8 casse-tête et un état final à atteindre, trouvez le chemin le
plus rentable pour atteindre l'état final à partir de l'état initial.
Etat initial :
_ 1 3
4 2 5
7 8 6
Etat final :
1 2 3
4 5 6
7 8 _
Heuristique à assumer :
Considérons la distance de Manhattan entre l'état actuel et l'état final comme heuristique pour
cette déclaration de problème.
h(n) = | x - p | + | y - q |
where x and y are cell co-ordinates in the current state
p and q are cell co-ordinates in the final state
f(n) = g(n) + h(n), where g(n) is the cost required to reach the current state from given
initial state
Nous trouvons d'abord la valeur heuristique requise pour atteindre l'état final à partir de l'état
initial. La fonction coût, g (n) = 0, comme nous sommes dans l'état initial
h(n) = 8
[Link] 15
La valeur ci-dessus est obtenue, car 1 dans l'état actuel est 1 distance horizontale que le 1 dans
l'état final. Même chose pour 2 , 5 , 6 . _ est à 2 distances horizontales et à 2 distances verticales.
La valeur totale pour h(n) est donc 1 + 1 + 1 + 1 + 2 + 2 = 8. La fonction de coût total f(n) est
égale à 8 + 0 = 8.
Maintenant, les états possibles qui peuvent être atteints d' un état initial se trouvent et il arrive que
l' on peut déplacer soit _ vers la droite ou vers le bas.
1 _ 3 4 1 3
4 2 5 _ 2 5
7 8 6 7 8 6
(1) (2)
Encore une fois, la fonction de coût total est calculée pour ces états en utilisant la méthode décrite
ci-dessus et il s’avère être respectivement 6 et 7. Nous avons choisi l'état avec un coût minimum
qui est l'état (1). Les prochains mouvements possibles peuvent être Left, Right ou Down. Nous ne
bougerons pas à gauche comme nous l'étions auparavant dans cet état. Donc, on peut bouger à
droite ou en bas.
Encore une fois, nous trouvons les états obtenus à partir de (1).
1 3 _ 1 2 3
4 2 5 4 _ 5
7 8 6 7 8 6
(3) (4)
(3) conduit à une fonction de coût égale à 6 et (4) à 4. En outre, on considérera (2) obtenu avant,
dont la fonction de coût est égale à 7. En choisissant minimum, on obtient (4). Les prochains
mouvements possibles peuvent être Left, Right ou Down. Nous obtenons des états:
1 2 3 1 2 3 1 2 3
_ 4 5 4 5 _ 4 8 5
7 8 6 7 8 6 7 _ 6
(5) (6) (7)
Nous obtenons des coûts égaux à 5, 2 et 4 pour (5), (6) et (7) respectivement. Nous avons
également les états précédents (3) et (2) avec 6 et 7 respectivement. Nous avons choisi l'état de
coût minimum qui est (6). Les prochains mouvements possibles sont Up, et Down et clairement
Down nous mèneront à l'état final conduisant à une valeur de fonction heuristique égale à 0.
[Link] 16
Supposons que ce soit un labyrinthe . Il n'y a pas de murs / obstacles, cependant. Nous avons
seulement un point de départ (le carré vert) et un point final (le carré rouge). Supposons
également que pour passer du vert au rouge, nous ne pouvons pas nous déplacer en diagonale.
Donc, en partant du carré vert, voyons les carrés vers lesquels nous pouvons aller et les
surlignons en bleu:
[Link] 17
Pour choisir la case à déplacer, nous devons prendre en compte 2 heuristiques:
Pour calculer ces heuristiques, voici la formule que nous allons utiliser: distance = abs(from.x -
to.x) + abs(from.y - to.y)
Calculons la valeur "g" pour le carré bleu immédiatement à gauche du carré vert: abs(3 - 2) +
abs(2 - 2) = 1
Génial! Nous avons la valeur: 1. Maintenant, essayons de calculer la valeur "h": abs(2 - 0) + abs(2
- 0) = 4
Faisons la même chose pour tous les autres carrés bleus. Le grand nombre au centre de chaque
carré est la valeur "f", tandis que le nombre en haut à gauche est la valeur "g" et le nombre en
haut à droite est la valeur "h":
[Link] 18
Nous avons calculé les valeurs g, h et f pour tous les nœuds bleus. Maintenant, que choisissons-
nous?
Cependant, dans ce cas, nous avons 2 nœuds avec la même valeur f, 5. Comment les
choisissons-nous?
Simplement, vous pouvez en choisir un au hasard ou définir une priorité. Je préfère généralement
avoir une priorité comme celle-ci: "Right> Up> Down> Left"
L'un des nœuds avec la valeur f de 5 nous emmène dans la direction "Down", et l'autre nous
prend "Left". Puisque Down est plus prioritaire que Left, nous choisissons le carré qui nous
emmène "Down".
Je marque maintenant les noeuds pour lesquels nous avons calculé les heuristiques, mais ne les
a pas déplacés en orange, et le noeud que nous avons choisi comme cyan:
[Link] 19
Bon, maintenant, calculons la même heuristique pour les nœuds autour du nœud cyan:
Encore une fois, nous choisissons le nœud descendant du nœud cyan, car toutes les options ont
la même valeur f:
[Link] 20
Calculons l'heuristique pour le seul voisin que le noeud cyan a:
D'accord, puisque nous allons suivre le même schéma que nous avons suivi:
[Link] 21
Encore une fois, calculons l'heuristique pour le voisin du noeud:
Allons là-bas:
[Link] 22
Enfin, nous pouvons voir que nous avons une place gagnante à côté de nous, alors nous allons
là-bas et nous avons terminé.
[Link] 23
Chapitre 4: Algo: - Imprime une matrice * en
carré
Introduction
Vérifiez l'entrée et la sortie de l'échantillon ci-dessous.
Examples
Exemple d'échantillon
Input :-
14 15 16 17 18 21
19 10 20 11 54 36
64 55 44 23 80 39
91 92 93 94 95 42
Output:-
print value in index
14 15 16 17 18 21 36 39 42 95 94 93 92 91 64 19 10 20 11 54 80 23 44 55
or print index
00 01 02 03 04 05 15 25 35 34 33 32 31 30 20 10 11 12 13 14 24 23 22 21
function noOfLooping(m,n) {
if(m > n) {
smallestValue = n;
} else {
smallestValue = m;
}
if(smallestValue % 2 == 0) {
return smallestValue/2;
} else {
return (smallestValue+1)/2;
}
}
function squarePrint(m,n) {
var looping = noOfLooping(m,n);
for(var i = 0; i < looping; i++) {
for(var j = i; j < m - 1 - i; j++) {
[Link](i+''+j);
}
for(var k = i; k < n - 1 - i; k++) {
[Link](k+''+j);
}
for(var l = j; l > i; l--) {
[Link] 24
[Link](k+''+l);
}
for(var x = k; x > i; x--) {
[Link](x+''+l);
}
}
}
squarePrint(6,4);
[Link] 25
Chapitre 5: Algorithme Bellman – Ford
Remarques
Étant donné un graphe orienté G , nous voulons souvent trouver la distance la plus courte d'un
nœud A donné au reste des nœuds du graphe. L' algorithme de Dijkstra est l'algorithme le plus
connu pour trouver le chemin le plus court, mais il ne fonctionne que si les poids d'arête du
graphique donné ne sont pas négatifs. Bellman-Ford vise cependant à trouver le chemin le plus
court à partir d'un nœud donné (s'il en existe un), même si certains des poids sont négatifs. Notez
que la distance la plus courte peut ne pas exister si un cycle négatif est présent dans le graphique
(auquel cas nous pouvons contourner le cycle pour obtenir une distance totale infiniment petite).
Bellman-Ford nous permet en outre de déterminer la présence d’un tel cycle.
Examples
Algorithme du chemin le plus court à source unique (étant donné qu'il y a un
cycle négatif dans un graphique)
Avant de lire cet exemple, il est nécessaire d'avoir une brève idée de la relaxation des contours.
Vous pouvez l'apprendre d' ici
Algorithme Bellman-Ford calcule les chemins les plus courts d'un sommet source unique vers tous
les autres sommets d'un digraphe pondéré. Même s'il est plus lent que l'algorithme de Dijkstra , il
fonctionne dans les cas où le poids de l'arête est négatif et qu'il détecte également un cycle de
pondération négatif dans le graphique. Le problème avec l'algorithme de Dijkstra est que s'il y a
un cycle négatif, vous continuez à parcourir le cycle encore et encore et continuez à réduire la
distance entre deux sommets.
L'idée de cet algorithme est de parcourir tous les bords de ce graphe un par un dans un ordre
aléatoire. Cela peut être n'importe quel ordre aléatoire. Mais vous devez vous assurer que si uv
(où u et v sont deux sommets dans un graphique) est l’un de vos ordres, alors il doit y avoir un
bord de u à v . Habituellement, il est pris directement dans l'ordre des entrées données. Encore
une fois, tout ordre aléatoire fonctionnera.
Après avoir sélectionné la commande, nous détendrons les bords selon la formule de relaxation.
Pour une arête uv donnée vont de u à v de la formule de relaxation est la suivante :
[Link] 26
Nous devons assouplir les arêtes au maximum (V-1) fois où V est le nombre d'arêtes du
graphique. Pourquoi (V-1) vous demandez? Nous allons l'expliquer dans un autre exemple. Nous
allons également garder une trace du sommet parent de tout sommet, c'est-à-dire que lorsque
nous relâchons un bord, nous allons définir:
parent[v] = u
Cela signifie que nous avons trouvé un autre chemin plus court pour atteindre v via u . Nous
aurons besoin de cela plus tard pour imprimer le plus court chemin de la source au sommet de
destination.
Nous avons sélectionné 1 comme sommet source . Nous voulons trouver le plus court chemin de
la source à tous les autres sommets.
Au début, d [1] = 0 car c'est la source. Et le repos est infini , car nous ne connaissons pas encore
leur distance.
+--------+--------+--------+--------+--------+--------+--------+
| Serial | 1 | 2 | 3 | 4 | 5 | 6 |
+--------+--------+--------+--------+--------+--------+--------+
| Edge | 4->5 | 3->4 | 1->3 | 1->4 | 4->6 | 2->3 |
+--------+--------+--------+--------+--------+--------+--------+
Vous pouvez prendre n'importe quelle séquence que vous voulez. Si nous détendons les bords
une fois, qu'est-ce que nous obtenons? Nous obtenons la distance entre la source et tous les
autres sommets du chemin qui utilise au maximum 1 front. Détendons maintenant les arêtes et
mettons à jour les valeurs de d [] . On a:
1. d [4] + coût [4] [5] = infini + 7 = infini . Nous ne pouvons pas mettre à jour celui-ci.
[Link] 27
2. d [2] + coût [3] [4] = infini . Nous ne pouvons pas mettre à jour celui-ci.
3. d [1] + coût [1] [2] = 0 + 2 = 2 < d [2] . Donc d [2] = 2 . Aussi parent [2] = 1 .
4. d [1] + coût [1] [4] = 4 . Donc d [4] = 4 < d [4] . parent [4] = 1 .
5. d [4] + coût [4] [6] = 9 . d [6] = 9 < d [6] . parent [6] = 4 .
6. d [2] + coût [2] [2] = infini . Nous ne pouvons pas mettre à jour celui-ci.
Nous n'avons pas pu mettre à jour certains sommets, car la condition d[u] + cost[u][v] < d[v] ne
correspondait pas. Comme nous l'avons déjà dit, nous avons trouvé les chemins de la source
vers les autres nœuds en utilisant un maximum de 1 front.
[Link] 28
Notre 3ème itération ne mettra à jour que le sommet 5 , où d [5] sera 8 . Notre graphique
ressemblera à:
Après cela, peu importe le nombre d'itérations que nous faisons, nous aurons les mêmes
distances. Nous allons donc garder un drapeau qui vérifie si une mise à jour a lieu ou non. Si ce
n'est pas le cas, nous allons simplement casser la boucle. Notre pseudo-code sera:
[Link] 29
for all edges from (u,v) in Graph
if d[u] + cost[u][v] < d[v]
d[v] := d[u] + cost[u][v]
parent[v] := u
flag := true
end if
end for
if flag == false
break
end for
Return d
Pour suivre le cycle négatif, nous pouvons modifier notre code en utilisant la procédure décrite ici .
Notre pseudo-code complété sera:
Chemin d'impression:
Pour imprimer le plus court chemin vers un sommet, nous reviendrons à son parent jusqu'à ce
que nous trouvions NULL , puis imprimons les sommets. Le pseudo-code sera:
Procedure PathPrinting(u)
v := parent[u]
if v == NULL
return
PathPrinting(v)
print -> u
Complexité:
Puisque nous devons assouplir les arêtes maximales (V-1) , la complexité temporelle de cet
[Link] 30
algorithme sera égale à O (V * E) où E représente le nombre d'arêtes, si nous utilisons la
adjacency list pour représenter le graphique. Cependant, si la adjacency matrix est utilisée pour
représenter le graphique, la complexité temporelle sera O (V ^ 3) . La raison est que nous
pouvons parcourir tous les fronts dans le temps O (E) lorsque la adjacency list est utilisée, mais
cela prend du temps O (V ^ 2) lorsque la adjacency matrix est utilisée.
Pour comprendre cet exemple, il est recommandé d'avoir une idée succincte de l'algorithme de
chemin le plus court à source unique de Bellman-Ford, disponible ici
Dans l'algorithme Bellman-Ford, pour trouver le chemin le plus court, nous devons assouplir tous
les bords du graphique. Ce processus est répété au maximum (V-1) fois, où V est le nombre de
sommets dans le graphique.
Le nombre d'itérations nécessaires pour trouver le plus court chemin de la source à tous les
autres sommets dépend de l'ordre que nous choisissons pour assouplir les bords.
Regardons un exemple:
Ici, le sommet source est 1. Nous allons trouver la plus courte distance entre la source et tous les
autres sommets. Nous pouvons clairement voir que, pour atteindre le sommet 4 , dans le pire des
cas, cela prendra des arêtes (V-1) . Maintenant, en fonction de l'ordre dans lequel les arêtes sont
découvertes, il faut parfois (V-1) fois pour découvrir le sommet 4 . Vous n'avez pas compris?
Utilisons l'algorithme de Bellman-Ford pour trouver le chemin le plus court ici:
+--------+--------+--------+--------+
| Serial | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Edge | 3->4 | 2->3 | 1->2 |
+--------+--------+--------+--------+
Nous pouvons voir que notre processus de relaxation n'a changé que d [2] . Notre graphique
[Link] 31
ressemblera à:
Deuxième itération:
Troisième itération:
Notre troisième itération a finalement trouvé le plus court chemin vers 4 à partir de 1 . Notre
graphique ressemblera à:
Donc, il a fallu 3 itérations pour trouver le chemin le plus court. Après celui-ci, peu importe le
nombre de fois où nous relâchons les bords, les valeurs de d [] resteront les mêmes. Maintenant,
si nous considérions une autre séquence:
+--------+--------+--------+--------+
| Serial | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Edge | 1->2 | 2->3 | 3->4 |
+--------+--------+--------+--------+
Nous aurions:
[Link] 32
3. d [3] + coût [3] [4] = 7 < d [4] . d [4] = 5 .
Notre toute première itération a trouvé le chemin le plus court entre la source et tous les autres
nœuds. Une autre séquence 1-> 2 , 3-> 4 , 2-> 3 est possible, ce qui nous donnera le plus court
chemin après 2 itérations. Nous pouvons décider que peu importe la manière dont nous
organisons la séquence, il ne faudra pas plus de 3 itérations pour trouver le plus court chemin à
partir de la source dans cet exemple.
Nous pouvons conclure que, dans le meilleur des cas, il faudra 1 itération pour trouver le plus
court chemin depuis la source . Dans le pire des cas, il faudra des itérations (V-1) , c'est pourquoi
nous répétons le processus de relaxation (V-1) .
Pour comprendre cet exemple, il est recommandé d'avoir une brève idée de l'algorithme de
Bellman-Ford, disponible ici
En utilisant l'algorithme de Bellman-Ford, nous pouvons détecter s'il y a un cycle négatif dans
notre graphique. Nous savons que pour trouver le chemin le plus court, nous devons assouplir
toutes les arêtes du graphe (V-1) , où V est le nombre de sommets d'un graphe. Nous avons déjà
vu que dans cet exemple , après les itérations (V-1) , nous ne pouvons pas mettre à jour d [] , peu
importe le nombre d'itérations que nous faisons. Ou pouvons-nous?
S'il y a un cycle négatif dans un graphique, même après des itérations (V-1) , nous pouvons
mettre à jour d [] . Cela se produit car pour chaque itération, le parcours du cycle négatif diminue
toujours le coût du chemin le plus court. C'est pourquoi l'algorithme Bellman-Ford limite le nombre
d'itérations à (V-1) . Si nous utilisions l'algorithme de Dijkstra ici, nous serions coincés dans une
boucle sans fin. Cependant, concentrons-nous sur la recherche de cycle négatif.
Prenons le sommet 1 comme source . Après avoir appliqué l'algorithme de chemin le plus court à
source unique de Bellman-Ford au graphique, nous découvrirons les distances entre la source et
tous les autres sommets.
[Link] 33
Voici comment le graphique se présente après (V-1) = 3 itérations. Ce devrait être le résultat car il
y a 4 arêtes, nous avons besoin d'au plus 3 itérations pour trouver le plus court chemin. Donc, soit
c'est la réponse, soit il y a un cycle de pondération négatif dans le graphique. Pour trouver que,
après les itérations (V-1) , nous effectuons une dernière itération finale et que si la distance
continue à diminuer, cela signifie qu'il y a définitivement un cycle de pondération négatif dans le
graphique.
Pour cet exemple: si on vérifie 2-3 , d [2] + cost [2] [3] nous donnera 1 qui est inférieur à d [3] .
On peut donc en conclure qu'il y a un cycle négatif dans notre graphique.
Alors, comment découvrons-nous le cycle négatif? Nous modifions un peu la procédure Bellman-
Ford:
C'est ainsi que nous découvrons s'il y a un cycle négatif dans un graphique. Nous pouvons
[Link] 34
également modifier l'algorithme Bellman-Ford pour suivre les cycles négatifs.
[Link] 35
Chapitre 6: Algorithme catalan
Examples
Algorithme des nombres catalans Informations de base
1. Le nombre de manières d'empiler des pièces sur une rangée inférieure constituée de n
pièces consécutives dans un plan, de sorte qu'aucune pièce ne puisse être placée des deux
côtés des pièces du bas et que chaque pièce supplémentaire soit supérieure à deux autres
pièces, est le nième numéro catalan.
2. Le nombre de façons de regrouper une chaîne de n paires de parenthèses, de sorte que
chaque parenthèse ouverte ait une parenthèse fermée, est le nième numéro catalan.
3. Le nombre de façons de découper en triangles un polygone convexe à 2 faces n + dans un
plan en reliant des sommets à des lignes droites sans intersection est le nième nombre
catalan. C'est l'application dans laquelle Euler était intéressé.
En utilisant la numérotation de base zéro, la n - ième nombre de Catalan est donnée directement
en termes de coefficients binomiaux par l'équation suivante.
[Link] 36
Espace auxiliaire: O(n)
Complexité temporelle: O(n^2)
Implémentation C #
[Link] 37
Chapitre 7: Algorithme de fenêtre coulissante
Examples
Algorithme de fenêtre coulissante Informations de base
L'algorithme de fenêtre glissante est utilisé pour effectuer les opérations requises sur une taille de
fenêtre spécifique d'un grand tampon ou d'un tableau donné. La fenêtre part du 1er élément et
continue de se déplacer d'un élément à l'autre. L'objectif est de trouver le nombre minimal de k
présent dans chaque fenêtre. Ceci est communément appelé problème de fenêtre coulissante ou
algorithme.
Par exemple, pour trouver l'élément maximal ou minimal de chaque élément n dans un tableau
donné, un algorithme de fenêtre glissante est utilisé.
Exemple:
Tableau d'entrée: [1 3 -1 -3 5 3 6 7]
Taille de la fenêtre: 3
+---------------------------------+---------+
| Windows Position | Max |
+------------+----+---+---+---+---+---------+
|[1 3 -1]| -3 | 5 | 3 | 6 | 7 | 3 |
+------------+----+---+---+---+---+---------+
| 1 |[3 -1 -3]| 5 | 3 | 6 | 7 | 3 |
+---+-------------+---+---+---+---+---------+
| 1 | 3 |[-1 -3 5]| 3 | 6 | 7 | 5 |
+---+---+-------------+---+---+---+---------+
| 1 | 3 | -1 |[-3 5 3]| 6 | 7 | 5 |
+---+---+----+------------+---+---+---------+
| 1 | 3 | -1 | -3 |[5 3 6]| 7 | 6 |
+---+---+----+----+-----------+---+---------+
| 1 | 3 | -1 | -3 | 5 |[3 6 7]| 7 |
+---+---+----+----+---+-----------+---------+
+---------------------------------+---------+
| Windows Position | Min |
+------------+----+---+---+---+---+---------+
|[1 3 -1]| -3 | 5 | 3 | 6 | 7 | -1 |
+------------+----+---+---+---+---+---------+
| 1 |[3 -1 -3]| 5 | 3 | 6 | 7 | -3 |
+---+-------------+---+---+---+---+---------+
| 1 | 3 |[-1 -3 5]| 3 | 6 | 7 | -3 |
+---+---+-------------+---+---+---+---------+
| 1 | 3 | -1 |[-3 5 3]| 6 | 7 | -3 |
+---+---+----+------------+---+---+---------+
[Link] 38
| 1 | 3 | -1 | -3 |[5 3 6]| 7 | 3 |
+---+---+----+----+-----------+---+---------+
| 1 | 3 | -1 | -3 | 5 |[3 6 7]| 3 |
+---+---+----+----+---+-----------+---------+
Méthode 1:
La première méthode consiste à utiliser le tri rapide, lorsque le pivot est à la position K, tous les
éléments du côté droit sont supérieurs au pivot, par conséquent, tous les éléments du côté
gauche deviennent automatiquement les plus petits éléments d’un tableau donné.
Méthode 2:
Conservez un tableau d'éléments K, remplissez-le avec les premiers K éléments d'un tableau
d'entrée donné. Maintenant, à partir de l'élément K + 1, vérifiez si l'élément actuel est inférieur à
l'élément maximal du tableau auxiliaire, si oui, ajoutez cet élément dans le tableau. Le seul
problème avec la solution ci-dessus est que nous devons garder la trace de l'élément maximum.
Toujours réalisable. Comment pouvons-nous garder trace de l'élément maximum dans l'ensemble
des nombres entiers? Pensez tas Pensez Max tas.
Méthode 3:
Génial! Dans O (1), on obtiendrait l'élément max parmi K éléments déjà choisis comme plus petits
éléments K. Si max dans l'ensemble actuel est supérieur à l'élément nouvellement considéré,
nous devons supprimer max et introduire un nouvel élément dans l'ensemble du plus petit élément
K. Heapify à nouveau pour conserver la propriété heap. Maintenant, nous pouvons facilement
obtenir K éléments minimum dans le tableau de N.
[Link] 39
public static int[] MinSlidingWindow(int[] input, int k)
{
int[] result = new int[[Link] - k + 1];
for (int i = 0; i <= [Link] - k; i++)
{
var min = input[i];
for (int j = 1; j < k; j++)
{
if (input[i + j] < min) min = input[i + j];
}
result[i] = min;
}
return result;
}
[Link] 40
Chapitre 8: Algorithme de ligne
Introduction
Le dessin au trait est effectué en calculant des positions intermédiaires le long du tracé de ligne
entre deux positions de point final spécifiées. Un périphérique de sortie est ensuite dirigé pour
remplir ces positions entre les points d'extrémité.
Examples
Algorithme de dessin au trait de Bresenham
3. Calculer
Delx = | x2 - x1 |
Dely = | y2 - y1 |
Si p <0 alors
X1 = x1 + 1
Pot (x1, y1)
P = p + 2 dely
[Link] 41
Autre
X1 = x1 + 1
Y1 = y1 + 1
Tracé (x1, y1)
P = p + 2 dely - 2 * delx
Fin si
Fin pour
6. FIN
Code source:
int main()
{
int gdriver=DETECT,gmode;
int x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\\BGI");
putpixel(x1,y1,RED);
delx=fabs(x2-x1);
dely=fabs(y2-y1);
p=(2*dely)-delx;
for(i=0;i<delx;i++){
if(p<0)
{
x1=x1+1;
putpixel(x1,y1,RED);
p=p+(2*dely);
}
else
{
x1=x1+1;
y1=y1+1;
putpixel(x1,y1,RED);
p=p+(2*dely)-(2*delx);
}
}
getch();
closegraph();
return 0;
[Link] 42
}
Autre
X1 = x1 + 1
Y1 = y1 + 1
Terrain (x1, y1)
P = p + 2 delx - 2 * dely
Fin si
Fin pour
6. FIN
Code source:
[Link] 43
p=p+(2*delx);
}
else
{
x1=x1+1;
y1=y1+1;
putpixel(x1,y1,RED);
p=p+(2*delx)-(2*dely);
}
}
getch();
closegraph();
return 0;
}
[Link] 44
Chapitre 9: Algorithme de partition entier
Examples
Informations de base sur l'algorithme de partition entière
La partition d'un entier est une manière de l'écrire comme une somme d'entiers positifs. Par
exemple, les partitions du numéro 5 sont:
• 5
• 4+1
• 3+2
• 2+2+1
• 2+1+1+1
• 1+1+1+1+1
Notez que la modification de l'ordre des sommets ne crée pas une partition différente.
La fonction de partition est intrinsèquement récursive puisque les résultats des nombres plus
petits apparaissent comme des composants dans le résultat d'un nombre plus grand. Soit p (n, m)
le nombre de partitions de n en utilisant uniquement des entiers positifs inférieurs ou égaux à m .
On peut voir que p (n) = p (n, n) , et aussi p (n, m) = p (n, n) = p (n) pour m > n .
[Link] 45
Espace auxiliaire: O(n^2)
Complexité temporelle: O(n(logn))
[Link] 46
Result[i, j] = Result[i, j - 1] + Result[i - j, j];
}
}
return Result[targetNumber, largestNumber];
}
[Link] 47
Chapitre 10: Algorithme de somme de chemin
maximale
Examples
Informations de base sur la somme de chemin maximale
Maximum Path Sum est un algorithme permettant de trouver un chemin tel que la somme des
éléments (nœuds) de ce chemin est supérieure à tout autre chemin.
3
7 4
2 4 6
8 5 9 3
Dans le triangle ci-dessus, trouvez le chemin maximum qui a la somme maximum. La réponse est
3 + 7 + 4 + 9 = 23
Pour trouver la solution, nous avons toujours une idée de la méthode Brute-Force. La méthode
Brute-Force est bonne pour ce triangle à 4 rangées, mais pensez à un triangle avec 100 ou plus
de 100 lignes. Donc, nous ne pouvons pas utiliser la méthode Brute-Force pour résoudre ce
problème.
Algorithme:
Pour chaque nœud dans un triangle ou dans un arbre binaire, il peut y avoir quatre façons pour
que le chemin max traverse le nœud.
1. Node seulement
2. Chemin maximum à travers l'enfant gauche + le nœud
3. Max chemin à travers Right Child + Node
4. Chemin maximum à travers l'enfant gauche + Nœud + Max chemin à travers l'enfant droit.
[Link] 48
Espace auxiliaire: O(n)
Complexité temporelle: O(n)
Implémentation C #
class Res
{
public int Val;
}
[Link] 49
{
if (node == null) return 0;
int l = FindMaxUtil([Link], res);
int r = FindMaxUtil([Link], res);
int maxSingle = [Link]([Link](l, r) + [Link], [Link]);
int maxTop = [Link](maxSingle, l + r + [Link]);
[Link] = [Link]([Link], maxTop);
return maxSingle;
}
int FindMaxSum()
{
return FindMaxSum(_root);
}
[Link] 50
Chapitre 11: Algorithme de sous-tableau
maximum
Examples
Algorithme du sous-tableau maximum Informations de base
Le problème a été proposé à l'origine par Ulf Grenander de l'Université Brown en 1977, en tant
que modèle simplifié pour l'estimation du maximum de vraisemblance des motifs dans les images
numérisées.
Nous pouvons avoir un problème comme celui-ci, considérons une liste de différents entiers. Nous
pourrions être intéressés par le sous-ensemble complètement adjacent qui aura la plus grande
somme. Par exemple, si nous avons le tableau [0, 1, 2, -2, 3, 2] , le sous-tableau maximal est
[3, 2] , avec une somme de 5 .
Cette méthode est la plus inefficace pour trouver la solution. En cela, nous finirons par parcourir
chaque sous-tableau possible, puis trouver la somme de tous. Enfin, comparez toutes les valeurs
et trouvez le sous-tableau maximum.
MaxSubarray(array)
maximum = 0
for i in input
current = 0
for j in input
current += array[j]
if current > maximum
maximum = current
return maximum
Trouvez la somme des sous-réseaux à gauche, les sous-réseaux à droite. Jetez ensuite un coup
d’œil à tous ceux qui traversent la ligne de partage des centres et retournez la somme maximale.
Comme il s’agit d’un algorithme de division et de conquête, nous devons avoir deux fonctions
différentes.
[Link] 51
La première est une étape de division,
maxSubarray(array)
if start = end
return array[start]
else
middle = (start + end) / 2
return max(maxSubarray(array(From start to middle)), maxSubarray(array(From middle + 1 to
end)), maxCrossover(array))
Dans la deuxième partie, séparez les différentes parties créées dans la première partie.
maxCrossover(array)
currentLeftSum = 0
leftSum = 0
currentRightSum = 0
rightSum = 0
for i in array
currentLeftSum += array[i]
if currentLeftSum > leftSum
leftSum = currentLeftSum
for i in array
currentRightSum += array[i]
if currentRightSum > rightSum
rightSum = currentRightSum
return leftSum + rightSum
La complexité temporelle de la méthode Divide and Conquer est O(nlogn) . Passons donc à
l'approche de la programmation dynamique.
Cette solution est également connue sous le nom d'Algorithme de Kadane. C'est un algorithme de
temps linéaire. Cette solution est donnée par Joseph B. Kadane à la fin des années 70.
Cet algorithme ne fait que passer par la boucle, modifiant continuellement la somme maximale
actuelle. Il est intéressant de noter que ceci est un exemple très simple d'algorithme de
programmation dynamique, car il prend un problème de chevauchement et le réduit pour que nous
puissions trouver une solution plus efficace.
MaxSubArray(array)
max = 0
currentMax = 0
for i in array
currentMax += array[i]
if currentMax < 0
currentMax = 0
if max < currentMax
max = currentMax
return max
[Link] 52
Implémentation C #
[Link] 53
Chapitre 12: Algorithme Floyd-Warshall
Examples
Algorithme du chemin le plus court de toutes les paires
L'algorithme de Floyd-Warshall permet de trouver les chemins les plus courts dans un graphique
pondéré avec des poids de bord positifs ou négatifs. Une seule exécution de l'algorithme trouvera
les longueurs (poids additionnés) des plus courts chemins entre toutes les paires de sommets.
Avec une petite variation, il peut imprimer le chemin le plus court et détecter les cycles négatifs
dans un graphique. Floyd-Warshall est un algorithme de programmation dynamique.
La première chose à faire est de prendre deux matrices 2D. Ce sont des matrices d'adjacence .
La taille des matrices va être le nombre total de sommets. Pour notre graphique, nous prendrons
4 * 4 matrices. La distance Matrix va stocker la distance minimale trouvée entre deux sommets.
Au début, pour les arêtes, s'il y a un bord entre uv et la distance / poids est w , nous allons
stocker: distance[u][v] = w . Pour tous les bords qui n'existent pas, nous allons mettre l' infini . La
matrice de chemin sert à régénérer le chemin de distance minimum entre deux sommets. Donc,
au départ, s'il y a un chemin entre u et v , nous allons mettre le path[u][v] = u . Cela signifie que
la meilleure façon d’arriver à vertex-v à partir de vertex-u est d’utiliser l’arête qui connecte v avec
u . S'il n'y a pas de chemin entre deux sommets, nous allons y mettre N indiquant qu'il n'y a pas
de chemin disponible maintenant. Les deux tableaux de notre graphique ressembleront à:
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| | 1 | 2 | 3 | 4 | | | 1 | 2 | 3 | 4 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 1 | 0 | 3 | 6 | 15 | | 1 | N | 1 | 1 | 1 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 2 | inf | 0 | -2 | inf | | 2 | N | N | 2 | N |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 3 | inf | inf | 0 | 2 | | 3 | N | N | N | 3 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| 4 | 1 | inf | inf | 0 | | 4 | 4 | N | N | N |
[Link] 54
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
distance path
Comme il n'y a pas de boucle, les diagonales sont définies sur N. Et la distance du sommet lui-
même est 0 .
Donc, ce que nous vérifions en fait, c'est que pour chaque paire de sommets, la distance est-elle
plus courte en passant par un autre sommet? Le nombre total d'opérations pour notre graphique
sera 4 * 4 * 4 = 64 . Cela signifie que nous allons faire cette vérification 64 fois. Regardons
quelques-unes d'entre elles:
Lorsque k = 1 , i = 2 et j = 3 , la distance [i] [j] est -2 , ce qui n’est pas supérieur à la distance [i]
[k] + distance [k] [j] = -2 + 0 = -2 . Donc, il restera inchangé. Là encore, lorsque k = 1 , i = 4 et j =
2 , la distance [i] [j] = infini , qui est supérieure à la distance [i] [k] + distance [k] [j] = 1 + 3 = 4
. On met donc la distance [i] [j] = 4 et on met le chemin [i] [j] = chemin [k] [j] = 1 . Cela signifie
que pour aller du sommet 4 au sommet 2 , le chemin 4-> 1> 2 est plus court que le chemin
existant. C'est ainsi que nous remplissons les deux matrices. Le calcul pour chaque étape est
indiqué ici . Après avoir apporté les modifications nécessaires, nos matrices ressembleront à ceci:
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
| | 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 |
+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+
distance path
C'est notre matrice de distance la plus courte. Par exemple, la distance la plus courte entre 1 et 4
est 3 et la distance la plus courte entre 4 et 3 est 2 . Notre pseudo-code sera:
Procedure Floyd-Warshall(Graph):
for k from 1 to V // V denotes the number of vertex
for i from 1 to V
[Link] 55
for j from 1 to V
if distance[i][j] > distance[i][k] + distance[k][j]
distance[i][j] := distance[i][k] + distance[k][j]
path[i][j] := path[k][j]
end if
end for
end for
end for
Imprimer le chemin:
Pour imprimer le chemin, nous allons vérifier la matrice Path . Pour imprimer le chemin de u à v ,
nous allons commencer par le chemin [u] [v] . Nous allons continuer à changer v = path [u] [v]
jusqu'à ce que nous trouvions le chemin [u] [v] = u et poussez toutes les valeurs du chemin [u]
[v] dans une pile. Après avoir trouvé u , nous allons imprimer u et commencer à extraire des
éléments de la pile et les imprimer. Cela fonctionne parce que la matrice de chemin stocke la
valeur du sommet qui partage le plus court chemin vers v depuis n'importe quel autre nœud. Le
pseudo-code sera:
Pour savoir s'il existe un cycle de front négatif, nous devons vérifier la diagonale principale de la
matrice de distance . Si une valeur sur la diagonale est négative, cela signifie qu'il y a un cycle
négatif dans le graphique.
Complexité:
[Link] 56
Chapitre 13: Algorithme Knuth Morris Pratt
(KMP)
Introduction
Le KMP est un algorithme de recherche de modèle qui recherche les occurrences d'un "mot" W
dans une "chaîne de texte" S principale en utilisant l'observation que, en cas d'incompatibilité,
nous disposons des informations suffisantes pour déterminer où la prochaine correspondance
pourrait commencer. tirer parti de cette information pour éviter de faire correspondre les
caractères que nous savons de toute façon [Link] complexité du cas le plus
défavorable pour rechercher un motif se réduit à O (n) .
Examples
Exemple KMP
Algorithme
Cet algorithme est un processus en deux étapes. Nous créons d'abord un tableau auxiliaire lps []
et utilisons ensuite ce tableau pour rechercher le motif.
Prétraitement :
1. Nous pré-traitons le motif et créons un tableau auxiliaire lps [] qui est utilisé pour ignorer les
caractères lors de la correspondance.
2. Ici, lps [] indique le préfixe correct le plus long qui est également le suffixe. Un préfixe
approprié est le préfixe dans lequel la chaîne entière n'est pas incluse. Par exemple, les
préfixes de la chaîne ABC sont “” , “A” , “AB” et “ABC” . Les préfixes appropriés sont “” ,
“A” et “AB” . Les suffixes de la chaîne sont “” , “C” , “BC” et “ABC” .
Recherche
1. Nous continuons à faire correspondre les caractères txt [i] et pat [j] et continuons à
incrémenter i et j pendant que pat [j] et txt [i] continuent de correspondre.
2. Quand on voit une incompatibilité, on sait que les caractères pat [0..j-1] correspondent à txt
[i-j + 1… i-1] . On sait aussi que lps [j-1] est le nombre de caractères de pat [0… j-1] qui
sont à la fois préfixe et suffixe.À partir de cela, nous pouvons conclure que nous n'avons pas
besoin de faire correspondre ces caractères lps [j-1] avec txt [ij… i-1] car nous savons que
ces caractères va correspondre de toute façon.
Implémentation en Java
[Link] 57
public static void main(String[] args) {
// TODO Auto-generated method stub
String str = "abcabdabc";
String pattern = "abc";
KMP obj = new KMP();
[Link]([Link]([Link](), [Link]()));
}
lps[0] = 0;
int j = 0;
for(int i =1;i<[Link];i++){
if(str[j] == str[i]){
lps[i] = j+1;
j++;
i++;
}else{
if(j!=0){
j = lps[j-1];
}else{
lps[i] = j+1;
i++;
}
}
return lps;
}
if(j==[Link])
return true;
return false;
}
[Link] 58
Chapitre 14: algorithme lié à un temps
polynomial pour la couverture de vertex
minimum
Introduction
Il s’agit d’un algorithme polynomial permettant d’obtenir la couverture de sommet minimum du
graphe non dirigé connecté. La complexité temporelle de cet algorithme est O (n2)
Paramètres
Variable Sens
X Ensemble de sommets
Remarques
La première chose à faire dans cet algorithme consiste à classer tous les sommets du graphe par
ordre décroissant en fonction de leur degré.
Après cela, vous les répétez et ajoutez-les à l'ensemble des sommets finaux qui n'ont aucun
sommet adjacent dans cet ensemble.
Au stade final, itère sur les sommets finaux et supprime tous les sommets dont l'un des sommets
est situé dans cet ensemble.
Examples
Pseudo-code d'algorithme
Entrée connectée G
[Link] 59
Set C <- new Set<Vertex>()
X <- [Link]()
for v in X do
List<Vertex> adjacentVertices1 <- [Link](v)
[Link](v)
for vertex in C do
[Link](vertex)
return C
on peut utiliser un tri par godet pour trier les sommets selon son degré car la valeur
maximale des degrés est (n-1) où n est le nombre de sommets alors la complexité
temporelle du tri sera O (n)
Lire algorithme lié à un temps polynomial pour la couverture de vertex minimum en ligne:
[Link]
couverture-de-vertex-minimum
[Link] 60
Chapitre 15: Algorithmes en ligne
Remarques
Théorie
Définition 1: Un problème d'optimisation of consiste en un ensemble d' instances Σ Π . Pour
tous les cas σ∈Σ Π il existe un ensemble de solutions Ζ σ et une fonction objective f σ: Ζ σ → ℜ ≥0
qui attribue apositive une valeur réelle à chaque solution.
Nous disons que OPT (σ) est la valeur d'une solution optimale, A (σ) est la solution d'un
algorithme A pour le problème Π et w A (σ) = f σ (A (σ)) sa valeur.
pour toutes les instances σ∈Σ Π . A s'appelle un algorithme en ligne r-concurrentiel . Est même
pour toutes les instances σ∈Σ Π alors A est appelé un algorithme en ligne strictement compétitif
.
Preuve: Au début de chaque phase (sauf pour la première), FWF a un cache manquant et a
effacé le cache. cela signifie que nous avons k pages vides. Dans chaque phase, k maximum de
pages différentes sont demandées, il y aura donc maintenant une expulsion pendant la phase.
Donc, FWF est un algorithme de marquage.
Supposons que LRU n'est pas un algorithme de marquage. Ensuite, il y a une instance σ où LRU
une page marquée x dans la phase j'ai expulsé. Soit σ t la requête dans la phase i où x est
expulsé. Puisque x est marqué, il doit y avoir une requête antérieure σ t * pour x dans la même
phase, donc t * <t. Après t * x est la page la plus récente des caches, donc pour être expulsé à la
séquence σ t * + 1 , ..., σ t doit demander au moins k à partir de x pages différentes. Cela implique
que la phase i a demandé au moins k + 1 pages différentes, ce qui est en contradiction avec la
définition de la phase. Donc, LRU doit être un algorithme de marquage.
Preuve: Soit σ une instance pour le problème de pagination et l le nombre de phases pour σ. Is l
= 1 alors tout algorithme de marquage est optimal et l'algorithme hors ligne optimal ne peut être
meilleur.
Nous supposons que l ≥ 2. Le coût de chaque algorithme de marquage, par exemple σ, est limité
par le haut avec l ⋅ k car dans chaque phase un algorithme de marquage ne peut pas expulser plus de k
[Link] 61
pages sans expulser une page marquée.
Nous essayons maintenant de montrer que l’algorithme hors ligne optimal expulse au moins k + l-
2 pages pour σ, k dans la première phase et au moins une pour chaque phase suivante, à
l’exception de la dernière. Pour preuve, définissons l-2 sous-séquences disjointes de σ. La sous-
séquence i ∈ {1, ..., l-2} commence à la deuxième position de la phase i + 1 et se termine à la
première position de la phase i + 2.
Soit x la première page de la phase i + 1. Au début de la sous-séquence, il y a page x et au plus
k-1 pages différentes dans le cache optimal des algorithmes hors ligne. Dans la sous-séquence, la
requête de page k est différente de x, donc l'algorithme hors ligne optimal doit expulser au moins
une page pour chaque sous-séquence. Comme à la phase 1, le cache est toujours vide,
l’algorithme hors ligne optimal provoque des expulsions au cours de la première phase. Cela
montre que
N'y a-t-il pas de constante pour laquelle un algorithme en ligne A est concurrentiel, nous appelons
A non compétitif .
Preuve: Soit l ≥ 2 une constante, k ≥ 2 la taille du cache. Les différentes pages de cache sont
nubered 1, ..., k + 1. Nous regardons la séquence suivante:
La première page 1 est demandée 1 fois plus que la page 2 et ainsi de suite. À la fin, il y a (l-1)
requêtes alternées pour la page k et k + 1.
LFU et LIFO remplissent leur cache avec les pages 1-k. Lorsque la page k + 1 est demandée, la
page k est expulsée et vice versa. Cela signifie que chaque requête de sous-séquence (k, k + 1) l-
1 expulse une page. De plus, leurs caches k-1 manquent pour la première fois en utilisant les
pages 1 (k-1). Ainsi, LFU et LIFO expulsent les pages exactes k-1 + 2 (l-1).
Maintenant, il faut montrer que pour toute constante τ∈ℜ et toute constante r ≤ 1, il existe un l
pour que
Pour satisfaire cette inégalité, il suffit de choisir une taille suffisante. Donc, LFU et LIFO ne sont
pas compétitifs.
Proposition 1.7: Il n'existe pas d' algorithme en ligne déterministe r-compétitif pour la
[Link] 62
pagination avec r <k .
Sources
Matériel de base
Lectures complémentaires
Code source
Examples
Paging (mise en cache en ligne)
Préface
Au lieu de commencer par une définition formelle, l’objectif est d’aborder ces sujets via une
rangée d’exemples, en introduisant des définitions en cours de route. La section remarque La
théorie consistera en toutes les définitions, théorèmes et propositions pour vous donner toutes les
informations nécessaires pour rechercher plus rapidement des aspects spécifiques.
Les sources de la section des remarques comprennent le matériel de base utilisé pour ce sujet et
des informations supplémentaires pour une lecture ultérieure. De plus, vous trouverez les codes
sources complets pour les exemples. Veuillez faire attention que pour rendre le code source des
exemples plus lisible et plus court, il évite des choses comme la gestion des erreurs, etc. Il
transmet également certaines fonctionnalités de langage spécifiques qui masqueraient la clarté de
l'exemple
Pagination
Le problème de pagination provient de la limitation des espaces finis. Supposons que notre cache
C a k pages. Nous souhaitons maintenant traiter une séquence de m pages qui doivent avoir été
placées dans le cache avant d'être traitées. Bien sûr, si m<=k nous mettons simplement tous les
éléments dans le cache et cela fonctionnera, mais habituellement, c'est m>>k .
Nous disons qu'une requête est un succès de cache , lorsque la page est déjà en cache, sinon,
elle est appelée un échec de cache . Dans ce cas, nous devons amener la page demandée dans
[Link] 63
le cache et en expulser un autre, à condition que le cache soit plein. Le but est un calendrier
d'expulsion qui minimise le nombre d'expulsions .
1. Premier entré, premier sorti (FIFO) : la plus ancienne page est expulsée
2. Last in, first out (LIFO) : la page la plus récente est expulsée
3. Moins utilisé récemment (LRU) : page d'éviction dont l'accès le plus récent était le plus
ancien
4. Moins utilisé (LFU) : page d'éviction la moins fréquemment demandée
5. Distance la plus longue (LFD) : expulse la page dans le cache qui n'est demandée que
plus tard.
6. Flush lorsqu'il est plein (FWF) : efface le cache complet dès qu'un échec de cache est
survenu
// no free pages
return 0;
}
[Link] 64
if(cacheMiss && cachePos == 0)
{
for(int i = 1; i < cacheSize; ++i)
cache[i] = emptyPage;
}
}
};
Le code source complet est disponible ici . Si nous réutilisons l'exemple du sujet, nous obtenons
la sortie suivante:
Strategy: FWF
Même si LFD est optimal, FWF a moins de caches manquants. Mais l'objectif principal était de
minimiser le nombre d'expulsions et, pour la FWF, cinq manquants signifiaient 15 expulsions, ce
qui en fait le choix le plus pauvre pour cet exemple.
Approche en ligne
Nous voulons maintenant aborder le problème en ligne de la pagination. Mais nous devons
d'abord comprendre comment le faire. De toute évidence, un algorithme en ligne ne peut pas être
meilleur que l'algorithme hors ligne optimal. Mais à quel point est-ce pire? Nous avons besoin de
définitions formelles pour répondre à cette question:
[Link] 65
compétitif de r ≥ 1 s'il existe une constante τ∈ℜ avec
pour toutes les instances σ∈Σ Π . A s'appelle un algorithme en ligne r-concurrentiel . Est même
pour toutes les instances σ∈Σ Π alors A est appelé un algorithme en ligne strictement compétitif
.
La question est donc de savoir si notre algorithme en ligne est compétitif par rapport à un
algorithme hors ligne optimal. Dans leur célèbre livre, Allan Borodin et Ran El-Yaniv ont utilisé un
autre scénario pour décrire la situation de paging en ligne:
Il y a un adversaire malin qui connaît votre algorithme et l'algorithme hors ligne optimal. À
chaque étape, il essaie de demander une page qui est la pire pour vous et simultanément la
meilleure pour l’algorithme hors ligne. Le facteur de compétitivité de votre algorithme est le
facteur déterminant dans la performance de votre algorithme par rapport à l'algorithme hors ligne
optimal de l'adversaire. Si vous voulez essayer d'être l'adversaire, vous pouvez essayer le jeu de l'
adversaire (essayez de battre les stratégies de pagination).
Algorithmes de marquage
Au lieu d'analyser chaque algorithme séparément, examinons une famille d'algorithmes en ligne
spéciale pour le problème de pagination appelé algorithmes de marquage .
Soit σ = (σ 1 , ..., σ p ) une instance pour notre problème et k notre taille de cache, que σ peut être
divisé en phases:
Un algorithme de marquage (implicite ou explicite) maintient si une page est marquée ou non. Au
début de chaque phase, toutes les pages ne sont pas marquées. Est-ce une page demandée
pendant une phase qui est marquée. Un algorithme est un algorithme de marquage s'il n'est
jamais expulsé d'une page marquée du cache. Cela signifie que les pages utilisées pendant une
phase ne seront pas expulsées.
[Link] 66
Proposition 1.3: LRU et FWF sont des algorithmes de marquage.
Preuve: Au début de chaque phase (sauf pour la première), FWF a un cache manquant et a
effacé le cache. cela signifie que nous avons k pages vides. Dans chaque phase, k maximum de
pages différentes sont demandées, il y aura donc maintenant une expulsion pendant la phase.
Donc, FWF est un algorithme de marquage.
Supposons que LRU ne soit pas un algorithme de marquage. Ensuite, il y a une instance σ où
LRU une page marquée x dans la phase j'ai expulsé. Soit σ t la requête dans la phase i où x est
expulsé. Puisque x est marqué, il doit y avoir une requête antérieure σ t * pour x dans la même
phase, donc t * <t. Après t * x est la page la plus récente des caches, donc pour être expulsé à la
séquence σ t * + 1 , ..., σ t doit demander au moins k à partir de x pages différentes. Cela implique
que la phase i a demandé au moins k + 1 pages différentes, ce qui est en contradiction avec la
définition de la phase. Donc, LRU doit être un algorithme de marquage.
Preuve: Soit σ une instance pour le problème de pagination et l le nombre de phases pour σ. Is l
= 1 alors tout algorithme de marquage est optimal et l'algorithme hors ligne optimal ne peut être
meilleur.
Nous supposons que l ≥ 2. Le coût de chaque algorithme de marquage, par exemple, σ est limité
par le haut avec l ⋅ k car dans chaque phase un algorithme de marquage ne peut pas expulser plus de k
pages sans expulser une page marquée.
Nous essayons maintenant de montrer que l’algorithme hors ligne optimal expulse au moins k + l-
2 pages pour σ, k dans la première phase et au moins une pour chaque phase suivante, à
l’exception de la dernière. Pour preuve, définissons l-2 sous-séquences disjointes de σ. La sous-
séquence i ∈ {1, ..., l-2} commence à la deuxième position de la phase i + 1 et se termine à la
première position de la phase i + 2.
Soit x la première page de la phase i + 1. Au début de la sous-séquence, il y a page x et au plus
k-1 pages différentes dans le cache optimal des algorithmes hors ligne. Dans la sous-séquence, la
requête de page k est différente de x, donc l'algorithme hors ligne optimal doit expulser au moins
une page pour chaque sous-séquence. Comme à la phase 1, le cache est toujours vide,
l’algorithme hors ligne optimal provoque des expulsions au cours de la première phase. Cela
montre que
Exercice: Montrer que le FIFO n’est pas un algorithme de marquage, mais strictement k-
compétitif .
N'y a-t-il pas de constante pour laquelle un algorithme en ligne A est concurrentiel, nous appelons
A non compétitif
Preuve: Soit l ≥ 2 une constante, k ≥ 2 la taille du cache. Les différentes pages de cache sont
[Link] 67
nubered 1, ..., k + 1. Nous regardons la séquence suivante:
La première page 1 est demandée 1 fois plus que la page 2 et ainsi de suite. À la fin, il y a (l-1)
requêtes alternées pour la page k et k + 1.
LFU et LIFO remplissent leur cache avec les pages 1-k. Lorsque la page k + 1 est demandée, la
page k est expulsée et vice versa. Cela signifie que chaque requête de sous-séquence (k, k + 1) l-
1 expulse une page. De plus, leur cache est k-1 pour la première fois en utilisant les pages 1 (k-1).
Ainsi, LFU et LIFO expulsent les pages exactes k-1 + 2 (l-1).
Maintenant, il faut montrer que pour toute constante τ∈ℜ et toute constante r ≤ 1, il existe un l
pour que
Pour satisfaire cette inégalité, il suffit de choisir une taille suffisante. Donc LFU et LIFO ne sont
pas compétitifs.
Proposition 1.7: Il n'existe pas d' algorithme en ligne déterministe r-compétitif pour la
pagination avec r <k .
La preuve de cette dernière proposition est plutôt longue et basée sur l'affirmation que LFD est un
algorithme hors ligne optimal. Le lecteur intéressé peut consulter le livre de Borodin et El-Yaniv
(voir les sources ci-dessous).
La question est de savoir si nous pourrions faire mieux. Pour cela, nous devons laisser l’approche
déterministe derrière nous et commencer à randomiser notre algorithme. De toute évidence, il est
beaucoup plus difficile pour l'adversaire de punir votre algorithme s'il est randomisé.
La pagination aléatoire sera discutée dans l'un des prochains exemples ...
[Link] 68
Chapitre 16: Algorithmes Gourmands
Remarques
Un algorithme glouton est un algorithme dans lequel, à chaque étape, nous choisissons l'option la
plus avantageuse à chaque étape sans regarder l'avenir. Le choix ne dépend que du profit actuel.
Une approche gourmande est généralement une bonne approche lorsque chaque profit peut être
pris en compte à chaque étape, de sorte qu'aucun choix ne bloque un autre.
Examples
Problème de sac à dos continu
Compte tenu des éléments comme (value, weight) nous devons les placer dans un sac à dos
(conteneur) d'une capacité k . Remarque! Nous pouvons casser des articles pour maximiser la
valeur!
Exemple d'entrée:
Production attendue:
maximumValueOfItemsInK = 20;
Algorithme:
Huffman Codage
Le code Huffman est un type particulier de code de préfixe optimal couramment utilisé pour la
[Link] 69
compression de données sans perte. Il compresse très efficacement les données en économisant
de 20% à 90% de mémoire, en fonction des caractéristiques des données compressées. Nous
considérons les données comme une suite de caractères. L'algorithme glouton d'Huffman utilise
un tableau donnant la fréquence à laquelle chaque caractère se produit (c.-à-d. Sa fréquence)
pour constituer une manière optimale de représenter chaque caractère sous la forme d'une chaîne
binaire. Le code de Huffman a été proposé par David A. Huffman en 1951.
Supposons que nous ayons un fichier de données de 100 000 caractères que nous souhaitons
stocker de manière compacte. Nous supposons qu'il n'y a que 6 caractères différents dans ce
fichier. La fréquence des caractères est donnée par:
+------------------------+-----+-----+-----+-----+-----+-----+
| Character | a | b | c | d | e | f |
+------------------------+-----+-----+-----+-----+-----+-----+
|Frequency (in thousands)| 45 | 13 | 12 | 16 | 9 | 5 |
+------------------------+-----+-----+-----+-----+-----+-----+
Nous avons plusieurs options pour représenter un tel fichier d'informations. Ici, nous considérons
le problème de la conception d'un code de caractère binaire dans lequel chaque caractère est
représenté par une chaîne binaire unique, que nous appelons un mot de code .
+------------------------+-----+-----+-----+-----+-----+-----+
| Character | a | b | c | d | e | f |
+------------------------+-----+-----+-----+-----+-----+-----+
| Fixed-length Codeword | 000 | 001 | 010 | 011 | 100 | 101 |
+------------------------+-----+-----+-----+-----+-----+-----+
|Variable-length Codeword| 0 | 101 | 100 | 111 | 1101| 1100|
+------------------------+-----+-----+-----+-----+-----+-----+
[Link] 70
Si nous utilisons un code de longueur fixe , nous avons besoin de trois bits pour représenter 6
caractères. Cette méthode nécessite 300 000 bits pour coder la totalité du fichier. Maintenant, la
question est la suivante: pouvons-nous faire mieux?
Un code de longueur variable peut faire beaucoup mieux qu'un code de longueur fixe, en
donnant aux mots fréquents des mots de code courts et des mots de code longs sans caractère.
Ce code nécessite: (45 X 1 + 13 X 3 + 12 X 3 + 16 X 3 + 9 X 4 + 5 X 4) X 1000 = 224000 bits pour
représenter le fichier, ce qui économise environ 25% de la mémoire.
Une chose à retenir, nous considérons ici uniquement les codes dans lesquels aucun mot de code
n'est également un préfixe d'un autre mot de code. Celles-ci s'appellent des codes de préfixe.
Pour le codage à longueur variable, nous codons le fichier à 3 caractères abc avec la valeur
0.101.100 = 0101100, où "." dénote la concaténation.
Les codes de préfixe sont souhaitables car ils simplifient le décodage. Aucun mot de code n'étant
un préfixe d'un autre, le mot de code qui commence un fichier codé est sans ambiguïté. Nous
pouvons simplement identifier le mot de code initial, le ramener au caractère d'origine et répéter le
processus de décodage sur le reste du fichier codé. Par exemple, 001011101 analyse de manière
unique comme 0.0.101.1101, ce qui décode en aabe . En bref, toutes les combinaisons de
représentations binaires sont uniques. Par exemple, si une lettre est désignée par 110, aucune
autre lettre ne sera désignée par 1101 ou 1100. En effet, vous risquez d’être confus quant à la
sélection de 110 ou à la concaténation du bit suivant et à sa sélection.
Technique de compression:
La technique fonctionne en créant un arbre binaire de noeuds. Celles-ci peuvent être stockées
dans un tableau régulier dont la taille dépend du nombre de symboles, n . Un nœud peut être un
nœud feuille ou un nœud interne . Au départ, tous les nœuds sont des nœuds feuilles, qui
contiennent le symbole lui-même, sa fréquence et, éventuellement, un lien vers ses nœuds
enfants. Par convention, le bit '0' représente l'enfant gauche et le bit '1' représente l'enfant droit.
La file d'attente prioritaire est utilisée pour stocker les nœuds, ce qui fournit au nœud la fréquence
la plus basse lorsqu'il est survolé. Le processus est décrit ci-dessous:
1. Créez un nœud feuille pour chaque symbole et ajoutez-le à la file d'attente prioritaire.
2. Bien qu'il y ait plus d'un nœud dans la file d'attente:
1. Supprimez les deux noeuds les plus prioritaires de la file d'attente.
2. Créez un nouveau nœud interne avec ces deux nœuds en tant qu’enfants et avec une
fréquence égale à la somme de la fréquence des deux nœuds.
3. Ajoutez le nouveau nœud à la file d'attente.
3. Le nœud restant est le nœud racine et l’arbre Huffman est complet.
[Link] 71
Le pseudo-code ressemble à:
Bien que les entrées triées selon le temps linéaire, dans les cas généraux d'entrées arbitraires,
l'utilisation de cet algorithme nécessite un tri préalable. Ainsi, comme le tri prend du temps O
[Link] 72
(nlogn) dans les cas généraux, les deux méthodes ont la même complexité.
Comme n est le nombre de symboles dans l'alphabet, qui est généralement très petit (comparé à
la longueur du message à coder), la complexité temporelle n'est pas très importante dans le choix
de cet algorithme.
Technique de décompression:
Explication gourmande:
Le codage de Huffman examine l'occurrence de chaque caractère et le stocke comme une chaîne
binaire de manière optimale. L'idée est d'affecter des codes de longueur variable aux caractères
d'entrée, la longueur des codes assignés étant basée sur les fréquences des caractères
correspondants. Nous créons un arbre binaire et nous y travaillons de manière ascendante afin
que les deux caractères les plus fréquents soient le plus loin possible de la racine. De cette façon,
le caractère le plus fréquent reçoit le plus petit code et le caractère le moins fréquent reçoit le
code le plus grand.
Les références:
• Introduction aux algorithmes - Charles E. Leiserson, Clifford Stein, Ronald Rivest et Thomas
H. Cormen
• Huffman Coding - Wikipedia
• Mathématiques discrètes et leurs applications - Kenneth H. Rosen
Problème de changement
Étant donné un système monétaire, est-il possible de donner une quantité de pièces et de trouver
[Link] 73
un ensemble minimal de pièces correspondant à ce montant.
Systèmes monétaires canoniques Pour certains systèmes monétaires, comme ceux que nous
utilisons dans la vie réelle, la solution "intuitive" fonctionne parfaitement. Par exemple, si les
différentes pièces et billets en euros (à l'exclusion des centimes) sont de 1 €, 2 €, 5 €, 10 €,
donner la pièce la plus élevée jusqu'à ce que nous atteignions le montant et répéter cette
procédure .
Ces systèmes sont conçus pour faciliter le changement. Le problème devient plus difficile en ce
qui concerne le système monétaire arbitraire.
Cas général Comment donner 99 € avec des pièces de 10 €, 7 € et 5 €? Ici, donner des pièces
de 10 € jusqu'à ce qu'il nous reste 9 € conduit évidemment à aucune solution. Pire que cela, une
solution pourrait ne pas exister. Ce problème est en fait np-hard, mais des solutions acceptables
combinant la gourmandise et la mémorisation existent. L'idée est d'explorer toutes les
possibilités et de choisir celle avec le nombre minimal de pièces.
Pour donner un montant X> 0, nous choisissons une pièce P dans le système monétaire, puis
nous résolvons le sous-problème correspondant à XP. Nous essayons ceci pour toutes les pièces
du système. La solution, si elle existe, est alors le plus petit chemin qui mène à 0.
Voici une fonction récursive OCaml correspondant à cette méthode. Il renvoie Aucun si aucune
solution n'existe.
(* option utilities *)
let optmin x y =
match x,y with
| None,a | a,None -> a
| Some x, Some y-> Some (min x y)
(* Change-making problem*)
let change_make money_system amount =
let rec loop n =
let onepiece acc piece =
match n - piece with
| 0 -> (*problem solved with one coin*)
Some 1
| x -> if x < 0 then
[Link] 74
(*we don't reach 0, we discard this solution*)
None
else
(*we search the smallest path different to None with the remaining pieces*)
optmin (optsucc (loop x)) acc
in
(*we call onepiece forall the pieces*)
List.fold_left onepiece None money_system
in loop amount
Note : Nous pouvons remarquer que cette procédure peut calculer plusieurs fois le changement
défini pour la même valeur. En pratique, l'utilisation de la mémorisation pour éviter ces répétitions
conduit à des résultats plus rapides (beaucoup plus rapides).
Le problème
Vous avez un ensemble de choses à faire (activités). Chaque activité a une heure de début et une
heure de fin. Vous n'êtes pas autorisé à effectuer plus d'une activité à la fois. Votre tâche consiste
à trouver un moyen d'effectuer le maximum d'activités.
Par exemple, supposons que vous ayez une sélection de classes parmi lesquelles choisir.
1 10h20 11h00
2 10h30 11h30
3 11h00 12h00
4 10h00 11h30
5 9h00 11h00
N'oubliez pas que vous ne pouvez pas suivre deux cours en même temps. Cela signifie que vous
ne pouvez pas prendre les classes 1 et 2 car elles partagent un temps commun 10h30 à 11h00.
Cependant, vous pouvez prendre les classes 1 et 3 car elles ne partagent pas une heure
commune. Ainsi, votre tâche consiste à prendre le plus grand nombre possible de classes sans
aucun chevauchement. Comment peux-tu faire ça?
Une analyse
Pensons à la solution par une approche gourmande. Tout d'abord, nous avons choisi au hasard
une approche et une vérification qui fonctionneront ou non.
• triez l’activité par heure de début, c’est-à-dire quelle activité commence en premier, nous
la prendrons en premier. puis prenez la première de la liste triée et vérifiez qu'elle se
[Link] 75
recoupe ou non de l'activité précédente. Si l'activité en cours ne recoupe pas l'activité
précédemment prise, nous effectuerons l'activité sinon nous ne réaliserons pas. cette
approche fonctionnera pour certains cas comme
1 11h00 13h30
2 11h30 12h00
3 13h30 14h00
4 10h00 11h00
l'ordre de tri sera 4 -> 1 -> 2 -> 3. L'activité 4 -> 1 -> 3 sera effectuée et l'activité 2 sera ignorée. 3
activités maximum seront effectuées. Cela fonctionne pour ce type de cas. mais cela échouera
dans certains cas. Permet d'appliquer cette approche pour le cas
1 11h00 13h30
2 11h30 12h00
3 13h30 14h00
4 10h00 15h00
L'ordre de tri sera 4 -> 1 -> 2 -> 3 et seule l'activité 4 sera effectuée mais la réponse peut être
l'activité 1 -> 3 ou 2 -> 3 sera effectuée. Donc, notre approche ne fonctionnera pas pour le cas ci-
dessus. Essayons une autre approche
• Triez l’activité par durée, c’est-à-dire effectuez d’abord l’activité la plus courte. cela peut
résoudre le problème précédent. Bien que le problème ne soit pas complètement résolu. Il y
a encore des cas qui peuvent faire échouer la solution. appliquer cette approche sur le cas
ci-dessous.
1 6h00 11h40
2 11h30 12h00
3 23h40 14h00
Si nous trions l'activité par durée, l'ordre de tri sera 2 -> 3 ---> 1. et si nous effectuons l'activité n °
2 en premier, aucune autre activité ne peut être effectuée. Mais la réponse sera effectuer l'activité
1 puis effectuer 3. Donc, nous pouvons effectuer un maximum d'activité 2. Cela ne peut donc pas
[Link] 76
être une solution à ce problème. Nous devrions essayer une approche différente.
La solution
• Triez l'activité en terminant l'heure, ce qui signifie que l'activité se termine en premier et
en premier. l'algorithme est donné ci-dessous
1 10h20 11h00
2 10h30 11h30
3 11h00 12h00
4 10h00 11h30
5 9h00 11h00
trier l'activité par ses heures de fin, donc l'ordre de tri sera 1 -> 5 -> 2 -> 4 -> 3 .. la réponse est 1 -
> 3 ces deux activités seront effectuées. c'est la réponse. voici le code sudo.
1. trier: activités
2. effectuer la première activité à partir de la liste triée d'activités.
3. Set: Current_activity: = première activité
4. set: end_time: = end_time de l'activité en cours
5. aller à l'activité suivante s'il existe, s'il n'existe pas terminer
6. si start_time de l'activité en cours <= end_time: effectue l'activité et passe à 4
7. sinon: a 5.
[Link] 77
Chapitre 17: Algorithmes multithread
Introduction
Exemples pour certains algorithmes multithread.
Syntaxe
• parallel before a loop signifie que chaque itération de la boucle est indépendante et peut
être exécutée en parallèle.
• spawn est d'indiquer la création d'un nouveau thread.
• sync consiste à synchroniser tous les threads créés.
• Les tableaux / matrice sont indexés de 1 à n dans les exemples.
Examples
Multithread à multiplication carrée
multiply-square-matrix-parallel(A, B)
n = [Link]
C = Matrix(n,n) //create a new matrix n*n
parallel for i = 1 to n
parallel for j = 1 to n
C[i][j] = 0
pour k = 1 to n
C[i][j] = C[i][j] + A[i][k]*B[k][j]
return C
matrix-vector(A,x)
n = [Link]
y = Vector(n) //create a new vector of length n
parallel for i = 1 to n
y[i] = 0
parallel for i = 1 to n
for j = 1 to n
y[i] = y[i] + A[i][j]*x[j]
return y
fusionner-trier multithread
A est un tableau et les index p et q du tableau, comme vous allez trier le sous-tableau A [p..r] . B
est un sous-tableau qui sera rempli par le tri.
Un appel à p-merge-sort (A, p, r, B, s) trie les éléments de A [p..r] et les place dans B [s..s + rp] .
[Link] 78
p-merge-sort(A,p,r,B,s)
n = r-p+1
if n==1
B[s] = A[p]
else
T = new Array(n) //create a new array T of size 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)
sync
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
if n1<n2 //check if n1>=n2
permute p1 and p2
permute r1 and r2
permute n1 and n2
if n1==0 //both empty?
return
else
q1 = floor((p1+r1)/2)
q2 = dichotomic-search(T[q1],T,p2,r2)
q3 = p3 + (q1-p1) + (q2-p2)
A[q3] = T[q1]
spawn p-merge(T,p1,q1-1,p2,q2-1,A,p3)
p-merge(T,q1+1,r1,q2,r2,A,q3+1)
sync
dichotomic-search(x,T,p,r)
inf = p
sup = max(p,r+1)
while inf<sup
half = floor((inf+sup)/2)
if x<=T[half]
sup = half
else
inf = half+1
return sup
[Link] 79
Chapitre 18: Ancêtre commun le plus bas
d'un arbre binaire
Introduction
L'ancêtre commun le plus bas entre deux nœuds n1 et n2 est défini comme le nœud le plus bas
de l'arborescence ayant à la fois n1 et n2 comme descendants.
Examples
Trouver le plus petit ancêtre commun
Considérez l'arbre:
[Link] 80
Chapitre 19: Applications de la technique
gourmande
Remarques
Sources
1. Les exemples ci-dessus sont tirés de notes de cours d'un cours donné en 2008 à Bonn, en
Allemagne. En terme, ils sont basés sur le livre Algorithm Design de Jon Kleinberg et Eva
Tardos:
Examples
Automate à billet
Vous avez un automate de ticket qui échange des pièces de monnaie avec les valeurs 1, 2, 5, 10
et 20. La répartition de l'échange peut être vue comme une série de pièces tombant jusqu'à ce
que la bonne valeur soit distribuée. Nous disons qu'une dissension est optimale lorsque son
nombre de pièces est minimal pour sa valeur.
Soit M dans [1,50] le prix du ticket T et P dans [1,50] l'argent que quelqu'un a payé pour T , avec P
>= M Soit D=PM . Nous définissons l' avantage d'une étape comme la différence entre D et Dc avec c
la pièce que l'automate distribue dans cette étape.
Ensuite, la somme de toutes les pièces est clairement égale à D C'est un algorithme glouton car
après chaque étape et après chaque répétition d'une étape, l'avantage est maximisé. Nous ne
pouvons pas dispenser une autre pièce avec un avantage plus élevé.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
[Link] 81
// read some coin values, sort them descending,
// purge copies and guaratee the 1 coin is in it
std::vector<unsigned int> readInCoinValues();
int main()
{
std::vector<unsigned int> coinValues; // Array of coin values ascending
int ticketPrice; // M in example
int paidMoney; // P in example
coinCount.push_back(countCoins);
}
return 0;
}
[Link] 82
std::vector<unsigned int> readInCoinValues()
{
// coin values
std::vector<unsigned int> coinValues;
if(coinValue > 0)
coinValues.push_back(coinValue);
else
break;
}
// sort values
sort([Link](), [Link](), std::greater<int>());
// print array
cout << "Coin values: ";
for(auto i : coinValues)
cout << i << " ";
return coinValues;
}
Sachez qu'il y a maintenant une vérification des entrées pour garder l'exemple simple. Un
exemple de sortie:
[Link] 83
Tant que 1 est dans les valeurs de pièce, nous avons maintenant, que l'algorithme se terminera,
car:
1. Soit C la plus grande valeur de pièce. Le runtime n'est que polynomial tant que D/C est
polynomial, car la représentation de D n'utilise que log D bits log D et le runtime est au moins
linéaire en D/C
2. À chaque étape, notre algorithme choisit l'optimum local. Mais cela ne suffit pas pour dire
que l'algorithme trouve la solution optimale globale (voir plus d'informations ici ou dans le
Livre de Korte et de Vygen ).
Un exemple simple de compteur: les pièces sont 1,3,4 et D=6 . La solution optimale est clairement
de deux pièces de valeur 3 mais la gourmande choisit 4 dans la première étape, elle doit donc
choisir 1 à l'étape deux et trois. Donc, cela ne donne aucune soution optimale. Un algorithme
optimal possible pour cet exemple est basé sur la programmation dynamique .
Planification d'intervalle
Nous avons un ensemble de travaux J={a,b,c,d,e,f,g} . Soit j in J un travail que son début en sj
et se termine en fj . Deux tâches sont compatibles si elles ne se chevauchent pas. Une image
comme exemple: Le but est de trouver le sous-ensemble maximum de travaux compatibles
entre eux . Il existe plusieurs approches gourmandes pour ce problème:
1. Heure de début au plus tôt : considérez les travaux dans l'ordre croissant de sj
2. Heure de fin la plus proche : considérez les travaux dans l'ordre croissant de fj
3. Intervalle le plus court : considérez les travaux dans l'ordre croissant de fj-sj
4. Moins de conflits : pour chaque emploi j , compter le nombre de tâches en conflit cj
La question est maintenant, quelle approche est vraiment réussie. Heure de début précoce
définitivement pas, voici un contre-exemple L'intervalle le plus court n'est pas optimal non plus
et les conflits les moins nombreux peuvent en effet sembler optimaux, mais voici un cas
problématique pour cette approche: Ce qui nous laisse le plus tôt possible . Le pseudo-code est
simple:
#include <iostream>
#include <utility>
#include <tuple>
#include <vector>
[Link] 84
#include <algorithm>
int main()
{
vector<pair<int,int>> jobs;
// step 1: sort
sort([Link](), [Link](),[](pair<int,int> p1, pair<int,int> p2)
{ return [Link] < [Link]; });
// step 3:
for(int i=0; i<jobCnt; ++i)
{
auto job = jobs[i];
bool isCompatible = true;
for(auto jobIndex : A)
{
// test whether the actual job and the job from A are incompatible
if([Link] >= jobs[jobIndex].first &&
[Link] <= jobs[jobIndex].second)
{
isCompatible = false;
break;
}
}
if(isCompatible)
A.push_back(i);
}
//step 4: print A
cout << "Compatible: ";
for(auto i : A)
cout << "(" << jobs[i].first << "," << jobs[i].second << ") ";
cout << endl;
return 0;
}
La sortie pour cet exemple est: Compatible: (1,3) (4,5) (6,8) (9,10)
[Link] 85
log n) et le lecteur intéressé peut continuer à lire ci-dessous (exemple Java).
Maintenant, nous avons un algorithme glouton pour le problème de planification des intervalles,
mais est-ce optimal?
Supposons que greedy n'est pas optimal et que i1,i2,...,ik désignent l'ensemble des jobs
sélectionnés par les gourmands. Soit j1,j2,...,jm l'ensemble des travaux dans une solution
optimale avec i1=j1,i2=j2,...,ir=jr pour la plus grande valeur possible de r .
Le travail i(r+1) existe et se termine avant j(r+1) (première arrivée). Mais que sont
j1,j2,...,jr,i(r+1),j(r+2),...,jm aussi une solution optimale et pour tout k dans [1,(r+1)] est
jk=ik . thats une contradiction à la maximalité de r . Ceci conclut la preuve.
import [Link];
import [Link];
class Job
{
int start, finish, profit;
[Link] 86
if (jobs[mid].finish <= jobs[index].start)
{
if (jobs[mid + 1].finish <= jobs[index].start)
lo = mid + 1;
else
return mid;
}
else
hi = mid - 1;
}
return -1;
}
int n = [Link];
int table[] = new int[n];
table[0] = jobs[0].profit;
return table[n-1];
}
Il existe de nombreux problèmes minimisant le retard, nous avons ici une seule ressource qui ne
peut traiter qu'un seul travail à la fois. La tâche j nécessite tj unités de temps de traitement et est
due à l'instant dj . si j commence à l'instant sj il finira à l'instant fj=sj+tj . Nous définissons le
retard L=max{0,fj-dh} pour tout j . Le but est de minimiser le retard maximal L.
[Link] 87
1 2 3 4 5 6
tj 3 2 1 4 3 2
dj 6 8 9 9 dix 11
Emploi 3 2 2 5 5 5 4 4 4 4 1 1 1 6 6
Temps 1 2 3 4 5 6 7 8 9 dix 11 12 13 14 15
Lj -8 -5 -4 1 7 4
La solution L=7 n'est évidemment pas optimale. Regardons quelques stratégies gourmandes:
1. Temps de traitement le plus court en premier : planifier les travaux dans l'ordre croissant
og temps de traitement j`
2. Première date limite en premier : Planifier les travaux dans l'ordre croissant des délais dj
3. Le plus petit jeu : planifier les travaux en ordre croissant de jeu dj-tj
Il est facile de voir que le temps de traitement le plus court n’est pas optimal.
1 2
tj 1 5
dj dix 5
1 2
tj 1 5
dj 3 5
#include <iostream>
#include <utility>
[Link] 88
#include <tuple>
#include <vector>
#include <algorithm>
int main()
{
vector<pair<int,int>> jobs;
// step 1: sort
sort([Link](), [Link](),[](pair<int,int> p1, pair<int,int> p2)
{ return [Link] < [Link]; });
// step 3:
vector<pair<int,int>> jobIntervals;
int lateness = 0;
cout << "(" << [Link] << "," << [Link] << ") "
<< "Lateness: " << [Link]-jobs[i].second << std::endl;
}
return 0;
}
Intervals:
[Link] 89
(0,2) Lateness:-2
(2,5) Lateness:-2
(5,8) Lateness: 0
(8,9) Lateness: 0
(9,12) Lateness: 3
(12,17) Lateness: 6
(17,21) Lateness: 8
(21,23) Lateness: 6
(23,25) Lateness: 3
(25,26) Lateness: 1
maximal lateness is 8
Le temps d'exécution de l'algorithme est évidemment Θ (n log n) car le tri est l'opération
dominante de cet algorithme. Maintenant, nous devons montrer que c'est optimal. Clairement, un
horaire optimal n'a pas de temps d'inactivité . la première échéance n'a pas non plus de temps
mort.
Supposons que les travaux sont numérotés de sorte que d1<=d2<=...<=dn . Nous disons qu'une
inversion d'une planification est une paire de travaux i et j sorte que i<j mais j est planifié avant
i . En raison de sa définition, la première échéance n’a pas d’inversion. Bien sûr, si un
programme a une inversion, il en a un avec une paire de travaux inversés programmés
consécutivement.
Proposition: L' échange de deux travaux inversés adjacents réduit le nombre d'inversions de un
et n'augmente pas le retard maximal.
Preuve: Soit L le retard avant le swap et M le retard après. Comme l'échange de deux travaux
adjacents ne déplace pas les autres travaux de leur position, c'est Lk=Mk pour tout k != i,j .
De toute évidence , il est Mi<=Li depuis travail i suis programmé plus tôt. si le travail j est en
retard, il découle de la définition:
Mj = fi-dj (definition)
<= fi-di (since i and j are exchanged)
<= Li
Cela signifie que le retard après l’échange est inférieur ou égal à celui d’avant. Ceci conclut la
preuve.
Supposons que S* est le programme optimal avec le plus petit nombre possible d'inversions.
nous pouvons supposer que S* n'a pas de temps d'inactivité. Si S* n'a pas d'inversions, alors S=S*
et nous avons terminé. Si S* a une inversion, il a une inversion adjacente. La dernière proposition
indique que nous pouvons échanger l'inversion adjacente sans augmenter les retards mais en
diminuant le nombre d'inversions. Ceci contredit la définition de S* .
[Link] 90
Le problème de la minimisation des retards et son problème de makespan minimum proche, où
la question d'un calendrier minimal est posée, ont beaucoup d'applications dans le monde réel.
Mais généralement, vous n'avez pas une seule machine, mais beaucoup d'entre elles et elles
gèrent la même tâche à des rythmes différents. Ces problèmes font que NP-complete est vraiment
rapide.
Une autre question intéressante se pose si nous ne regardons pas le problème hors ligne , où
nous avons toutes les tâches et les données à portée de main, mais dans la variante en ligne , où
les tâches apparaissent pendant l'exécution.
Le problème de mise en cache provient de la limitation de l'espace fini. Supposons que notre
cache C a k pages. Maintenant, nous voulons traiter une séquence de m requêtes qui doivent avoir
été placées dans le cache avant d'être traitées. Bien sûr, si m<=k nous mettons simplement tous les
éléments dans le cache et cela fonctionnera, mais habituellement, c'est m>>k .
Nous disons qu'une requête est un succès de cache , lorsque l'élément est déjà en cache, sinon
il est appelé un échec de cache . Dans ce cas, nous devons mettre l'élément demandé en cache
et en expulser un autre, à condition que le cache soit plein. L'objectif est un calendrier d'expulsion
qui minimise le nombre d'expulsions .
1. Premier entré, premier sorti (FIFO) : la plus ancienne page est expulsée
2. Last in, first out (LIFO) : la page la plus récente est expulsée
3. Dernière sortie récente (LRU) : page d'éviction dont l'accès le plus récent était le plus
ancien
4. Moins fréquemment demandé (LFU) : page d'éviction la moins fréquemment demandée
5. Distance la plus longue (LFD) : expulse la page dans le cache qui n'est demandée que
plus tard.
Attention: Pour les exemples suivants, nous expulsons la page avec le plus petit index, si
plusieurs pages peuvent être expulsées.
Exemple (FIFO)
cache 2 b b b e e e e c c c e e e b b b
miss
X X X X X X X X X X X X X
cache
[Link] 91
Treize caches manquent par seize requêtes ne sonnent pas très optimales, essayons le même
exemple avec une autre stratégie:
Exemple (LFD)
cache 3 c c c c c c c c F ré ré ré ré b b b
miss
X X X X X X X X
cache
Selftest : Faites l'exemple pour LIFO, LFU, RFU et regardez ce qui s'est passé.
Le squelette est une application qui résout le problème en fonction de la stratégie gourmande
choisie:
#include <iostream>
#include <memory>
// for reset
char originalCache[] = {'a','b','c'};
class Strategy {
public:
Strategy(std::string name) : strategyName(name) {}
virtual ~Strategy() = default;
[Link] 92
const std::string strategyName;
};
// write to cache
cache[cachePlace] = request[requestIndex];
return isMiss;
}
int main()
{
Strategy* selectedStrategy[] = { new FIFO, new LIFO, new LRU, new LFU, new LFD };
int cntMisses = 0;
L'idée de base est simple: pour chaque demande, j'ai deux appels deux ma stratégie:
[Link] 93
2. update : une fois que l'appelant a utilisé le lieu, il indique à la stratégie si elle a échoué ou
non. La stratégie peut alors mettre à jour ses données internes. La stratégie LFU, par
exemple, doit mettre à jour la fréquence des accès aux pages de cache, tandis que la
stratégie LFD doit recalculer les distances des pages de cache.
FIFO
return oldest;
}
else
age[i] = 0;
}
}
private:
int age[cacheSize];
};
FIFO a juste besoin de savoir depuis combien de temps une page est dans le cache (et bien sûr
seulement par rapport aux autres pages). Donc, la seule chose à faire est d'attendre un échec,
puis de faire les pages qui ne sont pas expulsées plus anciennes. Pour notre exemple ci-dessus,
la solution du programme est la suivante:
[Link] 94
Strategy: FIFO
LIFO
return newest;
}
[Link] 95
{
if(i != cachePos)
age[i]++;
else
age[i] = 0;
}
}
private:
int age[cacheSize];
};
La mise en œuvre du LIFO est plus ou moins la même que celle du FIFO mais nous expulsons la
page la plus jeune et non la plus ancienne. Les résultats du programme sont les suivants:
Strategy: LIFO
LRU
[Link] 96
else if(age[i] > age[oldest])
oldest = i;
}
return oldest;
}
else
age[i] = 0;
}
}
private:
int age[cacheSize];
};
Dans le cas d'une LRU, la stratégie est indépendante de ce qui se trouve sur la page cache, son
seul intérêt est la dernière utilisation. Les résultats du programme sont les suivants:
Strategy: LRU
LFU
[Link] 97
for (int i=0; i<cacheSize; ++i) requestFrequency[i] = 0;
}
return least;
}
else
++requestFrequency[cachePos];
}
private:
LFU expulse la page utilisée le moins souvent. La stratégie de mise à jour consiste simplement à
compter chaque accès. Bien sûr, après un échec, le compte se réinitialise. Les résultats du
programme sont les suivants:
Strategy: LFU
[Link] 98
Total cache misses: 10
LFD
return latest;
}
private:
return requestLength + 1;
}
La stratégie LFD est différente de tout le monde auparavant. C'est la seule stratégie qui utilise les
demandes futures pour sa décision d'expulsion. L'implémentation utilise la fonction calcNextUse
pour obtenir la page la plus éloignée dans le futur. La solution du programme est égale à la
solution par le haut:
Strategy: LFD
[Link] 99
Cache initial: (a,b,c)
La stratégie gourmande LFD est en effet la seule stratégie optimale des cinq présentées. La
preuve est assez longue et peut être trouvée ici ou dans le livre de Jon Kleinberg et Eva Tardos
(voir les sources dans les remarques ci-dessous).
Algorithme vs réalité
La stratégie LFD est optimale, mais il y a un gros problème. C'est une solution hors ligne
optimale. Dans la praxis, la mise en cache est généralement un problème en ligne , ce qui signifie
que la stratégie est inutile car nous ne pouvons pas le faire la prochaine fois que nous avons
besoin d'un élément particulier. Les quatre autres stratégies sont également des stratégies en
ligne . Pour les problèmes en ligne, nous avons besoin d'une approche générale différente.
[Link] 100
Chapitre 20: Applications de programmation
dynamique
Introduction
L'idée de base de la programmation dynamique consiste à casser un problème complexe en
plusieurs petits problèmes simples qui se répètent. Si vous pouvez identifier un sous-problème
simple, calculé de manière répétée, il existe une approche dynamique du problème.
Remarques
Définitions
Mémo - technique d'optimisation utilisée principalement pour accélérer les programmes
informatiques en stockant les résultats d'appels de fonctions coûteux et en renvoyant le résultat
en cache lorsque les mêmes entrées se reproduisent.
Examples
Numéros de Fibonacci
Les nombres de Fibonacci sont un sujet de choix pour la programmation dynamique car
l'approche récursive traditionnelle fait beaucoup de calculs répétés. Dans ces exemples, j'utiliserai
le cas de base de f(0) = f(1) = 1 .
Voici un exemple d'arbre récursif pour fibonacci(4) , notez les calculs répétés:
[Link] 101
Programmation non dynamique O(2^n) Complexité d'exécution, O(n) Complexité de la pile
def fibonacci(n):
if n < 2:
return 1
return fibonacci(n-1) + fibonacci(n-2)
C'est la manière la plus intuitive d'écrire le problème. Tout au plus, l’espace de la pile sera O(n)
lorsque vous descendez la première branche récursive en faisant des appels à fibonacci(n-1)
jusqu’à ce que vous atteigniez le cas de base n < 2 .
La preuve de complexité d'exécution O(2^n) que l'on peut voir ici: la complexité informatique de la
séquence de Fibonacci . Le point principal à noter est que l’exécution est exponentielle, ce qui
signifie que le temps d’exécution doublera pour chaque terme suivant, fibonacci(15) prendra deux
fois plus de temps que fibonacci(14) .
Memoized O(n) Runtime Complexité, O(n) Complexité de l'espace, O(n) Complexité de la pile
memo = []
[Link](1) # f(1) = 1
[Link](1) # f(2) = 1
def fibonacci(n):
if len(memo) > n:
return memo[n]
Avec l'approche mémo, nous introduisons un tableau qui peut être considéré comme tous les
appels de fonction précédents. Le memo[n] localisation memo[n] est le résultat de l'appel de fonction
fibonacci(n) . Cela nous permet d'échanger la complexité d'espace de O(n) pour un runtime O(n)
car nous n'avons plus besoin de calculer les appels de fonction en double.
[Link] 102
Programmation dynamique itérative O(n) Complexité d'exécution, O(n) Complexité spatiale, Pas
de pile récursive
def fibonacci(n):
memo = [1,1] # f(0) = 1, f(1) = 1
return memo[n]
Si nous divisons le problème en éléments centraux, vous remarquerez que pour calculer
fibonacci(n) nous avons besoin de fibonacci(n-1) et de fibonacci(n-2) . Nous pouvons également
remarquer que notre cas de base apparaîtra à la fin de cet arbre récursif, comme indiqué ci-
dessus.
Avec cette information, il est maintenant logique de calculer la solution à rebours, en commençant
par les cas de base et en remontant. Maintenant, pour calculer fibonacci(n) nous calculons
d'abord tous les nombres de fibonacci jusqu'à et par n .
Cet avantage principal est que nous avons maintenant éliminé la pile récursive tout en conservant
le runtime O(n) . Malheureusement, nous avons toujours une complexité d'espace O(n) mais cela
peut aussi être changé.
def fibonacci(n):
memo = [1,1] # f(1) = 1, f(2) = 1
return memo[n%2]
Pour stocker ces 2 derniers résultats, j'utilise un tableau de taille 2 et je retourne simplement
l'index auquel je l'affecte en utilisant i % 2 qui alternera comme suit: 0, 1, 0, 1, 0, 1, ..., i % 2 .
J'ajoute les deux index du tableau car nous savons que l'addition est commutative ( 5 + 6 = 11 et 6
+ 5 == 11 ). Le résultat est ensuite attribué à la plus ancienne des deux taches (notée i % 2 ). Le
résultat final est ensuite stocké à la position n%2
[Link] 103
Remarques
• Il est important de noter que parfois, il peut être préférable de proposer une solution
mémorisée itérative pour les fonctions qui effectuent des calculs importants à plusieurs
reprises, car vous construirez un cache de la réponse aux appels de fonction et les appels
suivants pourront être O(1) si il a déjà été calculé.
[Link] 104
Chapitre 21: Arbres de recherche binaire
Introduction
L'arbre binaire est un arbre que chaque nœud contient au maximum deux enfants. L'arbre de
recherche binaire (BST) est un arbre binaire dont les éléments sont positionnés dans un ordre
spécial. Dans chaque BST, toutes les valeurs (c'est-à-dire la clé) dans le sous-arbre gauche sont
inférieures aux valeurs dans le sous-arbre droit.
Examples
Arbre de recherche binaire - Insertion (Python)
Ceci est une implémentation simple de l'insertion d'arbre de recherche binaire utilisant Python.
Suivant l'extrait de code, chaque image montre la visualisation de l'exécution, ce qui facilite la
visualisation de son fonctionnement.
class Node:
def __init__(self, val):
self.l_child = None
self.r_child = None
[Link] = val
[Link] 105
def insert(root, node):
if root is None:
root = node
else:
if [Link] > [Link]:
if root.l_child is None:
root.l_child = node
else:
insert(root.l_child, node)
else:
if root.r_child is None:
root.r_child = node
else:
insert(root.r_child, node)
def in_order_print(root):
if not root:
return
in_order_print(root.l_child)
print [Link]
in_order_print(root.r_child)
def pre_order_print(root):
if not root:
return
print [Link]
pre_order_print(root.l_child)
pre_order_print(root.r_child)
[Link] 106
Arbre de recherche binaire - Suppression (C ++)
Avant de commencer avec la suppression, je veux juste mettre quelques lumières sur ce qu'est un
arbre de recherche binaire (BST). Chaque nœud dans un fichier BST peut avoir au maximum
deux nœuds (enfant gauche et droit). Le sous-arbre gauche d'un nœud a un clé inférieure ou
égale à la clé de son nœud parent. Le sous-arbre droit d'un nœud a une clé plus grande que la clé
de son nœud parent.
Suppression d'un nœud dans une arborescence tout en conservant sa propriété d'arborescence
de recherche binaire.
• Cas 1: Le nœud à supprimer est le nœud feuille (nœud avec la valeur 22).
• Cas 2: le nœud à supprimer a un enfant (nœud avec la valeur 26).
• Cas 3: le nœud à supprimer comporte les deux enfants (nœud avec la valeur 49).
[Link] 107
2. Lorsqu'un noeud à supprimer ne possède qu'un seul enfant, copiez la valeur enfant dans la
valeur du noeud et supprimez le fils (converti en cas 1).
3. Lorsqu'un nœud à supprimer possède deux enfants, le minimum de son sous-arbre droit
peut être copié sur le nœud, puis la valeur minimale peut être supprimée du sous-arbre droit
du nœud (converti en cas 2).
struct node
{
int data;
node *left, *right;
};
else
{
if(root->left == nullptr && root->right == nullptr) // Case 1
{
free(root);
root = nullptr;
}
else if(root->left == nullptr) // Case 2
{
node* temp = root;
root= root->right;
free(temp);
}
else if(root->right == nullptr) // Case 2
{
node* temp = root;
root = root->left;
free(temp);
}
else // Case 3
{
node* temp = root->right;
root->data = temp->data;
root->right = delete_node(root->right, temp->data);
}
}
return root;
}
[Link] 108
Ancêtre commun le plus bas dans un BST
Considérons le BST:
La propriété d'arbre de recherche binaire peut être utilisée pour trouver l'ancêtre le plus bas des
noeuds
Code Psuedo:
lowestCommonAncestor(root,node1, node2){
if(root == NULL)
return NULL;
return root;
}
else {
return lowestCommonAncestor(root->right, node1, node2);
}
}
[Link] 109
Arbre de recherche binaire - Python
class Node(object):
def __init__(self, val):
self.l_child = None
self.r_child = None
[Link] = val
class BinarySearchTree(object):
def insert(self, root, node):
if root is None:
return node
return root
r = Node(3)
node = BinarySearchTree()
nodeList = [1, 8, 5, 12, 14, 6, 15, 7, 16, 8]
for nd in nodeList:
[Link](r, Node(nd))
[Link] 110
print "------Post order ---------"
print (node.post_order_place(r))
[Link] 111
Chapitre 22: Complexité de l'algorithme
Remarques
Tous les algorithmes sont une liste d'étapes pour résoudre un problème. Chaque étape a des
dépendances sur un ensemble d'étapes précédentes ou le début de l'algorithme. Un petit
problème peut ressembler à ceci:
Cette structure est appelée graphe acyclique dirigé, ou DAG en abrégé. Les liens entre chaque
nœud du graphique représentent des dépendances dans l'ordre des opérations et il n'y a pas de
cycle dans le graphique.
total = 0
for(i = 1; i < 10; i++)
total = total + i
[Link] 112
Si vous comprenez cette représentation des algorithmes, vous pouvez l'utiliser pour comprendre
la complexité de l'algorithme en termes de travail et de portée.
Travail
Work est le nombre réel d'opérations à exécuter pour atteindre l'objectif de l'algorithme pour une
taille d'entrée donnée n .
Envergure
La portée est parfois appelée le chemin critique et est le plus petit nombre d'étapes qu'un
algorithme doit effectuer pour atteindre l'objectif de l'algorithme.
L'image suivante met en évidence le graphique pour montrer les différences entre le travail et la
durée sur notre échantillon DAG.
Le travail est le nombre de nœuds dans le graphique dans son ensemble. Ceci est représenté par
le graphique ci-dessus à gauche. La portée est le chemin critique ou le plus long chemin du début
à la fin. Lorsque le travail peut être effectué en parallèle, les nœuds surlignés en jaune à droite
représentent la portée, le moins d’étapes requises. Lorsque le travail doit être effectué en série, la
durée est la même que le travail.
Le travail et l'étendue peuvent être évalués indépendamment en termes d'analyse. La vitesse d'un
algorithme est déterminée par la durée. La quantité de puissance de calcul requise est déterminée
par le travail.
[Link] 113
Examples
Notation Big Theta
Contrairement à la notation Big-O, qui ne représente que la limite supérieure du temps d'exécution
pour certains algorithmes, Big-Theta est une limite étroite; les deux limites supérieure et inférieure.
La limite étroite est plus précise, mais aussi plus difficile à calculer.
Une manière intuitive de le comprendre est que f(x) = Ө(g(x)) signifie que les graphes de f (x) et
g (x) croissent au même rythme, ou que les graphes se comportent de la même manière pour les
grands suffisamment de valeurs de x.
Un exemple
Si l'algorithme pour l'entrée n prend 42n^2 + 25n + 4 opérations pour finir, on dit que c'est O(n^2) ,
mais c'est aussi O(n^3) et O(n^100) . Cependant, c'est Ө(n^2) et ce n'est pas not Ө(n^3) , Ө(n^4) etc.
L'algorithme qui est Ө(f(n)) est aussi O(f(n)) , mais non l'inverse!
Ө(g(x)) = {f(x) such that there exist positive constants c1, c2, N such that 0 <= c1*g(x) <=
f(x) <= c2*g(x) for all x > N}
Comme Ө(g(x)) est un ensemble, nous pourrions écrire f(x) ∈ Ө(g(x)) pour indiquer que f(x) est
un membre de Ө(g(x)) . Au lieu de cela, nous écrirons habituellement f(x) = Ө(g(x)) pour
exprimer la même notion - c'est la manière commune.
Chaque fois que Ө(g(x)) apparaît dans une formule, nous l'interprétons comme représentant une
fonction anonyme à laquelle nous ne tenons pas compte. Par exemple, l'équation T(n) = T(n/2) +
Ө(n) signifie T(n) = T(n/2) + f(n) où f(n) est une fonction de l'ensemble Ө(n) .
Soit f et g deux fonctions définies sur un sous-ensemble des nombres réels. Nous écrivons f(x) =
Ө(g(x)) comme x->infinity si et seulement si il y a des constantes positives K et L et un nombre
réel x0 tel que:
[Link] 114
si limit(x->infinity) f(x)/g(x) = c ∈ (0,∞) c'est-à-dire que la limite existe et qu'elle est positive,
alors f(x) = Ө(g(x))
Constant Ө (1) 1 1
Notation Big-Omega
Définition formelle
Soit f(n) et g(n) deux fonctions définies sur l'ensemble des nombres réels positifs. On écrit f(n) =
Ω(g(n)) s'il y a des constantes positives c et n0 telles que:
Remarques
f(n) = Ω(g(n)) signifie que f(n) augmente asymptotiquement plus lentement que g(n) . On peut
aussi dire à propos de Ω(g(n)) lorsque l’analyse des algorithmes ne suffit pas pour énoncer sur Θ(
g(n)) ou / et O(g(n)) .
Pour deux fonctions f(n) et g(n) nous avons f(n) = Ө(g(n)) si et seulement si f(n) = O(g(n)) and
f(n) = Ω(g(n)) .
[Link] 115
Par exemple, on peut avoir f(n) = 3n^2 + 5n - 4 . Alors f(n) = Ω(n^2) . Il est également correct
que f(n) = Ω(n) , ou même f(n) = Ω(1) .
Un autre exemple pour résoudre l'algorithme d'appariement parfait: Si le nombre de sommets est
impair, alors la sortie "Pas de correspondance parfaite" est utilisée, sinon essayez tous les
appariements possibles.
Nous voudrions dire que l'algorithme nécessite un temps exponentiel mais en fait vous ne pouvez
pas prouver une limite inférieure Ω(n^2) utilisant la définition habituelle de Ω puisque l'algorithme
s'exécute en temps linéaire pour n impair. Nous devrions plutôt définir f(n)=Ω(g(n)) en disant pour
une constante c>0 , f(n)≥ cg(n) pour un nombre infini de n . Cela donne une belle correspondance
entre les limites supérieure et inférieure: f(n)=Ω(g(n)) ssi f(n) != o(g(n)) .
Les références
La définition formelle et le théorème sont tirés du livre "Thomas H. Cormen, Charles E. Leiserson,
Ronald L. Rivest, Clifford Stein. Introduction aux algorithmes".
Soit f(n) et g(n) deux fonctions définies sur l'ensemble des nombres réels positifs, c, c1, c2, n0
sont des constantes réelles positives.
Définition
∃ c > 0, ∃ n0 > 0 : ∀ n ≥ n0, 0 ≤ f(n) ≤ cg(n) ∃ c > 0, ∃ n0 > 0 : ∀ n ≥ n0, 0 ≤ cg
formelle
[Link] 116
Notation f (n) = O (g (n)) f (n) = Ω (g (n))
Analogie
entre la
comparaison
asymptotique a ≤ b a ≥ b
des nombres
f, g et réels
a, b
Interprétation
graphique
Les notations asymptotiques peuvent être représentées sur un diagramme de Venn comme suit:
Liens
Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introduction aux
[Link] 117
algorithmes.
[Link] 118
Chapitre 23: Comptage tri
Examples
Compter les informations de base du tri
Le tri par comptage est un algorithme de tri d'entiers pour une collection d'objets qui trie en
fonction des clés des objets.
Pas
1. Construisez un tableau de travail C dont la taille est égale à la plage du tableau d'entrée A.
2. Itérer via A , en assignant C [x] en fonction du nombre de fois que x est apparu dans A.
3. Transformer C dans un tableau où C [x] désigne le nombre de valeurs ≤ x par itération à
travers le réseau, en attribuant à chaque C [x] , la somme de sa valeur précédente et toutes
les valeurs de C qui lui sont soumises.
4. Itérer en arrière à travers A , en plaçant chaque valeur dans un nouveau tableau trié B à
l'index enregistré en C. Ceci est fait pour un A [x] donné en assignant B [ C [ A [x]]] à A [x] et
en décrémentant C [ A [x]] au cas où il y aurait des valeurs en double dans le tableau non
trié original.
Implémentation de Psuedocode
Contraintes:
[Link] 119
4. Count (un tableau de nombre)
Pseudocode:
for x in input:
count[key(x)] += 1
total = 0
for i in range(k):
oldCount = count[i]
count[i] = total
total += oldCount
for x in input:
output[count[key(x)]] = x
count[key(x)] += 1
return output
Implémentation C #
[Link] 120
Chapitre 24: Déformation temporelle
dynamique
Examples
Introduction à la déformation temporelle dynamique
Dynamic Time Warping (DTW) est un algorithme permettant de mesurer la similarité entre deux
séquences temporelles dont la vitesse peut varier. Par exemple, les similitudes dans la marche
pourraient être détectées à l'aide de DTW, même si une personne marchait plus vite que l'autre ou
s'il y avait des accélérations et des décélérations au cours d'une observation. Il peut être utilisé
pour associer un exemple de commande vocale à d'autres commandes, même si la personne
parle plus rapidement ou plus lentement que la voix échantillon préenregistrée. DTW peut être
appliqué à des séquences temporelles de données vidéo, audio et graphiques. En effet, toutes les
données pouvant être transformées en une séquence linéaire peuvent être analysées avec DTW.
En général, DTW est une méthode qui calcule une correspondance optimale entre deux
séquences données avec certaines restrictions. Mais restons aux points les plus simples ici.
Disons que nous avons deux séquences vocales, Sample et Test , et nous voulons vérifier si ces
deux séquences correspondent ou non. Ici, la séquence vocale fait référence au signal numérique
converti de votre voix. Ce pourrait être l'amplitude ou la fréquence de votre voix qui dénote les
mots que vous dites. Assumons:
Sample = {1, 2, 3, 5, 5, 5, 6}
Test = {1, 1, 2, 2, 3, 5}
Au début, nous définissons la distance entre deux points, d (x, y) où x et y représentent les deux
points. Laisser,
Créons un tableau de matrice 2D en utilisant ces deux séquences. Nous allons calculer les
distances entre chaque point de l' échantillon avec chaque point de test et trouver la
correspondance optimale entre eux.
+------+------+------+------+------+------+------+------+
| | 0 | 1 | 1 | 2 | 2 | 3 | 5 |
+------+------+------+------+------+------+------+------+
| 0 | | | | | | | |
+------+------+------+------+------+------+------+------+
| 1 | | | | | | | |
+------+------+------+------+------+------+------+------+
| 2 | | | | | | | |
+------+------+------+------+------+------+------+------+
[Link] 121
| 3 | | | | | | | |
+------+------+------+------+------+------+------+------+
| 5 | | | | | | | |
+------+------+------+------+------+------+------+------+
| 5 | | | | | | | |
+------+------+------+------+------+------+------+------+
| 5 | | | | | | | |
+------+------+------+------+------+------+------+------+
| 6 | | | | | | | |
+------+------+------+------+------+------+------+------+
Ici, le tableau [i] [j] représente la distance optimale entre deux séquences si l'on considère la
séquence jusqu'à Sample [i] et Test [j] , en tenant compte de toutes les distances optimales
observées auparavant.
Pour la première ligne, si nous ne prenons aucune valeur de Sample , la distance entre cette
valeur et Test sera infinie . Nous avons donc mis l' infini sur la première ligne. Même chose pour
la première colonne. Si nous ne prenons aucune valeur de Test , la distance entre celui-ci et
Sample sera également infinie. Et la distance entre 0 et 0 sera simplement 0 . On a,
+------+------+------+------+------+------+------+------+
| | 0 | 1 | 1 | 2 | 2 | 3 | 5 |
+------+------+------+------+------+------+------+------+
| 0 | 0 | inf | inf | inf | inf | inf | inf |
+------+------+------+------+------+------+------+------+
| 1 | inf | | | | | | |
+------+------+------+------+------+------+------+------+
| 2 | inf | | | | | | |
+------+------+------+------+------+------+------+------+
| 3 | inf | | | | | | |
+------+------+------+------+------+------+------+------+
| 5 | inf | | | | | | |
+------+------+------+------+------+------+------+------+
| 5 | inf | | | | | | |
+------+------+------+------+------+------+------+------+
| 5 | inf | | | | | | |
+------+------+------+------+------+------+------+------+
| 6 | inf | | | | | | |
+------+------+------+------+------+------+------+------+
Maintenant, pour chaque étape, nous allons considérer la distance entre chaque point concerné et
l'ajouter à la distance minimale que nous avons trouvée jusqu'à présent. Cela nous donnera la
distance optimale de deux séquences jusqu'à cette position. Notre formule sera,
Pour le premier, d (1, 1) = 0 , le tableau [0] [0] représente le minimum. La valeur de la table [1]
[1] sera donc 0 + 0 = 0 . Pour le second, d (1, 2) = 0 . Le tableau [1] [1] représente le minimum.
La valeur sera: Table [1] [2] = 0 + 0 = 0 . Si nous continuons de cette façon, après avoir fini, la
table ressemblera à:
+------+------+------+------+------+------+------+------+
| | 0 | 1 | 1 | 2 | 2 | 3 | 5 |
+------+------+------+------+------+------+------+------+
[Link] 122
| 0 | 0 | inf | inf | inf | inf | inf | inf |
+------+------+------+------+------+------+------+------+
| 1 | inf | 0 | 0 | 1 | 2 | 4 | 8 |
+------+------+------+------+------+------+------+------+
| 2 | inf | 1 | 1 | 0 | 0 | 1 | 4 |
+------+------+------+------+------+------+------+------+
| 3 | inf | 3 | 3 | 1 | 1 | 0 | 2 |
+------+------+------+------+------+------+------+------+
| 5 | inf | 7 | 7 | 4 | 4 | 2 | 0 |
+------+------+------+------+------+------+------+------+
| 5 | inf | 11 | 11 | 7 | 7 | 4 | 0 |
+------+------+------+------+------+------+------+------+
| 5 | inf | 15 | 15 | 10 | 10 | 6 | 0 |
+------+------+------+------+------+------+------+------+
| 6 | inf | 20 | 20 | 14 | 14 | 9 | 1 |
+------+------+------+------+------+------+------+------+
La valeur du tableau [7] [6] représente la distance maximale entre ces deux séquences données.
Ici 1 représente la distance maximale entre l' échantillon et le test est 1 .
Maintenant, si nous faisons un retour en arrière du dernier point, jusqu'au retour (0, 0) , nous
obtenons une longue ligne qui se déplace horizontalement, verticalement et en diagonale. Notre
procédure de retour en arrière sera:
Nous continuerons jusqu'à ce que nous atteignions (0, 0) . Chaque mouvement a sa propre
signification:
[Link] 123
Notre pseudo-code sera:
Nous pouvons également ajouter une contrainte de localité. Autrement dit, nous exigeons que si
Sample[i] correspond à Test[j] , alors |i - j| n'est pas plus grand que w , un paramètre de
fenêtre.
Complexité:
Applications:
[Link] 124
• Reconnaissance de mots parlés
• Analyse de puissance de corrélation
[Link] 125
Chapitre 25: Depth First Search
Examples
Introduction à la recherche en profondeur
La recherche en profondeur est un moyen systématique de trouver tous les sommets accessibles
depuis un sommet source. Comme pour la recherche en largeur, DFS traverse un composant
connecté d'un graphe donné et définit un spanning tree. L'idée de base de la recherche en
profondeur est d'explorer méthodiquement chaque bord. Nous recommençons à partir de
différents sommets si nécessaire. Dès que nous découvrons un sommet, DFS commence à
l'explorer (contrairement à BFS, qui place un sommet dans une file d'attente pour l'explorer
ultérieurement).
[Link] 126
en gris.
• Les nœuds complètement parcourus seront colorés en noir.
[Link] 127
[Link] 128
Nous pouvons voir un mot-clé important. C'est le backedge . Tu peux voir. 5-1 s'appelle
backedge. En effet, nous n’avons pas encore fini avec node-1 , donc passer d’un autre noeud au
noeud-1 signifie qu’il ya un cycle dans le graphique. Dans DFS, si nous pouvons passer d'un
nœud gris à un autre, nous pouvons être certains que le graphique a un cycle. C'est l'un des
moyens de détecter un cycle dans un graphique. En fonction du nœud source et de l'ordre des
nœuds que nous visitons, nous pouvons identifier n'importe quel bord d'un cycle en tant que
backedge . Par exemple: si nous passions à 5 au lieu de 1 , nous aurions découvert 2-1 comme
backedge.
Le bord que nous prenons pour passer du nœud gris au nœud blanc est appelé bord d'arbre . Si
nous ne conservons que l' arête de l' arborescence et en supprimons d'autres, nous obtiendrons
l' arborescence DFS .
[Link] 129
Dans un graphique non orienté, si nous pouvons visiter un nœud déjà visité, cela doit être un
backedge . Mais pour les graphes dirigés, il faut vérifier les couleurs. Si et seulement si nous
pouvons passer d'un nœud gris à un autre nœud gris, cela s'appelle un backedge .
Dans DFS, nous pouvons également conserver des horodatages pour chaque nœud, qui peuvent
être utilisés de nombreuses manières (par exemple: tri topologique).
Ici, d [] signifie temps de découverte et f [] signifie heure de fin . Notre pesudo-code ressemblera
à ceci:
Procedure DFS(G):
for each node u in V[G]
color[u] := white
parent[u] := NULL
end for
time := 0
for each node u in V[G]
if color[u] == white
DFS-Visit(u)
end if
end for
Procedure DFS-Visit(u):
color[u] := gray
time := time + 1
d[u] := time
for each node v adjacent to u
if color[v] == white
parent[v] := u
DFS-Visit(v)
end if
end for
color[u] := black
time := time + 1
f[u] := time
Complexité:
Chaque nœud et chaque arête sont visités une fois. La complexité de DFS est donc O (V + E) , où
V désigne le nombre de nœuds et E le nombre d’arêtes.
• Trouver tous les chemins les plus courts d'une paire dans un graphique non dirigé.
• Cycle de détection dans un graphique.
• Trouver son chemin.
• Tri topologique
• Tester si un graphique est bipartite.
• Trouver un composant fortement connecté.
• Résoudre des puzzles avec une solution.
[Link] 130
Lire Depth First Search en ligne: [Link]
[Link] 131
Chapitre 26: Des arbres
Remarques
Les arbres sont une sous-catégorie ou un sous-type de graphiques de bord de noeud. Ils sont
omniprésents en informatique en raison de leur prévalence en tant que modèles pour de
nombreuses structures algorithmiques différentes, qui sont à leur tour appliquées dans de
nombreux algorithmes différents.
Examples
introduction
Les arbres sont un sous-type de la structure de données de graphique de bord de nœud plus
générale.
[Link] 132
nœuds sont accessibles via un chemin dans le graphique.
La structure de données arborescente est assez courante en informatique. Les arbres sont utilisés
pour modéliser de nombreuses structures de données algorithmiques différentes, telles que les
arbres binaires ordinaires, les arbres rouge-noir, les arbres B, les arbres AB, les arbres 23, le tas
et les essais.
Généralement, nous représentons un arbre anary (un avec des enfants potentiellement illimités
par nœud) en tant qu'arbre binaire (un avec exactement deux enfants par nœud). Le "prochain"
enfant est considéré comme un frère. Notez que si une arborescence est binaire, cette
représentation crée des nœuds supplémentaires.
Nous parcourons ensuite les frères et sœurs et faisons reculer les enfants. Comme la plupart des
arbres sont relativement peu profonds - beaucoup d’enfants mais seulement quelques niveaux
hiérarchiques, cela donne lieu à un code efficace. Notez que les généalogies humaines sont une
exception (beaucoup de niveaux d'ancêtres, seulement quelques enfants par niveau).
Si nécessaire, des pointeurs peuvent être conservés pour permettre l’ascension de l’arbre. Ce
sont plus difficiles à maintenir.
Notez qu'il est courant d'avoir une fonction pour appeler la racine et une fonction récursive avec
des paramètres supplémentaires, dans ce cas la profondeur de l'arborescence.
struct node
{
struct node *next;
struct node *child;
std::string data;
}
while(node)
{
if(node->child)
{
for(i=0;i<depth*3;i++)
printf(" ");
printf("{\n"):
printtree_r(node->child, depth +1);
[Link] 133
for(i=0;i<depth*3;i++)
printf(" ");
printf("{\n"):
for(i=0;i<depth*3;i++)
printf(" ");
printf("%s\n", node->data.c_str());
node = node->next;
}
}
}
Exemple 1
une)
b)
Exemple: 2
une)
[Link] 134
b)
if(root1->data == root2->data
&& sameTree(root1->left,root2->left)
&& sameTree(root1->right, root2->right))
return true;
[Link] 135
Chapitre 27: Exponentiation Matricielle
Examples
Exponentiation de matrice pour résoudre des problèmes d'exemple
Trouver f (n): n ème numéro de Fibonacci. Le problème est assez facile quand n est relativement
petit. Nous pouvons utiliser la récursivité simple, f(n) = f(n-1) + f(n-2) , ou nous pouvons utiliser
l'approche de programmation dynamique pour éviter le calcul de la même fonction encore et
encore. Mais que ferez-vous si le problème indique: Compte tenu de 0 <n <10 find, recherchez f
(n) mod 999983? La programmation dynamique échouera, alors comment pouvons-nous
résoudre ce problème?
Voyons d'abord comment l'exponentiation de la matrice peut aider à représenter une relation
récursive.
Conditions préalables:
• Compte tenu de deux matrices, sachez trouver leur produit. En outre, sachez que la matrice
de produit de deux matrices et l’une d’elles permettent de trouver l’autre matrice.
• Étant donné une matrice de taille d X d , sachez trouver sa n e puissance dans O (d 3 log
(n)) .
Motifs:
Au début, nous avons besoin d'une relation récursive et nous voulons trouver une matrice M qui
peut nous conduire à l'état souhaité à partir d'un ensemble d'états déjà connus. Supposons que
nous connaissions les k états d'une relation de récurrence donnée et que nous voulons trouver le
(k + 1) ème état. Soit M une matrice k X k , et nous construisons une matrice A: [k X 1] à partir
des états connus de la relation de récurrence, nous voulons maintenant obtenir une matrice B: [k
X 1] qui représentera l'ensemble des états suivants, c.-à-d. MXA = B comme indiqué ci-dessous:
| f(n) | | f(n+1) |
| f(n-1) | | f(n) |
M X | f(n-2) | = | f(n-1) |
| ...... | | ...... |
| f(n-k) | |f(n-k+1)|
Donc, si nous pouvons concevoir M en conséquence, notre travail sera fait! La matrice sera alors
utilisée pour représenter la relation de récurrence.
Type 1:
Commençons par le plus simple, f(n) = f(n-1) + f(n-2)
On obtient, f(n+1) = f(n) + f(n-1) .
Supposons que nous connaissions f(n) et f(n-1) ; Nous voulons découvrir f(n+1) .
A partir de la situation indiquée ci-dessus, la matrice A et la matrice B peuvent être formées
comme indiqué ci-dessous:
[Link] 136
Matrix A Matrix B
| f(n) | | f(n+1) |
| f(n-1) | | f(n) |
[Note: la matrice A sera toujours conçue de telle sorte que chaque état dont dépend f(n+1) sera
présent]
Maintenant, nous devons concevoir une matrice 2X2 M telle qu'elle satisfasse à MXA = B comme
indiqué ci-dessus.
Le premier élément de B est f(n+1) qui est en fait f(n) + f(n-1) . Pour obtenir ceci, à partir de la
matrice A , nous avons besoin de 1 X f (n) et 1 X f (n-1) . La première ligne de M sera donc [1 1] .
| 1 1 | X | f(n) | = | f(n+1) |
| ----- | | f(n-1) | | ------ |
[Note: ----- signifie que nous ne sommes pas concernés par cette valeur.]
De même, le deuxième élément de B est f(n) qui peut être obtenu en prenant simplement 1 X f
(n) de A , donc la 2ème ligne de M est [1 0].
| 1 1 | X | f(n) | = | f(n+1) |
| 1 0 | | f(n-1) | | f(n) |
Type 2:
Matrix A Matrix B
| f(n) | | f(n+1) |
| f(n-1) | | f(n) |
Maintenant, pour f(n+1) = a X f(n) + b X f(n-1) , nous avons besoin de [a, b] dans la première
ligne de la matrice d’objectifs M. Et pour le 2ème élément de B , c'est-à-dire f(n) nous avons déjà
cela dans la matrice A , donc nous prenons cela, ce qui conduit à la 2ème ligne de la matrice M à
[1 0]. Cette fois nous obtenons:
| a b | X | f(n) | = | f(n+1) |
| 1 0 | | f(n-1) | | f(n) |
[Link] 137
Assez simple, hein?
Type 3:
Si vous avez survécu jusqu'à ce stade, vous êtes devenu beaucoup plus vieux, maintenant nous
sommes confrontés à une relation un peu complexe: find f(n) = a X f(n-1) + c X f(n-3) ?
Oups! Il y a quelques minutes, nous n'avons vu que des états contigus, mais ici, l'état f (n-2) est
absent. À présent?
En fait, ce n'est plus un problème, nous pouvons convertir la relation comme suit: f(n) = a X f(n-
1) + 0 X f(n-2) + c X f(n-3) , déduisant f(n+1) = a X f(n) + 0 X f(n-1) + c X f(n-2) . Maintenant,
nous voyons que c'est en fait une forme décrite dans le type 2. Donc ici la matrice d'objectifs M
sera 3 X 3 et les éléments sont:
| a 0 c | | f(n) | | f(n+1) |
| 1 0 0 | X | f(n-1) | = | f(n) |
| 0 1 0 | | f(n-2) | | f(n-1) |
Celles-ci sont calculées de la même manière que le type 2, si vous trouvez cela difficile, essayez-
le sur un stylo et du papier.
Type 4:
La vie devient complexe en enfer, et Mr, Problem vous demande maintenant de trouver f(n) =
f(n-1) + f(n-2) + c où c est une constante.
Maintenant, c'est un nouveau et tout ce que nous avons vu dans le passé, après la multiplication,
chaque état dans A se transforme en son état suivant dans B.
Donc, normalement, nous ne pouvons pas passer à travers la mode précédente, mais que diriez-
vous d'ajouter c comme un état:
| f(n) | | f(n+1) |
M X | f(n-1) | = | f(n) |
| c | | c |
Maintenant, il n'est pas très difficile de concevoir M. Voici comment faire, mais n'oubliez pas de
vérifier:
| 1 1 1 | | f(n) | | f(n+1) |
| 1 0 0 | X | f(n-1) | = | f(n) |
| 0 0 1 | | c | | c |
Type 5:
[Link] 138
comme un exercice pour vous. Essayez d'abord de trouver les états et la matrice M. Et vérifiez si
cela correspond à votre solution. Trouvez également les matrices A et B.
| a 0 c d 1 |
| 1 0 0 0 0 |
| 0 1 0 0 0 |
| 0 0 1 0 0 |
| 0 0 0 0 1 |
Type 6:
En bref:
Ici, nous pouvons diviser les fonctions dans la base de pair impair et conserver 2 matrices
différentes pour les deux et les calculer séparément.
Type 7:
Se sentir un peu trop confiant? Bien pour vous. Parfois, il peut être nécessaire de maintenir
plusieurs récurrences, là où cela les intéresse. Par exemple, laissez une récurrence être:
Ici, la récurrence g (n) dépend de f(n) et celle-ci peut être calculée dans la même matrice mais de
dimensions accrues. À partir de cela, commençons par concevoir les matrices A et B.
Matrix A Matrix B
| g(n) | | g(n+1) |
| g(n-1) | | g(n) |
| f(n+1) | | f(n+2) |
| f(n) | | f(n+1) |
Ici, g(n+1) = 2g(n-1) + f(n+1) and f(n+2) = 2f(n+1) + 2f(n) . Maintenant, en utilisant les processus
indiqués ci-dessus, nous pouvons trouver que la matrice d'objectifs M est:
| 2 2 1 0 |
| 1 0 0 0 |
| 0 0 2 2 |
| 0 0 1 0 |
Donc, ce sont les catégories de base des relations de récurrence qui sont utilisées pour résoudre
par cette technique simple.
[Link] 139
Lire Exponentiation Matricielle en ligne:
[Link]
[Link] 140
Chapitre 28: Fonctions de hachage
Examples
Introduction aux fonctions de hachage
La fonction de hachage h() est une fonction arbitraire qui mappe les données x ∈ X de taille
arbitraire à la valeur y ∈ Y de taille fixe: y = h(x) . Les bonnes fonctions de hachage suivent les
restrictions suivantes:
• les fonctions de hachage sont déterministes. h(x) devrait toujours retourner la même valeur
pour un x donné
En général, la taille de la fonction de hachage est inférieure à la taille des données d'entrée: |y| <
|x| . Les fonctions de hachage ne sont pas réversibles ou, en d'autres termes, elles peuvent être
une collision: ∃ x1, x2 ∈ X, x1 ≠ x2: h(x1) = h(x2) . X peut être un ensemble fini ou infini et Y un
ensemble fini.
Les fonctions de hachage sont utilisées dans de nombreuses parties de l'informatique, par
exemple dans le génie logiciel, la cryptographie, les bases de données, les réseaux,
l'apprentissage automatique, etc. Il existe différents types de fonctions de hachage, avec des
propriétés spécifiques au domaine différentes.
Le hachage est souvent une valeur entière. Il existe des méthodes spéciales de programmation
des langages pour le calcul du hachage. Par exemple, dans C# GetHashCode() méthode pour tous
les types renvoie la valeur Int32 (nombre entier 32 bits). En Java chaque classe fournit la
hashCode() qui renvoie int . Chaque type de données possède des implémentations propres ou
définies par l'utilisateur.
Méthodes de hachage
Il existe plusieurs approches pour déterminer la fonction de hachage. Sans perte de généralité,
admettons que x ∈ X = {z ∈ ℤ: z ≥ 0} sont des nombres entiers positifs. Souvent, m est premier
(pas trop près d'une puissance exacte de 2).
[Link] 141
Table de hachage
Fonctions de hachage utilisées dans les tables de hachage pour calculer l'index dans un tableau
de logements. La table de hachage est une structure de données pour l'implémentation de
dictionnaires (structure clé-valeur). Les bonnes tables de hachage implémentées ont l'heure O (1)
pour les opérations suivantes: insérer, rechercher et supprimer des données par clé. Plus d'une
clé peut se connecter au même emplacement. Il existe deux manières de résoudre une collision:
1. Chaînage: la liste chaînée est utilisée pour stocker des éléments avec la même valeur de
hachage dans l'emplacement.
Les méthodes suivantes sont utilisées pour calculer les séquences de sonde requises pour
l'adressage ouvert
Méthode Formule
Où i ∈ {0, 1, ..., m-1} , h'(x), h1(x), h2(x) sont des fonctions de hachage auxiliaires, c1, c2
sont des constantes auxiliaires positives.
Exemples
Laisse x ∈ U{1, 1000}, h = x mod m . Le tableau suivant montre les valeurs de hachage en cas de
non premier et premier. Le texte en gras indique les mêmes valeurs de hachage.
723 23 16
103 3 2
738 38 31
292 92 90
61 61 61
87 87 87
995 95 86
[Link] 142
X m = 100 (pas premier) m = 101 (prime)
549 49 44
991 91 82
757 57 50
920 20 11
626 26 20
557 57 52
831 31 23
619 19 13
Liens
• Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introduction aux
algorithmes.
Les codes de hachage produits par la méthode GetHashCode() pour les types C # intégrés et
communs à partir de l'espace de noms System sont présentés ci-dessous.
Booléen
SByte
Carboniser
[Link] 143
Int16
Int64 , double
Décimal
Objet
[Link](this);
L'implémentation par défaut est utilisée pour l' index du bloc de synchronisation .
Chaîne
Type de valeur
Le premier champ non statique est chercher et obtenir son code de hachage. Si le type n'a pas de
[Link] 144
champs non statiques, le code de hachage du type renvoie. Le code de hachage d'un membre
statique ne peut pas être pris, car si ce membre est du même type que le type d'origine, le calcul
se termine dans une boucle infinie.
Nullable <T>
Tableau
int ret = 0;
for (int i = (Length >= 8 ? Length - 8 : 0); i < Length; i++)
{
ret = ((ret << 5) + ret) ^ [Link](GetValue(i));
}
Les références
• GitHub .Net Core CLR
[Link] 145
Chapitre 29: Graphique
Introduction
Un graphique est un ensemble de points et de lignes qui en connectent un sous-ensemble
(éventuellement vide). Les points d'un graphe sont appelés "sommets de graphe", "nœuds" ou
simplement "points". De même, les lignes reliant les sommets d’un graphe sont appelées arêtes
de graphe, "arcs" ou "lignes".
Un graphe G peut être défini comme un couple (V, E), où V est un ensemble de sommets, et E est
un ensemble d'arêtes entre les sommets E ⊆ {(u, v) | u, v ∈ V}.
Remarques
Les graphes sont une structure mathématique qui modélise des ensembles d'objets pouvant ou
non être connectés à des membres provenant de jeux d'arêtes ou de liens.
Un graphique peut être décrit à travers deux ensembles d'objets mathématiques différents:
• Un ensemble de sommets .
• Un ensemble d' arêtes qui connectent des paires de sommets.
• Les graphes dirigés contiennent des arêtes qui "se connectent" d'une seule manière.
• Les graphes non orientés ne contiennent que des arêtes connectant automatiquement
deux sommets dans les deux sens.
Examples
Tri topologique
Un ordre topologique, ou un tri topologique, ordonne les sommets dans un graphe acyclique dirigé
sur une ligne, c'est-à-dire dans une liste, de sorte que tous les bords dirigés vont de gauche à
droite. Un tel ordre ne peut exister si le graphique contient un cycle dirigé, car il est impossible de
continuer sur une ligne et de revenir à votre point de départ.
Formellement, dans un graphe G = (V, E) , un ordre linéaire de tous ses sommets est tel que si G
contient une arête (u, v) ∈ E du sommet u au sommet v alors u précède v dans l’ordre.
Il existe des algorithmes connus pour construire un ordre topologique de n'importe quel DAG en
temps linéaire, par exemple:
1. Appelez depth_first_search(G) pour calculer les temps de finition vf pour chaque sommet v
[Link] 146
2. Au fur et à mesure que chaque sommet est terminé, insérez-le au début d'une liste chaînée
3. la liste des sommets liés, telle qu'elle est maintenant triée.
De nombreuses applications utilisent des graphiques acycliques dirigés pour indiquer les priorités
parmi les événements. Nous utilisons un tri topologique pour obtenir un ordre de traitement de
chaque sommet avant chacun de ses successeurs.
Les sommets d'un graphique peuvent représenter des tâches à exécuter et les arcs peuvent
représenter des contraintes qu'une tâche doit être exécutée avant une autre; un ordre topologique
est une séquence valide pour effectuer l'ensemble de tâches décrit dans V
Soit notre graphe appelé dag (puisque c'est un graphe acyclique dirigé), et laissez-le contenir 5
sommets:
A <- dag.add_vertex(Task(4));
B <- dag.add_vertex(Task(5));
C <- dag.add_vertex(Task(3));
D <- dag.add_vertex(Task(2));
E <- dag.add_vertex(Task(7));
où nous connectons les sommets avec des arêtes dirigées telles que le graphique est acyclique,
// A ---> C ----+
// | | |
// v v v
// B ---> D --> E
dag.add_edge(A, B, Cooldown(2));
dag.add_edge(A, C, Cooldown(2));
dag.add_edge(B, D, Cooldown(1));
dag.add_edge(C, D, Cooldown(1));
dag.add_edge(C, E, Cooldown(1));
dag.add_edge(D, E, Cooldown(3));
[Link] 147
L'algorithme de Thorup
L'algorithme de Thorup pour le chemin le plus court à source unique pour le graphe non dirigé a la
complexité temporelle O (m), inférieure à celle de Dijkstra.
Les idées de base sont les suivantes. (Désolé, je n'ai pas encore essayé de l'implémenter, alors
certains détails mineurs me manqueront. Et le papier original est protégé par un mur de paye,
alors j'ai essayé de le reconstituer à partir d'autres sources. Supprimez ce commentaire si vous
pouvez vérifier.)
• Il existe des moyens de trouver l'arbre couvrant dans O (m) (non décrit ici). Vous devez
"faire croître" le spanning tree du bord le plus court au plus long, et ce serait une forêt avec
plusieurs composants connectés avant sa pleine croissance.
• Sélectionnez un entier b (b> = 2) et ne considérez que les forêts couvrantes avec une limite
de longueur b ^ k. Fusionnez les composants qui sont exactement les mêmes mais avec un
k différent, et appelez le minimum k le niveau du composant. Faites ensuite logiquement des
composants dans un arbre. u est le parent de vsi u est le plus petit composant distinct de v
qui contient entièrement v. La racine est le graphe entier et les feuilles sont des sommets
simples dans le graphe original (avec le niveau d'infini négatif). L'arbre n'a encore que des
noeuds O (n).
• Maintenir la distance de chaque composant à la source (comme dans l'algorithme de
Dijkstra). La distance d'un composant avec plusieurs sommets est la distance minimale de
ses enfants non développés. Définissez la distance du sommet source sur 0 et mettez à jour
les ancêtres en conséquence.
• Considérez les distances dans la base b. Lorsque vous visitez un nœud du niveau k la
première fois, placez ses enfants dans des compartiments partagés par tous les nœuds de
niveau k (comme dans le type seau, remplaçant le tas dans l'algorithme de Dijkstra) par le
chiffre k et supérieur. À chaque fois que vous visitez un nœud, considérez uniquement ses
premiers segments, visitez chacun d'eux et supprimez-les, mettez à jour la distance du
nœud actuel et reliez le nœud actuel à son propre parent en utilisant la nouvelle distance et
attendez la prochaine visite pour les éléments suivants. seaux.
• Lorsqu'une feuille est visitée, la distance actuelle est la distance finale du sommet.
Développez tous les bords du graphique d'origine et mettez à jour les distances en
conséquence.
• Visitez le nœud racine (graphe entier) à plusieurs reprises jusqu'à ce que la destination soit
atteinte.
Il est basé sur le fait qu’il n’existe pas d’extrémité de longueur inférieure à 1 entre deux
composants connectés de la forêt couvrant avec une limite de longueur l, donc, à partir de la
distance x, vous pouvez vous concentrer sur un seul composant la distance x + l. Vous visiterez
des sommets avant que les sommets de plus courte distance ne soient tous visités, mais cela n'a
aucune importance car on sait qu'il n'y aura pas de chemin plus court vers ces sommets. D'autres
parties fonctionnent comme le tri par base de type compartiment / MSD et, bien sûr, elles
requièrent l'arbre couvrant O (m).
[Link] 148
Traversal
Il existe un cycle dans un graphe orienté s’il existe un bord arrière découvert lors d’un DFS. Un
bord arrière est une arête d'un nœud à lui-même ou à l'un des ancêtres d'une arborescence DFS.
Pour un graphique déconnecté, nous obtenons une forêt DFS, vous devez donc parcourir tous les
sommets du graphique pour trouver des arborescences DFS disjointes.
Implémentation C ++:
#include <iostream>
#include <list>
#define NUM_V 4
for(int i = 0;i<V;i++)
visited[i]=false, recStack[i]=false; //initialize all vertices as not visited and not
recursed
for(int u = 0; u < V; u++) //Iteratively checks if every vertices have been visited
{ if(visited[u]==false)
{ if(helper(graph, u, visited, recStack)) //checks if the DFS tree from the vertex
contains a cycle
return true;
}
}
return false;
}
/*
[Link] 149
Driver function
*/
int main()
{
list<int>* graph = new list<int>[NUM_V];
graph[0].push_back(1);
graph[0].push_back(2);
graph[1].push_back(2);
graph[2].push_back(0);
graph[2].push_back(3);
graph[3].push_back(3);
bool res = isCyclic(graph, NUM_V);
cout<<res<<endl;
}
Résultat: comme indiqué ci-dessous, il y a trois bords arrière dans le graphique. Un entre les
sommets 0 et 2; entre les sommets 0, 1 et 2; et sommet 3. La complexité temporelle de la
recherche est O (V + E) où V est le nombre de sommets et E est le nombre d'arêtes.
[Link] 150
Introduction à la théorie des graphes
La théorie des graphes est l'étude des graphes, qui sont des structures mathématiques utilisées
pour modéliser les relations par paires entre les objets.
Saviez-vous que presque tous les problèmes de la planète Terre peuvent être transformés en
problèmes de routes et de villes et résolus? La théorie des graphes a été inventée il y a plusieurs
années, même avant l'invention de l'ordinateur. Leonhard Euler a écrit un article sur les sept ponts
de Königsberg, considéré comme le premier article de la théorie des graphes. Depuis lors, les
gens se sont rendu compte que si nous pouvions convertir n'importe quel problème à ce problème
City-Road, nous pourrons le résoudre facilement grâce à la théorie des graphes.
La théorie des graphes a de nombreuses applications. L'une des applications les plus courantes
consiste à trouver la distance la plus courte entre une ville et une autre. Nous savons tous que
pour accéder à votre PC, cette page Web devait parcourir de nombreux routeurs à partir du
serveur. La théorie des graphes l'aide à trouver les routeurs à croiser. Pendant la guerre, quelle
rue doit être bombardée pour déconnecter la capitale des autres, cela peut aussi être découvert
en utilisant la théorie des graphes.
Laissez-nous d'abord apprendre quelques définitions de base sur la théorie des graphes.
Graphique:
Disons que nous avons 6 villes. Nous les marquons comme 1, 2, 3, 4, 5, 6. Nous connectons
maintenant les villes qui ont des routes entre elles.
Ceci est un graphique simple où certaines villes sont affichées avec les routes qui les connectent.
Dans la théorie des graphes, nous appelons chacune de ces villes Nœud ou Sommet et les
routes sont appelées Edge. Le graphique est simplement une connexion de ces nœuds et arêtes.
Un nœud peut représenter beaucoup de choses. Dans certains graphiques, les nœuds
représentent des villes, certains représentent des aéroports, certains représentent un carré dans
[Link] 151
un échiquier. Edge représente la relation entre chaque nœud. Cette relation peut être le moment
d'aller d'un aéroport à un autre, les déplacements d'un chevalier d'un carré à tous les autres
carrés, etc.
Dans les mots simples, un nœud représente n'importe quel objet et Edge représente la relation
entre deux objets.
Noeud adjacent:
Si un nœud A partage un bord avec le nœud B , alors B est considéré comme adjacent à A. En
d'autres termes, si deux nœuds sont directement connectés, ils sont appelés nœuds adjacents.
Un nœud peut avoir plusieurs nœuds adjacents.
Dans les graphes dirigés, les arêtes ont des signes de direction sur un côté, ce qui signifie que les
arêtes sont unidirectionnelles . Par ailleurs, les arêtes des graphes non orientés ont des signes de
direction des deux côtés, ce qui signifie qu'ils sont bidirectionnels . Les graphes non dirigés sont
généralement représentés sans signe des deux côtés des arêtes.
Supposons qu'il y ait une fête en cours. Les membres du groupe sont représentés par des nœuds
et il y a un avantage entre deux personnes s'ils se serrent la main. Ensuite, ce graphique n'est pas
[Link] 152
dirigé car toute personne A serre la main de la personne B si et seulement si B serre la main avec
A. En revanche, si les bords d'une personne A à une autre personne B correspond à admirer B
de A, ce graphique est dirigé, parce que l' admiration est pas nécessairement un mouvement
alternatif. Le premier type de graphe est appelé graphe non dirigé et les arêtes sont appelées
arêtes non dirigées tandis que le dernier type de graphe est appelé graphe orienté et les arêtes
sont appelées arêtes dirigées.
Un graphique pondéré est un graphique dans lequel un nombre (le poids) est attribué à chaque
arête. Ces pondérations peuvent représenter, par exemple, les coûts, les longueurs ou les
Un graphique non pondéré est simplement l'inverse. Nous supposons que le poids de toutes les
arêtes est le même (probablement 1).
Chemin:
Un chemin représente un moyen de passer d'un nœud à un autre. Il consiste en une séquence de
bords. Il peut y avoir plusieurs chemins entre deux nœuds.
[Link] 153
Dans l'exemple ci-dessus, il existe deux chemins de A à D. A-> B, B-> C, C-> D est un chemin.
Le coût de ce chemin est 3 + 4 + 2 = 9 . Encore une fois, il y a un autre chemin A-> D. Le coût de
ce chemin est de 10 . Le chemin qui coûte le plus bas est appelé le plus court chemin .
Degré:
Le degré d'un sommet est le nombre d'arêtes qui lui sont connectées. S'il y a un bord qui se
connecte au sommet aux deux extrémités (une boucle) est compté deux fois.
Dans les graphes dirigés, les nœuds ont deux types de degrés:
Pour les graphiques non orientés, ils sont simplement appelés degré.
[Link] 154
Quelques algorithmes liés à la théorie des graphes
• Matrice d'adjacence
• Liste d'adjacence
Une matrice d'adjacence est une matrice carrée utilisée pour représenter un graphe fini. Les
éléments de la matrice indiquent si les paires de sommets sont adjacentes ou non dans le
graphique.
Adjacent signifie «à côté ou à côté de quelque chose» ou à côté de quelque chose. Par exemple,
vos voisins sont adjacents à vous. En théorie des graphes, si on peut aller au nœud B du nœud
A , on peut dire que le nœud B est adjacent au nœud A. Nous allons maintenant apprendre à
stocker quels nœuds sont adjacents à celui via Adjacency Matrix. Cela signifie que nous allons
représenter les nœuds partageant un bord entre eux. Ici, la matrice signifie un tableau 2D.
[Link] 155
Ici vous pouvez voir un tableau à côté du graphique, c'est notre matrice d'adjacence. Ici, la
matrice [i] [j] = 1 représente un bord entre i et j . S'il n'y a pas de bord, nous mettons simplement
Matrix [i] [j] = 0 .
Ces arêtes peuvent être pondérées, comme si elles pouvaient représenter la distance entre deux
villes. Ensuite, nous mettrons la valeur dans Matrix [i] [j] au lieu de mettre 1.
Le graphe décrit ci-dessus est bidirectionnel ou non dirigé , ce qui signifie que si nous pouvons
aller au nœud 1 du nœud 2 , nous pouvons également aller au nœud 2 du nœud 1 . Si le
graphique était dirigé , il y aurait eu un signe de flèche sur un côté du graphique. Même dans ce
cas, nous pourrions le représenter en utilisant une matrice de contiguïté.
Nous représentons les nœuds qui ne partagent pas l'arête à l' infini . Une chose à noter est que si
le graphe n'est pas orienté, la matrice devient symétrique .
[Link] 156
Matrix[N][N]
for i from 1 to N
for j from 1 to N
Take input -> Matrix[i][j]
endfor
endfor
Pour les graphes dirigés, nous pouvons supprimer Matrix [n2] [n1] = ligne de coût .
La mémoire est un énorme problème. Peu importe le nombre d'arêtes présentes, nous aurons
toujours besoin d'une matrice de taille N * N où N est le nombre de nœuds. S'il y a 10000 nœuds,
la taille de la matrice sera de 4 * 10000 * 10000 autour de 381 mégaoctets. Ceci est un énorme
gaspillage de mémoire si l'on considère les graphiques qui ont quelques bords.
Supposons que nous voulions savoir à quel nœud nous pouvons aller depuis un nœud u . Nous
devons vérifier toute la rangée de u , ce qui coûte beaucoup de temps.
Le seul avantage est que nous pouvons facilement trouver la connexion entre les nœuds uv et
leur coût en utilisant Adjacency Matrix.
import [Link];
public Represent_Graph_Adjacency_Matrix(int v)
{
vertices = v;
adjacency_matrix = new int[vertices + 1][vertices + 1];
}
[Link] 157
[Link]("The vertices does not exists");
}
}
}
catch (Exception E)
{
[Link]("Somthing went wrong");
}
[Link] 158
[Link]();
}
}
Exemple:
$ java Represent_Graph_Adjacency_Matrix
Enter the number of vertices:
4
Enter the number of edges:
6
Enter the edges: <to> <from>
1 1
3 4
2 3
1 4
2 4
1 2
The adjacency matrix for the given graph is:
1 2 3 4
1 1 1 0 1
2 0 0 1 1
3 0 0 0 1
4 0 0 0 0
La liste d'adjacence est une collection de listes non ordonnées utilisées pour représenter un
graphe fini. Chaque liste décrit l'ensemble des voisins d'un sommet dans un graphique. Il faut
moins de mémoire pour stocker des graphiques.
[Link] 159
Ceci s'appelle la liste de contiguïté. Il montre quels nœuds sont connectés à quels nœuds. Nous
pouvons stocker ces informations en utilisant un tableau 2D. Mais cela nous coûtera la même
mémoire que la matrice d'adjacence. Au lieu de cela, nous allons utiliser la mémoire allouée
dynamiquement pour stocker celle-ci.
De nombreuses langues prennent en charge Vector ou List, que nous pouvons utiliser pour
stocker la liste d’adjacences. Pour ceux-ci, nous n'avons pas besoin de spécifier la taille de la
liste . Il suffit de spécifier le nombre maximum de nœuds.
Le pseudo-code sera:
[Link] 160
edge[x].push(y)
cost[x].push(w)
end for
Return edge, cost
De celui-ci, nous pouvons facilement trouver le nombre total de nœuds connectés à un nœud, et
ce que sont ces nœuds. Cela prend moins de temps que la matrice d'adjacence. Mais si nous
devions trouver s'il y avait un bord entre u et v , cela aurait été plus facile si nous conservions une
matrice d'adjacence.
[Link] 161
Chapitre 30: L'algorithme de Dijkstra
Examples
Algorithme du plus court chemin de Dijkstra
Avant de continuer, il est recommandé d'avoir une brève idée de la matrice d'adjacence et de la
BFS
L'algorithme de Dijkstra est connu sous le nom d'algorithme à source unique le plus court. Il est
utilisé pour trouver les chemins les plus courts entre les nœuds dans un graphique, qui peuvent
représenter, par exemple, des réseaux routiers. Il a été conçu par Edsger W. Dijkstra en 1956 et
publié trois ans plus tard.
Nous pouvons trouver le chemin le plus court en utilisant l’algorithme de recherche Breadth First
Search (BFS). Cet algorithme fonctionne bien, mais le problème est que cela suppose que le coût
de traverser chaque chemin est le même, ce qui signifie que le coût de chaque bord est le même.
L'algorithme de Dijkstra nous aide à trouver le chemin le plus court où le coût de chaque chemin
n'est pas le même.
Au début, nous verrons comment modifier BFS pour écrire l'algorithme de Dijkstra, puis nous
ajouterons une file d'attente prioritaire pour en faire un algorithme complet de Dijkstra.
Disons que la distance de chaque noeud de la source est conservée dans le tableau d [] . Comme
dans, d [3] représente le temps d [3] pour atteindre le nœud 3 depuis la source . Si nous ne
connaissons pas la distance, nous allons stocker l' infini dans d [3] . Aussi, laissez le coût [u] [v]
représenter le coût de uv . Cela signifie qu'il prend coût [u] [v] pour aller du noeud u au noeud v.
Nous devons comprendre Edge Relaxation. Disons que de chez vous, c'est la source , il faut 10
minutes pour aller au lieu A. Et il faut 25 minutes pour aller au lieu B. Nous avons,
d[A] = 10
d[B] = 25
Maintenant, disons qu'il faut 7 minutes pour aller du lieu A au lieu B , cela signifie:
cost[A][B] = 7
Ensuite, nous pouvons aller placer B à partir de la source en allant placer A à partir de la source
[Link] 162
, puis à partir de la position A , en passant à la position B , qui prendra 10 + 7 = 17 minutes au lieu
de 25 minutes. Alors,
Cela s'appelle la relaxation. Nous irons du noeud u au noeud v et si d [u] + cost [u] [v] <d [v]
alors nous mettrons à jour d [v] = d [u] + cost [u] [v] .
Dans BFS, nous n'avons pas eu besoin de visiter deux nœuds. Nous avons seulement vérifié si
un nœud est visité ou non. Si elle n'était pas visitée, nous avons poussé le nœud dans la file
d'attente, marqué comme visité et incrémenté la distance de 1. Dans Dijkstra, nous pouvons
pousser un nœud dans la file d'attente et au lieu de le mettre à jour, nous relâchons ou mettons à
jour le nouveau bord. Regardons un exemple:
d[1] = 0
d[2] = d[3] = d[4] = infinity (or a large value)
Nous définissons, d [2], d [3] et d [4] à l' infini car nous ne connaissons pas encore la distance. Et
la distance de la source est bien sûr 0 . Maintenant, nous allons à d'autres nœuds à partir de la
source et si nous pouvons les mettre à jour, nous les enverrons dans la file d'attente. Disons par
exemple, nous allons traverser le bord 1-2 . Comme d [1] + 2 <d [2] qui fera d [2] = 2 . De même,
nous traverserons l' arête 1-3, ce qui rend d [3] = 5 .
[Link] 163
Nous pouvons clairement voir que 5 n'est pas la distance la plus courte que nous pouvons
traverser pour aller au nœud 3 . Donc, traverser un nœud une seule fois, comme BFS, ne
fonctionne pas ici. Si nous passons du nœud 2 au nœud 3 en utilisant l' arête 2-3 , nous pouvons
mettre à jour d [3] = d [2] + 1 = 3 . Nous pouvons donc voir qu'un nœud peut être mis à jour
plusieurs fois. Combien de fois demandez-vous? Le nombre maximal de fois qu'un nœud peut
être mis à jour est le nombre de degrés d'un nœud.
Voyons le pseudo-code pour visiter n'importe quel noeud plusieurs fois. Nous allons simplement
modifier BFS:
Cela peut être utilisé pour trouver le chemin le plus court de tous les nœuds de la source. La
complexité de ce code n'est pas si bonne. Voici pourquoi,
En BFS, lorsque nous passons du nœud 1 à tous les autres nœuds, nous suivons la méthode du
premier arrivé, premier servi . Par exemple, nous sommes allés au nœud 3 de la source avant de
traiter le nœud 2 . Si nous allons au noeud 3 de la source , nous mettons à jour le noeud 4 en 5
+ 3 = 8 . Lorsque nous mettons à jour le nœud 3 à partir du nœud 2 , nous devons mettre à jour
le nœud 4 en tant que 3 + 3 = 6 ! Ainsi, le nœud 4 est mis à jour deux fois.
Dijkstra a proposé, au lieu de choisir la méthode du premier arrivé, premier servi , si nous mettons
à jour les nœuds les plus proches en premier, alors il faudra moins de mises à jour. Si nous
traitions le nœud 2 auparavant, alors le nœud 3 aurait été mis à jour avant et après avoir mis à
jour le nœud 4 en conséquence, nous obtiendrions facilement la distance la plus courte! L'idée
est de choisir dans la file d'attente, le nœud le plus proche de la source . Nous allons donc utiliser
la file d' attente prioritaire ici de sorte que lorsque nous sautons la file d' attente, il nous apportera
le nœud le plus proche de la source u. Comment ça va faire ça? Il vérifiera la valeur de d [u]
avec elle.
Voyons le pseudo-code:
[Link] 164
remove u from the Q
for all edges from u to v in [Link](v) do
if distance[u] + cost[u][v] < distance[v]
distance[v] = distance[u] + cost[u][v]
[Link](v)
end if
end for
end while
Return distance
Le pseudo-code renvoie la distance de tous les autres noeuds de la source . Si nous voulons
connaître la distance d'un seul nœud v , nous pouvons simplement renvoyer la valeur lorsque v
est sorti de la file d'attente.
Complexité:
La complexité de BFS est O (log (V + E)) où V est le nombre de nœuds et E est le nombre
d'arêtes. Pour Dijkstra, la complexité est similaire, mais le tri de la file d'attente prioritaire prend O
(logV) . La complexité totale est donc: O (Vlog (V) + E)
Vous trouverez ci-dessous un exemple Java pour résoudre l'algorithme du plus court chemin de
Dijkstra à l'aide de la matrice d'adjacence.
import [Link].*;
import [Link].*;
import [Link].*;
class ShortestPath
{
static final int V=9;
int minDistance(int dist[], Boolean sptSet[])
{
int min = Integer.MAX_VALUE, min_index=-1;
return min_index;
}
[Link] 165
void dijkstra(int graph[][], int src)
{
dist[src] = 0;
sptSet[u] = true;
printSolution(dist, V);
}
[Link] 166
Lire L'algorithme de Dijkstra en ligne: [Link]
dijkstra
[Link] 167
Chapitre 31: L'algorithme de Kruskal
Remarques
L'algorithme de Kruskal est un algorithme glouton utilisé pour trouver l' arbre à recouvrement
minimal (MST) d'un graphique. Un arbre couvrant minimal est un arbre qui connecte tous les
sommets du graphique et a le poids de bord total minimal.
L'algorithme de Kruskal le fait en sélectionnant de manière répétée des arêtes de poids minimal
(qui ne figurent pas déjà dans le fichier MST) et les ajoute au résultat final si les deux sommets
connectés par cette arête ne sont pas encore connectés dans le fichier MST. Union - La structure
de données de recherche peut être utilisée pour vérifier si deux sommets sont déjà connectés ou
non dans le fichier MST. Quelques propriétés de MST sont les suivantes:
Examples
Implémentation simple et plus détaillée
Afin de gérer efficacement la détection du cycle, nous considérons chaque nœud comme faisant
partie d'un arbre. Lors de l'ajout d'une arête, nous vérifions si ses deux nœuds composants font
partie d'arbres distincts. Initialement, chaque nœud constitue une arborescence à un nœud.
La méthodologie forestière ci-dessus est en réalité une structure de données disjointe, qui
implique trois opérations principales:
[Link] 168
subalgo unionSet(v, u: nodes):
vRoot = findSet(v)
uRoot = findSet(u)
[Link] = vRoot
Cette implémentation naïve conduit à un temps O(n log n) pour gérer la structure de données de
l'ensemble disjoint, conduisant à l'heure O(m*n log n) pour tout l'algorithme de Kruskal.
Nous pouvons faire deux choses pour améliorer les sous-algorithmes simples et sous-optimaux
des ensembles disjoints:
2. Heuristique de fusion basée sur la hauteur: pour chaque noeud, stockez la hauteur de son
sous-arbre. Lors de la fusion, faites du grand arbre le parent du plus petit, ce qui n'augmente
pas la hauteur de quiconque.
if vRoot == uRoot:
return
Cela conduit à un temps O(alpha(n)) pour chaque opération, où alpha est l'inverse de la fonction
Ackermann à croissance rapide, sa croissance est donc très lente et peut être considérée comme
O(1) à des fins pratiques.
[Link] 169
Cela rend l’algorithme entier de Kruskal O(m log m + m) = O(m log m) , à cause du tri initial.
Remarque
if vRoot == uRoot:
return
if random() % 2 == 0:
[Link] = uRoot
else:
[Link] = vRoot
Triez les arêtes par valeur et ajoutez-en chacune au MST dans l'ordre trié, si cela ne crée pas de
cycle.
return MST
[Link] 170
Chapitre 32: L'algorithme de Prim
Examples
Introduction à l'algorithme de Prim
Disons que nous avons 8 maisons. Nous voulons installer des lignes téléphoniques entre ces
maisons. Le bord entre les maisons représente le coût de la mise en ligne entre deux maisons.
Notre tâche consiste à mettre en place des lignes de telle sorte que toutes les maisons soient
connectées et que le coût de la configuration de la connexion soit minimal. Maintenant, comment
trouvons-nous cela? Nous pouvons utiliser l'algorithme de Prim .
L'algorithme de Prim est un algorithme glouton qui trouve un arbre couvrant minimal pour un
graphique non dirigé pondéré. Cela signifie qu'il trouve un sous-ensemble des arêtes qui forment
un arbre qui inclut tous les noeuds, où le poids total de toutes les arêtes de l'arborescence est
réduit. L'algorithme a été développé en 1930 par le mathématicien tchèque Vojtěch Jarník , puis
redécouvert et republié par l'informaticien Robert Clay Prim en 1957 et Edsger Wybe Dijkstra en
1959. Il est également connu comme algorithme DJP, algorithme de Jarnik, algorithme Prim-
Jarnik ou Prim-Dijsktra. algorithme .
Voyons maintenant les termes techniques en premier. Si nous créons un graphe, S en utilisant
des nœuds et des arêtes d'un graphe non orienté G , alors S s'appelle un sous -graphe du graphe
G. Maintenant, S sera appelé Spanning Tree si et seulement si:
[Link] 171
• Il contient tous les nœuds de G.
• C'est un arbre, cela signifie qu'il n'y a pas de cycle et que tous les nœuds sont connectés.
• Il y a (n-1) arêtes dans l'arbre, où n est le nombre de nœuds dans G.
Il peut y avoir beaucoup de Spanning Tree dans un graphique. L' arbre Spanning Minimum d'un
graphe non orienté pondéré est un arbre, de sorte que la somme du poids des arêtes est
minimale. Nous allons maintenant utiliser l'algorithme de Prim pour trouver l'arbre de
recouvrement minimal, c'est-à-dire comment configurer les lignes téléphoniques dans notre
exemple de graphique de manière à ce que le coût d'installation soit minimal.
Au début, nous sélectionnerons un nœud source . Disons que node-1 est notre source .
Maintenant, nous allons ajouter le bord de node-1 qui a le coût minimum à notre sous-graphe. Ici,
nous marquons les bords qui sont dans le sous-graphe en utilisant la couleur bleue . Ici, 1-5 est
notre avantage désiré.
Nous considérons maintenant tous les bords de node-1 et node-5 et prenons le minimum.
Puisque 1-5 est déjà marqué, nous prenons 1-2 .
[Link] 172
Cette fois, nous considérons le nœud 1 , le nœud 2 et le nœud 5 et prenons le bord minimal qui
est 5-4 .
[Link] 173
Si nous continuons ainsi, nous sélectionnerons l'arête 8-6 , 6-7 et 4-3 . Notre sous-graphe
ressemblera à:
Ceci est notre sous-graphe souhaité, qui nous donnera le spanning tree minimum. Si nous
supprimons les arêtes que nous n'avons pas sélectionnées, nous obtiendrons:
[Link] 174
Ceci est notre arbre spanning minimum (MST). Le coût de la mise en place des connexions
téléphoniques est donc le suivant: 4 + 2 + 5 + 11 + 9 + 2 + 1 = 34 . Et l'ensemble des maisons et
leurs connexions sont montrés dans le graphique. Il peut y avoir plusieurs MST d'un graphique.
Cela dépend du noeud source choisi.
Complexité:
La complexité temporelle de l'approche naïve ci-dessus est O (V²) . Il utilise une matrice de
contiguïté. Nous pouvons réduire la complexité en utilisant la file d'attente prioritaire. Lorsque
nous ajoutons un nouveau nœud à Vnew , nous pouvons ajouter ses bords adjacents dans la file
d'attente prioritaire. Ensuite, sortez le bord pondéré minimum. La complexité sera alors: O (ElogE)
, où E est le nombre d'arêtes. Encore une fois, un tas binaire peut être construit pour réduire la
complexité à O (ElogV) .
[Link] 175
Procedure MSTPrim(Graph, source):
for each u in V
key[u] := inf
parent[u] := NULL
end for
key[source] := 0
Q = Priority_Queue()
Q = V
while Q is not empty
u -> [Link]
for each v adjacent to i
if v belongs to Q and Edge(u,v) < key[v] // here Edge(u, v) represents
// cost of edge(u, v)
parent[v] := u
key[v] := Edge(u, v)
end if
end for
end while
Ici key [] stocke le coût minimum de traverser node-v . parent [] est utilisé pour stocker le nœud
parent. C'est utile pour parcourir et imprimer l'arbre.
import [Link].*;
[Link] 176
if ( r[i] == false )
return i;
return -1;
}
public void Prim( )
{
int i, j, k, x, y;
boolean[] Reached = new boolean[NNodes];
int[] predNode = new int[NNodes];
Reached[0] = true;
for ( k = 1; k < NNodes; k++ )
{
Reached[k] = false;
}
predNode[0] = 0;
printReachSet( Reached );
for (k = 1; k < NNodes; k++)
{
x = y = 0;
for ( i = 0; i < NNodes; i++ )
for ( j = 0; j < NNodes; j++ )
{
if ( Reached[i] && !Reached[j] &&
LinkCost[i][j] < LinkCost[x][y] )
{
x = i;
y = j;
}
}
[Link]("Min cost edge: (" +
+ x + "," +
+ y + ")" +
"cost = " + LinkCost[x][y]);
predNode[y] = x;
Reached[y] = true;
printReachSet( Reached );
[Link]();
}
int[] a= predNode;
for ( i = 0; i < NNodes; i++ )
[Link]( a[i] + " --> " + i );
}
void printReachSet(boolean[] Reached )
{
[Link]("ReachSet = ");
for (int i = 0; i < [Link]; i++ )
if ( Reached[i] )
[Link]( i + " ");
//[Link]();
}
public static void main(String[] args)
{
int[][] conn = {{0,3,0,2,0,0,0,0,4}, // 0
{3,0,0,0,0,0,0,4,0}, // 1
{0,0,0,6,0,1,0,2,0}, // 2
{2,0,6,0,1,0,0,0,0}, // 3
{0,0,0,1,0,0,0,0,8}, // 4
{0,0,1,0,0,0,8,0,0}, // 5
{0,0,0,0,0,8,0,0,0}, // 6
{0,4,2,0,0,0,0,0,0}, // 7
{4,0,0,0,8,0,0,0,0} // 8
[Link] 177
};
Graph G = new Graph(conn);
[Link]();
}
}
Sortie:
$ java Graph
* 3 * 2 * * * * 4
3 * * * * * * 4 *
* * * 6 * 1 * 2 *
2 * 6 * 1 * * * *
* * * 1 * * * * 8
* * 1 * * * 8 * *
* * * * * 8 * * *
* 4 2 * * * * * *
4 * * * 8 * * * *
ReachSet = 0 Min cost edge: (0,3)cost = 2
ReachSet = 0 3
Min cost edge: (3,4)cost = 1
ReachSet = 0 3 4
Min cost edge: (0,1)cost = 3
ReachSet = 0 1 3 4
Min cost edge: (0,8)cost = 4
ReachSet = 0 1 3 4 8
Min cost edge: (1,7)cost = 4
ReachSet = 0 1 3 4 7 8
Min cost edge: (7,2)cost = 2
ReachSet = 0 1 2 3 4 7 8
Min cost edge: (2,5)cost = 1
ReachSet = 0 1 2 3 4 5 7 8
Min cost edge: (5,6)cost = 8
ReachSet = 0 1 2 3 4 5 6 7 8
0 --> 0
0 --> 1
7 --> 2
0 --> 3
3 --> 4
2 --> 5
5 --> 6
1 --> 7
0 --> 8
[Link] 178
Chapitre 33: La plus longue sous-séquence
commune
Examples
Plus longue explication de sous-séquence commune
Sous-séquence:
Une sous-séquence est une séquence qui peut être dérivée d'une autre séquence en supprimant
certains éléments sans modifier l'ordre des éléments restants. Disons que nous avons une chaîne
ABC . Si nous effaçons zéro ou un ou plusieurs caractères de cette chaîne, nous obtenons la
sous-séquence de cette chaîne. Les sous-séquences de la chaîne ABC seront donc { "A" , "B" ,
"C" , "AB" , "AC" , "BC" , "ABC" , "" }. Même si nous supprimons tous les caractères, la chaîne
vide sera également une sous-séquence. Pour trouver la sous-séquence, pour chaque caractère
d'une chaîne, nous avons deux options: soit nous prenons le caractère, soit nous ne le faisons
pas. Donc, si la longueur de la chaîne est n , il y a 2 n sous- séquences de cette chaîne.
Comme son nom l'indique, de toutes les sous-séquences communes entre deux chaînes, la plus
longue sous-séquence commune (LCS) est celle qui a la longueur maximale. Par exemple: Les
sous-séquences communes entre "HELLOM" et "HMLD" sont "H" , "HL" , "HM", etc. Ici, "HLL"
est la plus longue sous-séquence commune de longueur 3.
Méthode Brute-Force:
Nous pouvons générer toutes les sous-séquences de deux chaînes en utilisant le backtracking .
Ensuite, nous pouvons les comparer pour trouver les sous-séquences communes. Après nous
aurons besoin de trouver celui avec la longueur maximale. Nous avons déjà vu que, il y a 2 n sous-
séquences d'une chaîne de longueur n . Il faudrait des années pour résoudre le problème si notre
n traverse 20-25.
Abordons notre méthode avec un exemple. Supposons que nous ayons deux chaînes abcdaf et
acbcf . Notons ceux-ci avec s1 et s2 . Donc, la plus longue sous-séquence commune de ces
deux chaînes sera "abcf" , qui a la longueur 4. Encore une fois, je vous le rappelle, les sous-
séquences ne doivent pas nécessairement être continues dans la chaîne. Pour construire "abcf" ,
nous avons ignoré "da" dans s1 et "c" dans s2 . Comment trouvons-nous cela en utilisant la
programmation dynamique?
[Link] 179
Nous allons commencer avec une table (un tableau 2D) ayant tous les caractères de s1 dans une
ligne et tous les caractères de s2 dans la colonne. Ici, la table est indexée sur 0 et nous mettons
les caractères de 1 à plus. Nous allons parcourir la table de gauche à droite pour chaque ligne.
Notre table ressemblera à:
0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch | | a | b | c | d | a | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0 | | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | a | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | c | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | b | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | c | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5 | f | | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
Ici, chaque ligne et chaque colonne représente la longueur de la plus longue sous-séquence
commune entre deux chaînes si nous prenons les caractères de cette ligne et de cette colonne et
les ajoutons au préfixe précédent. Par exemple: La table [2] [3] représente la longueur de la plus
longue sous-séquence commune entre "ac" et "abc" .
0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch | | a | b | c | d | a | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | a | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | c | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | b | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | c | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5 | f | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
Commençons. Lorsque nous remplissons la table [1] [1] , nous nous demandons si nous avons
une chaîne a et une autre chaîne a et rien d'autre, quelle sera la plus longue sous-séquence
commune ici? La longueur du LCS sera de 1. Regardons maintenant le tableau [1] [2] . Nous
avons une chaîne ab et une chaîne a . La longueur du LCS sera 1. Comme vous pouvez le voir,
[Link] 180
le reste des valeurs sera également 1 pour la première ligne car il ne considère que la chaîne a
avec abcd , abcda , abcdaf . Donc, notre table ressemblera à:
0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch | | a | b | c | d | a | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | c | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | b | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | c | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5 | f | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
Pour la ligne 2, qui inclura désormais c . Pour le tableau [2] [1], nous avons ac d'un côté et a de
l'autre côté. La longueur du LCS est donc de 1. Où avons-nous obtenu ce 1? Du haut, qui désigne
le LCS a entre deux sous-chaînes. Donc, ce que nous disons, c'est que si s1 [2] et s2 [1] ne sont
pas identiques, alors la longueur du LCS sera le maximum de la longueur de LCS en haut ou à
gauche . En prenant la longueur du LCS en haut, cela signifie que nous ne prenons pas le
caractère actuel de s2 . De même, la prise en compte de la longueur du LCS à gauche indique
que nous ne prenons pas le caractère actuel de s1 pour créer le LCS. On a:
0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch | | a | b | c | d | a | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | c | 0 | 1 | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | b | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | c | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5 | f | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
Passons maintenant à la table [2] [2], nous avons la chaîne ab et ac . Puisque c et b ne sont pas
les mêmes, nous mettons le maximum du haut ou de gauche ici. Dans ce cas, c'est encore 1.
Après cela, pour la Table [2] [3], nous avons la chaîne abc et ac . Cette fois, les valeurs actuelles
[Link] 181
des lignes et des colonnes sont identiques. Maintenant, la longueur de LCS sera égale à la
longueur maximale de LCS jusqu'à présent. 1. Comment pouvons-nous obtenir la longueur
maximale de LCS jusqu'à présent? Nous vérifions la valeur diagonale, qui représente la meilleure
correspondance entre ab et a . A partir de cet état, pour les valeurs actuelles, nous avons ajouté
un caractère supplémentaire à s1 et s2, qui se sont avérés identiques. Ainsi, la longueur de LCS
va bien sûr augmenter. Nous allons mettre 1 + 1 = 2 dans le tableau [2] [3] . On a,
0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch | | a | b | c | d | a | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
+-----+-----+-----+-----+-----+-----+-----+-----+
2 | c | 0 | 1 | 1 | 2 | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
3 | b | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
4 | c | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
5 | f | 0 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
Nous avons défini les deux cas. En utilisant ces deux formules, nous pouvons remplir toute la
table. Après avoir rempli la table, cela ressemblera à ceci:
0 1 2 3 4 5 6
+-----+-----+-----+-----+-----+-----+-----+-----+
| ch | | a | b | c | d | a | f |
+-----+-----+-----+-----+-----+-----+-----+-----+
0 | | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+-----+-----+-----+-----+-----+-----+-----+-----+
1 | a | 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 |
+-----+-----+-----+-----+-----+-----+-----+-----+
La longueur du LCS entre s1 et s2 sera la table [5] [6] = 4 . Ici, 5 et 6 sont respectivement la
longueur de s2 et s1 . Notre pseudo-code sera:
[Link] 182
Table[0][0] = 0
for i from 1 to [Link]
Table[0][i] = 0
endfor
for i from 1 to [Link]
Table[i][0] = 0
endfor
for i from 1 to [Link]
for j from 1 to [Link]
if s2[i] equals to s1[j]
Table[i][j] = Table[i-1][j-1] + 1
else
Table[i][j] = max(Table[i-1][j], Table[i][j-1])
endif
endfor
endfor
Return Table[[Link]][[Link]]
Point à noter: si le tableau [i-1] [j] et le tableau [i] [j-1] sont égaux au tableau [i] [j] et le tableau
[i-1] [j-1] n’est pas égale à la table [i] [j] - 1 , il peut y avoir deux LCS pour ce moment. Ce
pseudo-code ne considère pas cette situation. Vous devrez résoudre ce problème de manière
récursive pour trouver plusieurs LCS.
[Link] 183
La complexité temporelle de cet algorithme est la suivante: O (max (m, n)) .
[Link] 184
Chapitre 34: Le triangle de Pascal
Examples
Pascal's Triagle Informations de base
L'un des modèles de nombres les plus intéressants est le triangle de Pascal . Le Nom "Triangle de
Pascal" nommé d'après Blaise Pascal , célèbre mathématicien et philosophe français.
Par exemple, le nombre initial dans la première (ou toute autre) ligne est 1 (la somme de 0 et 1),
tandis que les numéros 1 et 3 dans la troisième ligne sont ajoutés pour produire le numéro 4 dans
la quatrième ligne.
pour tout entier non négatif n et tout entier k compris entre 0 et n inclus. Cette récurrence pour les
coefficients binomiaux est connue sous le nom de règle de Pascal . Le triangle de Pascal présente
des généralisations dimensionnelles plus élevées. La version tridimensionnelle s'appelle la
pyramide de Pascal ou le tétraèdre de Pascal, alors que les versions générales sont appelées les
simplices de Pascal.
[Link] 185
Implémentation du triangle de Pascal en C #
Triangle de Pascal en C
while(k != 2*i-1)
{
if (count <= rows-1)
{
printf("%d ", i+k);
[Link] 186
++count;
}
else
{
++count1;
printf("%d ", (i+k-2*count1));
}
++k;
}
count1 = count = k = 0;
printf("\n");
}
Sortie
1
2 3 2
3 4 5 4 3
4 5 6 7 6 5 4
5 6 7 8 9 8 7 6 5
[Link] 187
Chapitre 35: Modifier l'algorithme dynamique
de distance
Examples
Modifications minimales requises pour convertir la chaîne 1 en chaîne 2
L'énoncé du problème est comme si on nous donnait deux chaînes str1 et str2, puis combien de
nombres minimum d'opérations peuvent être effectuées sur la str1 qu'elle convertit en str2. Les
opérations peuvent être:
1. Insérer
2. Retirer
3. Remplacer
Par exemple
Pour dp [1] [1] , nous devons vérifier que pouvons - nous faire pour convertir un en sera de 0 .Il
dp .Pour [1] [2] , nous devons vérifier que pouvons - nous faire pour convertir un en ab .It sera 1
parce que nous devons insérer b . Donc, après la 1ère itération, notre tableau ressemblera à
[Link] 188
(a) (b) (c) (d) (e) (f)
+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | | | | | | |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+
Pour itération 2
Pour dp [2] [1] , nous devons vérifier que convertir az à nous devons supprimer z, d' où dp [2] [1]
sera 1 .Similary pour dp [2] [2] nous devons remplacer z avec b , donc dp [2] [2] sera 1. Après la
2ème itération, notre tableau dp [] ressemblera.
[Link] 189
+---+---+---+---+---+---+---+
(f)| 5 | 4 | 4 | 2 | 3 | 3 | 3 |
+---+---+---+---+---+---+---+
Implémentation en Java
La complexité du temps
O(n^2)
[Link] 190
Chapitre 36: Notation Big-O
Remarques
Définition
La notation Big-O est au cœur d'une notation mathématique, utilisée pour comparer le taux de
convergence des fonctions. Soit n -> f(n) et n -> g(n) fonctions définies sur les nombres naturels.
On dit alors que f = O(g) si et seulement si f(n)/g(n) est borné quand n s'approche de l'infini. En
d'autres termes, f = O(g) si et seulement s'il existe une constante A, telle que pour tout n,
f(n)/g(n) <= A
En réalité, la portée de la notation Big-O est un peu plus large en mathématiques, mais pour
simplifier, je l'ai réduite à ce qui est utilisé dans l'analyse de la complexité des algorithmes: les
fonctions définies sur les à l'infini.
Prenons le cas de f(n) = 100n^2 + 10n + 1 et g(n) = n^2 . Il est clair que ces deux fonctions
tendent à l'infini lorsque n tend vers l'infini. Mais parfois, connaître la limite ne suffit pas, et nous
voulons également connaître la vitesse à laquelle les fonctions se rapprochent de leur limite. Des
notions telles que Big-O permettent de comparer et de classer les fonctions selon leur vitesse de
convergence.
Voyons si f = O(g) en appliquant la définition. Nous avons f(n)/g(n) = 100 + 10/n + 1/n^2 . Depuis
10/n est 10 lorsque n est égal à 1 et diminue, et que 1/n^2 est égal à 1 lorsque n est égal à 1 et est
également diminué, on a F f(n)/g(n) <= 100 + 10 + 1 = 111 . La définition est satisfaite car nous
avons trouvé une limite de f(n)/g(n) (111) et donc f = O(g) (on dit que f est un Big-O de n^2 ).
Cela signifie que f tend vers l'infini approximativement à la même vitesse que g. Cela peut
sembler étrange à dire, car nous avons trouvé que f est au plus 111 fois plus grand que g, ou en
d'autres termes lorsque g augmente de 1, f augmente d'au plus 111. Il peut sembler croissant que
111 fois plus rapide n'est pas "approximativement la même vitesse". Et en effet, la notation Big-O
n'est pas un moyen très précis de classer la vitesse de convergence des fonctions. C'est
pourquoi, en mathématiques, nous utilisons la relation d'équivalence lorsque nous voulons une
estimation précise de la vitesse. Mais pour séparer les algorithmes dans les grandes classes de
vitesse, Big-O est suffisant. Nous n'avons pas besoin de séparer les fonctions qui se développent
un nombre de fois plus rapide que les autres, mais uniquement des fonctions qui augmentent
infiniment plus rapidement les unes que les autres. Par exemple si on prend h(n) = n^2*log(n) , on
voit que h(n)/g(n) = log(n) qui tend vers l'infini avec n donc h n'est pas O (n ^ 2), car h pousse
infiniment plus vite que n ^ 2.
Maintenant, je dois faire une remarque: vous avez peut-être remarqué que si f = O(g) et g = O(h) ,
alors f = O(h) . Par exemple, dans notre cas, nous avons f = O(n^3) et f = O(n^4) ... Dans
l’analyse de la complexité de l’algorithme, nous disons fréquemment que f = O(g) signifie que f =
O(g) et g = O(f) , ce qui peut être compris comme "g est le plus petit Big-O pour f". En
mathématiques, nous disons que ces fonctions sont les Big Thetas les unes des autres.
[Link] 191
Comment est-ce utilisé ?
Lorsque nous comparons les performances de l'algorithme, nous nous intéressons au nombre
d'opérations effectuées par un algorithme. Cela s'appelle la complexité du temps . Dans ce
modèle, nous considérons que chaque opération de base (addition, multiplication, comparaison,
affectation, etc.) prend un temps fixe et nous comptons le nombre de ces opérations. Nous
pouvons généralement exprimer ce nombre en fonction de la taille de l'entrée, que nous appelons
n. Et malheureusement, ce nombre augmente généralement à l'infini avec n (si ce n'est pas le
cas, nous disons que l'algorithme est O (1)). Nous séparons nos algorithmes en grandes classes
de vitesse définies par Big-O: lorsque nous parlons d'un "algorithme O (n ^ 2)", nous voulons dire
que le nombre d'opérations qu'il exécute, exprimé en fonction de n, est un O ( n ^ 2). Ceci dit que
notre algorithme est approximativement aussi rapide qu'un algorithme qui ferait un nombre
d'opérations égal au carré de la taille de son entrée, ou plus rapide . La partie "ou plus rapide" est
là parce que j'ai utilisé Big-O au lieu de Big-Theta, mais généralement, les gens diront Big-O pour
Big-Theta.
Lors du comptage d'opérations, nous considérons généralement le pire des cas: par exemple, si
nous avons une boucle qui peut s'exécuter au plus n fois et qui contient 5 opérations, le nombre
d'opérations que nous comptons est 5n. Il est également possible de considérer la complexité
moyenne des cas.
Note rapide: un algorithme rapide est celui qui effectue peu d'opérations, donc si le nombre
d'opérations augmente à l'infini plus rapidement , alors l'algorithme est plus lent : O (n) est
meilleur que O (n ^ 2).
Nous sommes aussi parfois intéressés par la complexité spatiale de notre algorithme. Pour cela,
considérons le nombre d'octets en mémoire occupés par l'algorithme en fonction de la taille de
l'entrée, et utilisons Big-O de la même manière.
Examples
Une boucle simple
La taille d'entrée est la taille du tableau, que j'ai appelée len dans le code.
[Link] 192
int max = INT_MIN;
size_t i = 0;
Ces deux affectations ne sont effectuées qu’une seule fois, c’est donc 2 opérations. Les
opérations en boucle sont les suivantes:
Comme il y a 3 opérations dans la boucle et que la boucle est effectuée n fois, nous ajoutons 3n à
nos 2 opérations déjà existantes pour obtenir 3n + 2 . Donc, notre fonction prend 3n + 2 opérations
pour trouver le max (sa complexité est 3n + 2 ). C'est un polynôme où le terme de croissance le
plus rapide est un facteur de n, donc il est O (n).
Vous avez probablement remarqué que "l'opération" n'est pas très bien définie. Par exemple, j'ai
dit que if (max < array[i]) était une opération, mais en fonction de l'architecture, cette instruction
peut compiler par exemple trois instructions: une lecture de mémoire, une comparaison et une
branche. J'ai également considéré toutes les opérations comme identiques, même si, par
exemple, les opérations de mémoire seront plus lentes que les autres et que leurs performances
varieront énormément, par exemple en raison des effets de cache. J'ai aussi complètement ignoré
la déclaration de retour, le fait qu'une image sera créée pour la fonction, etc. Finalement, l'analyse
de la complexité n'a pas d'importance, car peu importe comment je compte les opérations, cela ne
changera que le coefficient. du facteur n et de la constante, le résultat sera toujours O (n). La
complexité montre comment l'algorithme s'adapte à la taille de l'entrée, mais ce n'est pas le seul
aspect des performances!
La fonction suivante vérifie si un tableau a des doublons en prenant chaque élément, puis effectue
une itération sur l'ensemble du tableau pour voir si l'élément est présent
La boucle interne effectue à chaque itération un nombre d'opérations constant avec n . La boucle
externe effectue également quelques opérations constantes et exécute la boucle interne n fois. La
boucle externe elle-même est exécutée n fois. Donc, les opérations à l'intérieur de la boucle
interne sont exécutées n^2 fois, les opérations dans la boucle externe sont exécutées n fois et
l'affectation à i est effectuée une fois. Ainsi, la complexité sera quelque chose comme an^2 + bn +
c , et comme le terme le plus élevé est n^2 , la notation O(n^2) est O(n^2) .
[Link] 193
Comme vous l'avez peut-être remarqué, nous pouvons améliorer l'algorithme en évitant de faire
les mêmes comparaisons plusieurs fois. Nous pouvons commencer à partir de i + 1 dans la
boucle interne, car tous les éléments qui l'ont précédé auront déjà été vérifiés par rapport à tous
les éléments du tableau, y compris celui de l'index i + 1 . Cela nous permet de laisser tomber la
vérification i == j .
De toute évidence, cette deuxième version fait moins d'opérations et est donc plus efficace.
Comment cela se traduit-il par la notation Big-O? Eh bien, le corps de la boucle interne est
maintenant exécuté 1 + 2 + ... + n - 1 = n(n-1)/2 fois. C'est toujours un polynôme du deuxième
degré, et il en est toujours de même pour O(n^2) . Nous avons clairement réduit la complexité,
puisque nous avons divisé par 2 approximativement le nombre d'opérations que nous effectuons,
mais nous sommes toujours dans la même classe de complexité que celle définie par Big-O. Afin
de réduire la complexité à une classe inférieure, il faudrait diviser le nombre d'opérations par
quelque chose qui tend vers l'infini avec n .
Un exemple O (log n)
introduction
Considérez le problème suivant:
L est une liste triée contenant n entiers signés ( n étant assez grand), par exemple [-5, -2, -1, 0,
1, 2, 4] (ici, n vaut 7). Si L est connu pour contenir le nombre entier 0, comment pouvez-vous
trouver l'index de 0?
Approche naïve
La première chose qui me vient à l'esprit est de lire chaque index jusqu'à ce que 0 soit trouvé.
Dans le pire des cas, le nombre d'opérations est n , la complexité est donc O (n).
Cela fonctionne très bien pour les petites valeurs de n , mais existe-t-il un moyen plus efficace?
Dichotomie
Considérons l'algorithme suivant (Python3):
a = 0
[Link] 194
b = n-1
while True:
h = (a+b)//2 ## // is the integer division, so h is an integer
if L[h] == 0:
return h
elif L[h] > 0:
b = h
elif L[h] < 0:
a = h
aet b sont les index entre lesquels 0 doit être trouvé. Chaque fois que nous entrons dans la
boucle, nous utilisons un index entre a et b et l'utilisons pour affiner la zone à rechercher.
Dans le pire des cas, il faut attendre que a et b soient égaux. Mais combien d'opérations cela
prend-il? Pas n, car chaque fois que nous entrons dans la boucle, nous divisons la distance entre
a et b d'environ deux. Au contraire, la complexité est O (log n).
Explication
Note: Lorsque nous écrivons "log", nous entendons le logarithme binaire, ou log base 2 (que nous
écrirons "log_2"). Comme O (log_2 n) = O (log n) (vous pouvez faire le calcul), nous utiliserons
"log" au lieu de "log_2".
Conclusion
Face à des divisions successives (que ce soit par deux ou par un nombre quelconque), rappelez-
vous que la complexité est logarithmique.
Disons que nous avons un problème de taille n. Maintenant, pour chaque étape de notre
algorithme (que nous avons besoin d'écrire), notre problème initial devient moitié de sa taille
précédente (n / 2).
Étape Problème
1 n/2
2 n/4
3 n/8
4 n / 16
[Link] 195
Lorsque l'espace problème est réduit (c.-à-d. Complètement résolu), il ne peut plus être réduit (n
devient égal à 1) après avoir quitté la condition de contrôle.
taille du problème = 1
2. Mais nous savons à la cinquième étape, notre taille de problème devrait être:
taille du problème = n / 2 k
3. Du 1 et 2:
n / 2 k = 1 ou
n=2k
log e n = k log e 2
ou
k = log e n / log e 2
k = log 2 n
ou simplement k = log n
Maintenant, nous savons que notre algorithme peut fonctionner au maximum jusqu'à
log n, d'où la complexité du temps.
O (log n)
Un exemple très simple dans le code pour supporter le texte ci-dessus est:
Alors maintenant, si quelqu'un vous demande si n est 256, combien de pas cette boucle (ou tout
autre algorithme qui réduit la taille de son problème en deux) sera exécutée très facilement.
k = log 2 256
k=8
[Link] 196
Un autre très bon exemple pour un cas similaire est l' algorithme de recherche binaire .
while(low<=high){
mid=low+(high-low)/2;
if(arr[mid]==item)
return mid;
else if(arr[mid]<item)
low=mid+1;
else high=mid-1;
}
return –1;// Unsuccessful result
}
[Link] 197
Chapitre 37: Plus longue sous-séquence
croissante
Examples
Information de base sur la sous-séquence croissante la plus longue
Des algorithmes tels que la sous-séquence croissante la plus longue, la sous-séquence commune
la plus longue sont utilisés dans les systèmes de contrôle de version tels que Git, etc.
Considérons maintenant un exemple plus simple du problème de LCS. Ici, input n'est qu'une
séquence d'entiers distincts a1,a2,...,an. , et nous voulons y trouver la sous-séquence croissante
la plus longue. Par exemple, si l’entrée est 7,3,8,4,2,6 alors la plus longue sous-séquence
croissante est 3,4,6 .
L'approche la plus simple consiste à trier les éléments d'entrée dans un ordre croissant et à
appliquer l'algorithme LCS aux séquences d'origine et aux séquences triées. Cependant, si vous
regardez le tableau résultant, vous remarquerez que de nombreuses valeurs sont identiques et
que le tableau est très répétitif. Cela suggère que le problème de LIS (sous-séquence augmentant
le plus longtemps) peut être résolu avec un algorithme de programmation dynamique utilisant
uniquement un tableau à une dimension.
Pseudo Code:
[Link] 198
2. Donner une récurrence.
Pour 1 <= i <= n , A(i) = 1 + max{A(j)|1 ≤ j < i et input(j) < input(i)}.
3. Calculer les valeurs de A.
4. Trouvez la solution optimale.
Le programme suivant utilise A pour calculer une solution optimale. La première partie calcule une
valeur m telle que A (m) est la longueur d'une sous-séquence croissante optimale d'entrée. La
seconde partie calcule une sous-séquence croissante optimale, mais pour plus de commodité,
nous l'imprimons dans l'ordre inverse. Ce programme s'exécute dans le temps O (n), donc tout
l'algorithme s'exécute dans le temps O (n ^ 2).
Partie 1:
m ← 1
for i : 2..n
if A(i) > A(m) then
m ← i
end if
end for
Partie 2:
put a
while A(m) > 1 do
i ← m−1
while not(ai < am and A(i) = A(m)−1) do
i ← i−1
end while
m ← i
put a
end while
Solution récursive:
Approche 1:
LIS(A[1..n]):
if (n = 0) then return 0
m = LIS(A[1..(n − 1)])
B is subsequence of A[1..(n − 1)] with only elements less than a[n]
(* let h be size of B, h ≤ n-1 *)
m = max(m, 1 + LIS(B[1..h]))
Output m
Approche 2:
LIS(A[1..n], x):
if (n = 0) then return 0
m = LIS(A[1..(n − 1)], x)
if (A[n] < x) then
m = max(m, 1 + LIS(A[1..(n − 1)], A[n]))
[Link] 199
Output m
MAIN(A[1..n]):
return LIS(A[1..n], ∞)
Approche 3:
LIS(A[1..n]):
if (n = 0) return 0
m = 1
for i = 1 to n − 1 do
if (A[i] < A[n]) then
m = max(m, 1 + LIS(A[1..i]))
return m
MAIN(A[1..n]):
return LIS(A[1..i])
Algorithme itératif:
LIS(A[1..n]):
Array L[1..n]
(* L[i] = value of LIS ending(A[1..i]) *)
for i = 1 to n do
L[i] = 1
for j = 1 to i − 1 do
if (A[j] < A[i]) do
L[i] = max(L[i], 1 + L[j])
return L
MAIN(A[1..n]):
L = LIS(A[1..n])
return the maximum value in L
Prenons en entrée {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15} . Donc, la plus longue sous-
séquence croissante pour l'entrée donnée est {0, 2, 6, 9, 11, 15} .
Implémentation C #
[Link] 200
int max = 0;
for(int i = 0; i < n; i++)
{
lis[i] = 1;
}
for (int i = 1; i < n; i++)
{
for (int j = 0; j < i; j++)
{
if (input[i] > input[j] && lis[i] < lis[j] + 1)
lis[i] = lis[j] + 1;
}
}
for (int i = 0; i < n; i++)
{
if (max < lis[i])
max = lis[i];
}
return max;
}
[Link] 201
Chapitre 38: Problème de sac à dos
Remarques
Le problème de Knapsack se pose principalement dans les mécanismes d’allocation des
ressources. Le nom "Knapsack" a été introduit pour la première fois par Tobias Dantzig .
Examples
Les bases du problème du sac à dos
Le problème : Étant donné un ensemble d'éléments où chaque élément contient un poids et une
valeur, déterminez le nombre de chacun à inclure dans une collection afin que le poids total soit
inférieur ou égal à une limite donnée et que la valeur totale soit aussi grande que possible. .
Donné:
1. Valeurs (tableau v)
2. Poids (tableau w)
3. Nombre d'éléments distincts (n)
4. Capacité (W)
[Link] 202
val = [60, 100, 120]
wt = [10, 20, 30]
W = 50
n = len(val)
print(knapSack(W, wt, val, n))
$ python [Link]
220
Time Complexité du code ci-dessus: O(nW) où n est le nombre d'éléments et W la capacité du sac
à dos.
Solution implémentée en C #
[Link] 203
Chapitre 39: Problème de supersquence
commun le plus court
Examples
Problème de supersquence commune le plus court
La séquence la plus courte commune est un problème étroitement lié à la plus longue sous-
séquence commune, que vous pouvez utiliser comme fonction externe pour cette tâche. Le
problème de la super séquence commune le plus court est un problème étroitement lié au plus
long problème de sous-séquence commun.
Une supersquence commune la plus courte (scs) est une supersquence commune de longueur
minimale. Dans le plus court problème de supersquence commun, les deux séquences X et Y sont
données et la tâche est de trouver une supersquence commune la plus courte possible de ces
séquences. En général, un scs n'est pas unique.
Pour deux séquences d'entrée, un scs peut être formé à partir d'une plus longue sous-séquence
commune (lcs) facilement. Par exemple, si X[1..m]=abcbdab et Y[1..n]=bdcaba , le lcs est
Z[1..r]=bcba . En insérant les symboles non-lcs tout en préservant l'ordre des symboles, on obtient
les scs: U[1..t]=abdcabdab .
Il est clair que r+t=m+n pour deux séquences d'entrée. Cependant, pour trois séquences d'entrée
ou plus, cela ne tient pas. Notez également que les problèmes lcs et scs ne sont pas des
problèmes doubles.
Pour le problème plus général de trouver une chaîne, S qui est une superstring d'un ensemble de
chaînes S1,S2,...,Sl , le problème est NP-Complete. En outre, de bonnes approximations
peuvent être trouvées pour le cas moyen mais pas pour le pire des cas.
[Link] 204
Complexité temporelle: O(max(m,n))
[Link] 205
public static int Main(string x, string y)
{
return Scs(x, y);
}
}
[Link] 206
Chapitre 40: Programmation dynamique
Introduction
La programmation dynamique est un concept largement utilisé et souvent utilisé pour
l’optimisation. Il s'agit de simplifier un problème compliqué en le décomposant en sous-problèmes
plus simples de manière récursive, généralement l'approche Bottom up. Il y a deux attributs clés
qu'un problème doit avoir pour que la programmation dynamique soit applicable "Sous-structure
optimale" et "Sous-problèmes chevauchants". Pour parvenir à son optimisation, la programmation
dynamique utilise un concept appelé Mémorisation.
Remarques
La programmation dynamique est une amélioration de Brute Force. Voir cet exemple pour
comprendre comment obtenir une solution de programmation dynamique de Brute Force.
1. Problèmes de chevauchement
2. Structure optimale
Les sous-problèmes qui se chevauchent signifient que les résultats des versions plus petites du
problème sont réutilisés plusieurs fois pour arriver à la solution du problème initial
La sous-structure optimale signifie qu'il existe une méthode de calcul d'un problème à partir de
ses sous-problèmes.
La transition est la méthode pour résoudre un problème basé sur ses sous-problèmes
Le temps pris par une solution de programmation dynamique peut être calculé en nombre No. of
States * Transition Time . Ainsi, si une solution a N^2 états et que la transition est O(N) , alors la
solution prendrait approximativement le temps O(N^3) .
Examples
Problème de sac à dos
Le problème de sac à dos est un problème lorsqu'il est donné un ensemble d'éléments, chacun
[Link] 207
avec un poids, une valeur et exactement 1 copie , déterminer quel (s) élément (s) inclure dans
une collection de sorte que le poids total soit inférieur ou égal à un limite et la valeur totale est
aussi grande que possible.
Exemple C ++:
Mise en œuvre :
Test :
3 5
5 2
2 1
3 2
Sortie :
Cela signifie que la valeur maximale peut être atteinte est 3, ce qui est obtenu en choisissant (2,1)
et (3,2).
Le problème de sac à dos sans limite est un problème qui, avec un ensemble d'éléments, chacun
avec un poids, une valeur et des copies infinies , détermine le nombre de chaque élément à
inclure dans une collection de sorte que le poids total soit inférieur ou égal à une limite donnée et
la valeur totale est la plus grande possible.
[Link] 208
for r in range(1, c+1):
val = m[r-1]
for i, wi in enumerate(w):
if wi > r:
continue
val = max(val, v[i] + m[r-wi])
[Link](val)
return m[c] # return the maximum value can be achieved
Test :
w = [2, 3, 4, 5, 6]
v = [2, 4, 6, 8, 9]
Sortie :
20
Cela signifie que la valeur maximale pouvant être atteinte est de 20, obtenue en choisissant (5, 8),
(5, 8) et (3, 4).
L'algorithme de planification des travaux pondérés peut également être appelé algorithme de
sélection d'activité pondérée.
Le problème est que, compte tenu de certains emplois avec leur heure de début et de fin, et un
bénéfice que vous faites lorsque vous avez terminé le travail, quel est le profit maximum que vous
pouvez faire étant donné qu'aucun travail ne peut être exécuté en parallèle?
Regardons un exemple:
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | A | B | C | D | E | F |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (2,5) | (6,7) | (7,9) | (1,3) | (5,8) | (4,6) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 6 | 4 | 2 | 5 | 11 | 5 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Les emplois sont désignés par un nom, leur heure de début et de fin et leur profit. Après quelques
itérations, nous pouvons savoir si nous réalisons Job-A et Job-E , nous pouvons obtenir un profit
maximum de 17. Maintenant, comment le découvrir en utilisant un algorithme?
[Link] 209
La première chose à faire est de trier les travaux en fonction de leur temps d’arrivée dans un ordre
non décroissant. Pourquoi faisons-nous cela? C'est parce que si nous choisissons un travail qui
prend moins de temps pour finir, nous laissons le plus de temps possible pour choisir d'autres
emplois. Nous avons:
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Nous aurons un tableau temporaire supplémentaire Acc_Prof de taille n (Ici, n désigne le nombre
total de travaux). Cela contiendra le profit maximum accumulé de l'exécution des travaux. Ne
comprends pas? Attendez et regardez. Nous allons initialiser les valeurs du tableau avec le profit
de chaque travail. Cela signifie que , Acc_Prof [i] sera tout d' abord maintenir le bénéfice
d'effectuer travail i-ème.
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Notons maintenant la position 2 avec i et la position 1 avec j . Notre stratégie sera d'itérer j de 1
à i-1 et après chaque itération, nous incrémenterons i de 1 jusqu'à ce que i devienne n + 1 .
j i
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Nous vérifions si les tâches [i] et la tâche [j] se chevauchent, c’est-à-dire que si l’ heure de fin
de la tâche [j] est supérieure à celle de la tâche [i] , ces deux tâches ne peuvent pas être
effectuées ensemble. Cependant, s'ils ne se chevauchent pas, nous vérifierons si Acc_Prof [j] +
Profit [i]> Acc_Prof [i] . Si tel est le cas, nous mettrons à jour Acc_Prof [i] = Acc_Prof [j] +
Profit [i] . C'est:
Ici, Acc_Prof [j] + Profit [i] représente le bénéfice cumulé de la réalisation de ces deux tâches.
Vérifions-le pour notre exemple:
[Link] 210
Ici Job [j] chevauche Job [i] . Donc, ils ne peuvent pas être faits ensemble. Puisque notre j est
égal à i-1 , nous incrémentons la valeur de i à i + 1 soit 3 . Et on fait j = 1 .
j i
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Maintenant, Job [j] et Job [i] ne se chevauchent pas. Le montant total des bénéfices que nous
pouvons réaliser en choisissant ces deux emplois est le suivant: Acc_Prof [j] + Profit [i] = 5 + 5 =
10 qui est supérieur à Acc_Prof [i] . Nous mettons donc à jour Acc_Prof [i] = 10 . Nous
incrémentons aussi j de 1. Nous obtenons,
j i
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Ici, Job [j] chevauche Job [i] et j est également égal à i-1 . Nous incrémentons donc i de 1 et
faisons j = 1 . On a,
j i
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Maintenant, Job [j] et Job [i] ne se chevauchent pas, nous obtenons le bénéfice cumulé 5 + 4 = 9
, qui est supérieur à Acc_Prof [i] . Nous mettons à jour Acc_Prof [i] = 9 et incrémentons j de 1.
j i
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
[Link] 211
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 9 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Encore une fois, Job [j] et Job [i] ne se chevauchent pas. Le bénéfice accumulé est de: 6 + 4 =
10 , ce qui est supérieur à Acc_Prof [i] . Nous mettons à jour à nouveau Acc_Prof [i] = 10 . Nous
incrémentons j de 1. Nous obtenons:
j i
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 10 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Si nous continuons ce processus, après avoir parcouru toute la table en utilisant i , notre table
ressemblera à ceci:
+-------------------------+---------+---------+---------+---------+---------+---------+
| Name | D | A | F | B | E | C |
+-------------------------+---------+---------+---------+---------+---------+---------+
|(Start Time, Finish Time)| (1,3) | (2,5) | (4,6) | (6,7) | (5,8) | (7,9) |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Profit | 5 | 6 | 5 | 4 | 11 | 2 |
+-------------------------+---------+---------+---------+---------+---------+---------+
| Acc_Prof | 5 | 6 | 10 | 14 | 17 | 8 |
+-------------------------+---------+---------+---------+---------+---------+---------+
Procedure WeightedJobScheduling(Job)
sort Job according to finish time in non-decreasing order
for i -> 2 to n
for j -> 1 to i-1
if Job[j].finish_time <= Job[i].start_time
if Acc_Prof[j] + Profit[i] > Acc_Prof[i]
Acc_Prof[i] = Acc_Prof[j] + Profit[i]
endif
endif
endfor
endfor
maxProfit = 0
for i -> 1 to n
if maxProfit < Acc_Prof[i]
[Link] 212
maxProfit = Acc_Prof[i]
return maxProfit
Maintenant, si nous voulons savoir quels travaux ont été effectués pour obtenir le maximum de
profit, nous devons parcourir le tableau dans l’ordre inverse et si Acc_Prof correspond au
maxProfit , nous allons pousser le nom du travail dans une pile et soustraire Profit de ce travail
de maxProfit . Nous le ferons jusqu'à notre maxProfit> 0 ou nous atteignons le point de départ
du tableau Acc_Prof . Le pseudo-code ressemblera à:
Une chose à retenir: si plusieurs horaires de travail peuvent nous permettre de réaliser des
bénéfices maximum, nous ne pouvons trouver qu'un seul emploi par cette procédure.
Modifier la distance
L'énoncé du problème est comme si on nous donnait deux chaînes str1 et str2, puis combien de
nombres minimum d'opérations pourraient être exécutés sur la str1 qu'elle serait convertie en str2.
Implémentation en Java
[Link] 213
else{
dp[i][j] = 1 + [Link](dp[i-1][j], [Link](dp[i][j-1], dp[i-1][j-1]));
}
}
}
return dp[[Link]()][[Link]()];
}
Sortie
Si on nous donne les deux chaînes, nous devons trouver la plus longue sous-séquence commune
présente dans les deux.
Exemple
LCS pour les séquences d'entrée "ABCDGH" et "AEDFHR" est "ADH" de longueur 3.
LCS pour les séquences d’entrée “AGGTAB” et “GXTXAYB” est “GTAB” de longueur 4.
Implémentation en Java
//Recursive function
public int lcs(String str1, String str2, int m, int n){
if(m==0 || n==0)
return 0;
if([Link](m-1) == [Link](n-1))
return 1 + lcs(str1, str2, m-1, n-1);
else
return [Link](lcs(str1, str2, m-1, n), lcs(str1, str2, m, n-1));
}
//Iterative function
public int lcs2(String str1, String str2){
int lcs[][] = new int[[Link]()+1][[Link]()+1];
for(int i=0;i<=[Link]();i++){
for(int j=0;j<=[Link]();j++){
if(i==0 || j== 0){
lcs[i][j] = 0;
}
[Link] 214
else if([Link](i-1) == [Link](j-1)){
lcs[i][j] = 1 + lcs[i-1][j-1];
}else{
lcs[i][j] = [Link](lcs[i-1][j], lcs[i][j-1]);
}
}
}
return lcs[[Link]()][[Link]()];
}
Sortie
Numéro de Fibonacci
Arbre récursif
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
Ici, fib (0), fib (1) et fib (3) sont les sous-problèmes qui se [Link] (0) se répète 3 fois, fib
(1) se répète 5 fois et fib (3) se répète 2 fois.
la mise en oeuvre
La complexité du temps
Sur)
[Link] 215
Plus longue sous-chaîne commune
Compte tenu de 2 chaînes str1 et str2, il faut trouver la longueur de la plus longue sous-chaîne
commune entre elles.
Exemples
Implémentation en Java
La complexité du temps
O (m * n)
[Link] 216
Chapitre 41: Pseudocode
Remarques
Le pseudocode est par définition informel. Cette rubrique est destinée à décrire les moyens de
traduire un code spécifique à une langue en quelque chose que tout le monde avec une
expérience en programmation peut comprendre.
Le pseudocode est un moyen important de décrire un algorithme et est plus neutre que de donner
une implémentation spécifique à un langagage. Wikipedia utilise souvent une forme de
pseudocode pour décrire un algorithme
Certaines choses, comme les conditions de type if-else, sont assez faciles à écrire de manière
informelle. Mais d'autres choses, les callbacks de style js, par exemple, peuvent être difficiles à
transformer en pseudocode pour certaines personnes.
Examples
Affectations variables
Dactylographié
int a = 1
int a := 1
let int a = 1
int a <- 1
Aucun type
a = 1
a := 1
let a = 1
a <- 1
Les fonctions
Tant que le nom de la fonction, la déclaration de retour et les paramètres sont clairs, vous allez
bien.
def incr n
return n + 1
[Link] 217
ou
let incr(n) = n + 1
ou
sont tous assez clairs, vous pouvez donc les utiliser. Essayez de ne pas être ambigu avec une
affectation variable
[Link] 218
Chapitre 42: Recherche
Examples
Recherche binaire
introduction
La recherche binaire est un algorithme de recherche de division et de conquête. Il utilise l'heure
O(log n) pour trouver l'emplacement d'un élément dans un espace de recherche où n est la taille
de l'espace de recherche.
La recherche binaire fonctionne en divisant par deux l'espace de recherche à chaque itération
après avoir comparé la valeur cible à la valeur intermédiaire de l'espace de recherche.
Pour utiliser la recherche binaire, l'espace de recherche doit être trié (trié) d'une manière ou d'une
autre. Les entrées en double (qui se comparent comme étant égales selon la fonction de
comparaison) ne peuvent pas être distinguées, bien qu'elles ne violent pas la propriété de
recherche binaire.
Classiquement, nous utilisons moins que (<) comme fonction de comparaison. Si un <b, il
retournera vrai. si a n'est pas inférieur à b et b n'est pas inférieur à a, a et b sont égaux.
Exemple de question
Vous êtes un économiste, un très mauvais. Vous avez la tâche de trouver le prix d'équilibre (c'est-
à-dire le prix où l'offre = la demande) pour le riz.
Rappelez-vous que plus le prix est élevé, plus l'offre est grande et la demande est faible
Votre entreprise étant très efficace dans le calcul des forces du marché, vous pouvez obtenir
instantanément l'offre et la demande en unités de riz lorsque le prix du riz est fixé à un certain prix
p.
Votre patron veut le prix d'équilibre ASAP, mais vous dit que le prix d'équilibre peut être un
nombre entier positif d'au plus 10^17 et qu'il y a exactement 1 solution entière positive dans
l'intervalle. Alors lancez-vous dans votre travail avant de le perdre!
Vous êtes autorisé à appeler les fonctions getSupply(k) et getDemand(k) , qui feront exactement ce
qui est indiqué dans le problème.
Exemple d'explication
[Link] 219
Ici, notre espace de recherche est compris entre 1 et 10^17 . Une recherche linéaire est donc
impossible.
Cependant, notez que lorsque k augmente, getSupply(k) augmente et getDemand(k) diminue. Ainsi,
pour tout x > y , getSupply(x) - getDemand(x) > getSupply(y) - getDemand(y) . Par conséquent, cet
espace de recherche est monotone et nous pouvons utiliser la recherche binaire.
Cet algorithme s'exécute dans le temps ~O(log 10^17) . Ceci peut être généralisé à ~O(log S)
temps où S est la taille de l'espace de recherche depuis à chaque itération de la while boucle,
nous avons réduit de moitié l'espace de recherche ( à partir de [bas: haut] sur [bas: mi] ou [mi:
haut] ).
if (x == a[mid]) {
return (mid);
} else
if (x < a[mid]) {
binsearch(a, x, low, mid - 1);
} else {
binsearch(a, x, mid + 1, high);
}
}
Il est plus facile de montrer une recherche binaire sur des nombres en utilisant un pseudo-code
[Link] 220
int x; // value to search for
low = 0;
high = N -1;
while(low < high)
{
mid = (low + high)/2;
if(array[mid] < x)
low = mid + 1;
else
high = mid;
}
if(array[low] == x)
// found, index is low
else
// not found
N'essayez pas de revenir plus tôt en comparant le tableau [moyen] à x pour l'égalité. La
comparaison supplémentaire ne peut que ralentir le code. Notez que vous devez en ajouter un au
plus bas pour éviter de se laisser piéger par une division entière en arrondissant toujours vers le
bas.
Fait intéressant, la version de recherche binaire ci-dessus vous permet de trouver la plus petite
occurrence de x dans le tableau. Si le tableau contient des doublons de x, l'algorithme peut être
légèrement modifié pour qu'il renvoie la plus grande occurrence de x en ajoutant simplement au
conditionnel if:
Notez qu'au lieu de faire mid = (low + high) / 2 , il peut également être judicieux d'essayer mid =
low + ((high - low) / 2) pour des implémentations telles que les implémentations Java afin de
réduire le risque d'obtenir un débordement pour des entrées vraiment importantes.
Recherche linéaire
La recherche linéaire est un algorithme simple. Il parcourt les éléments jusqu'à ce que la requête
soit trouvée, ce qui en fait un algorithme linéaire - la complexité est O (n), où n est le nombre
d'éléments à parcourir.
Pourquoi O (n)? Dans le pire des cas, vous devez passer par tous les n éléments.
Cela peut être comparé à la recherche d'un livre dans une pile de livres - vous les parcourez tous
jusqu'à ce que vous trouviez celui que vous voulez.
[Link] 221
def linear_search(searchable_list, query):
for x in searchable_list:
if query == x:
return True
return False
Rabin Karp
Lors du calcul de la valeur de hachage, nous la divisons par un nombre premier afin d'éviter les
[Link] 222
collisions. Après la division par nombre premier, les chances de collision seront moindres, mais il
est possible que la valeur de hachage soit identique pour deux chaînes. nous obtenons un match
que nous devons vérifier caractère par caractère pour nous assurer que nous avons un match
correct.
Il s'agit de recalculer la valeur de hachage pour le motif, d'abord en supprimant le caractère le plus
à gauche, puis en ajoutant le nouveau caractère du texte.
1. Pire cas
2. Cas moyen
3. Meilleur cas
#include <stdio.h>
// otherwise return -1
int search(int arr[], int n, int x)
{
int i;
for (i=0; i<n; i++)
{
if (arr[i] == x)
return i;
}
return -1;
}
int main()
{
int arr[] = {1, 10, 30, 15};
int x = 30;
int n = sizeof(arr)/sizeof(arr[0]);
printf("%d is present at index %d", x, search(arr, n, x));
getchar();
return 0;
}
Dans le pire des cas, nous calculons la limite supérieure du temps d'exécution d'un algorithme.
Nous devons connaître le cas qui entraîne le nombre maximal d'opérations à exécuter. Pour la
[Link] 223
recherche linéaire, le pire des cas se produit lorsque l'élément à rechercher (x dans le code ci-
dessus) n'est pas présent dans le tableau. Lorsque x n'est pas présent, la fonction search () la
compare avec tous les éléments de arr [] un par un. Par conséquent, la complexité temporelle la
plus défavorable de la recherche linéaire serait Θ (n)
Dans une analyse de cas moyenne, nous prenons toutes les entrées possibles et calculons le
temps de calcul pour toutes les entrées. Sommez toutes les valeurs calculées et divisez la somme
par le nombre total d'entrées. Nous devons connaître (ou prédire) la distribution des cas. Pour le
problème de recherche linéaire, supposons que tous les cas sont uniformément distribués (y
compris le cas où x n'est pas présent dans le tableau). Nous somme donc tous les cas et divisons
la somme par (n + 1). Voici la valeur de la complexité du temps moyen.
Dans le meilleur des cas, nous calculons la limite inférieure sur le temps d'exécution d'un
algorithme. Nous devons connaître le cas à l'origine du nombre minimum d'opérations à exécuter.
Dans le problème de la recherche linéaire, le meilleur cas se produit lorsque x est présent au
premier emplacement. Le nombre d'opérations dans le meilleur des cas est constant (non
dépendant de n). Donc, dans le meilleur des cas, la complexité du temps serait Θ (1) La plupart
du temps, nous effectuons une analyse des pires scénarios pour analyser les algorithmes. Dans la
pire des analyses, nous garantissons une limite supérieure sur le temps d'exécution d'un
algorithme, ce qui est une bonne information. L'analyse de cas moyenne n'est pas facile à faire
dans la plupart des cas pratiques et elle est rarement réalisée. Dans l'analyse de cas moyenne,
nous devons connaître (ou prédire) la distribution mathématique de toutes les entrées possibles.
L'analyse des meilleurs cas est fausse. Garantir une limite inférieure sur un algorithme ne fournit
aucune information, car dans le pire des cas, un algorithme peut prendre des années.
Pour certains algorithmes, tous les cas sont asymptotiquement identiques, c’est-à-dire qu’il n’ya
pas de pire et de meilleur cas. Par exemple, Fusionner le tri. Fusionner le tri fait les opérations Θ (
nLogn) dans tous les cas. La plupart des autres algorithmes de tri ont les pires et les meilleurs
cas. Par exemple, dans l'implémentation classique du tri rapide (où pivot est choisi comme
élément d'angle), le pire se produit lorsque le tableau en entrée est déjà trié et le meilleur lorsque
les éléments pivotants divisent toujours le tableau en deux moitiés. Pour le tri par insertion, le pire
des cas se produit lorsque le tableau est trié en sens inverse et que le meilleur cas se produit
[Link] 224
lorsque le tableau est trié dans le même ordre que la sortie.
[Link] 225
Chapitre 43: Recherche de sous-chaîne
Examples
Algorithme KMP en C
Étant donné un texte txt et un motif pat , l'objectif de ce programme sera d'imprimer toute
l'occurrence de pat dans txt .
Exemples:
Contribution:
sortie:
Contribution:
txt[] = "AABAACAADAABAAABAA"
pat[] = "AABA"
sortie:
Implémentation du langage C:
[Link] 226
// Preprocess the pattern (calculate lps[] array)
computeLPSArray(pat, M, lps);
if (j == M)
{
printf("Found pattern at index %d \n", i-j);
j = lps[j-1];
}
[Link] 227
{
lps[i] = 0;
i++;
}
}
}
}
Sortie:
Référence:
[Link]
L'algorithme de Rabin-Karp est un algorithme de recherche de chaîne créé par Richard M. Karp et
Michael O. Rabin qui utilise le hachage pour trouver un ensemble de chaînes de motifs dans un
texte.
Une sous-chaîne d'une chaîne est une autre chaîne qui se produit. Par exemple, ver est une
sous-chaîne de stackoverflow . Ne pas confondre avec la sous-séquence car cover est une sous-
séquence de la même chaîne. En d'autres termes, tout sous-ensemble de lettres consécutives
dans une chaîne est une sous-chaîne de la chaîne donnée.
Dans l'algorithme Rabin-Karp, nous allons générer un hachage de notre modèle que nous
recherchons et vérifier si le hachage de notre texte correspond ou non au modèle . Si cela ne
correspond pas, nous pouvons garantir que le motif n'existe pas dans le texte . Cependant, si elle
correspond, le motif peut être présent dans le texte . Regardons un exemple:
Disons que nous avons un texte: yeminsajid et nous voulons savoir si le motif nsa existe dans le
texte. Pour calculer le hachage et le hash roulant, nous devons utiliser un nombre premier. Cela
peut être n'importe quel nombre premier. Prenons premier = 11 pour cet exemple. Nous
déterminerons la valeur de hachage en utilisant cette formule:
(1st letter) X (prime) + (2nd letter) X (prime)¹ + (3rd letter) X (prime)² X + ......
On notera:
[Link] 228
b -> 2 h -> 8 n -> 14 t -> 20 z -> 26
c -> 3 i -> 9 o -> 15 u -> 21
d -> 4 j -> 10 p -> 16 v -> 22
e -> 5 k -> 11 q -> 17 w -> 23
f -> 6 l -> 12 r -> 18 x -> 24
Maintenant, nous trouvons le hash roulant de notre texte. Si le hash roulant correspond à la valeur
de hachage de notre modèle, nous vérifierons si les chaînes correspondent ou non. Étant donné
que notre modèle a 3 lettres, nous allons prendre la 1ère 3 lettres YEM de notre texte et calcul de
la valeur de hachage. On a:
Cette valeur ne correspond pas à la valeur de hachage de notre modèle. Donc la chaîne n'existe
pas ici. Maintenant, nous devons envisager l'étape suivante. Pour calculer la valeur de hachage
de notre prochaine chaîne emi . Nous pouvons calculer cela en utilisant notre formule. Mais cela
serait plutôt trivial et nous coûterait plus cher. Au lieu de cela, nous utilisons une autre technique.
La nouvelle valeur de hachage n'est pas égale à la valeur de hachage de nos modèles. Sur la
route , pour n nous obtenons:
[Link] 229
Previous String: ins
First Letter of Previous String: i(9)
New Letter: a(1)
New String: "nsa"
2462 - 9 = 2453
2453 / 11 = 223
223 + 1 X 11² = 344
C'est un match! Maintenant, nous comparons notre modèle avec la chaîne actuelle. Comme les
deux chaînes correspondent, la sous-chaîne existe dans cette chaîne. Et nous retournons la
position de départ de notre sous-chaîne.
Le pseudo-code sera:
Calcul de hachage:
Recalcul de hachage:
Match de cordes:
Rabin-Karp:
[Link] 230
Return -1
Cet algorithme est utilisé pour détecter le plagiat. Étant donné le matériel source, l'algorithme peut
rapidement parcourir un article pour trouver des exemples de phrases à partir du matériau source,
en ignorant des détails tels que la casse et la ponctuation. En raison de l’abondance des chaînes
recherchées, les algorithmes de recherche à chaîne unique sont peu pratiques ici. Encore une
fois, l' algorithme de Knuth-Morris-Pratt ou l' algorithme de recherche de chaîne de Boyer-
Moore est un algorithme de recherche de chaîne à motif unique plus rapide que celui de Rabin-
Karp . Cependant, c'est un algorithme de choix pour la recherche de motifs multiples. Si nous
voulons trouver l'un des grands nombres, disons k, des motifs de longueur fixe dans un texte,
nous pouvons créer une variante simple de l'algorithme de Rabin-Karp.
Supposons que nous ayons un texte et un motif . Nous devons déterminer si le modèle existe ou
non dans le texte. Par exemple:
+-------+---+---+---+---+---+---+---+---+
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+-------+---+---+---+---+---+---+---+---+
| Text | a | b | c | b | c | g | l | x |
+-------+---+---+---+---+---+---+---+---+
+---------+---+---+---+---+
| Index | 0 | 1 | 2 | 3 |
+---------+---+---+---+---+
| Pattern | b | c | g | l |
+---------+---+---+---+---+
Ce modèle existe dans le texte . Donc, notre recherche de sous-chaîne doit retourner 3 , l’index
de la position à partir de laquelle ce modèle commence. Alors, comment fonctionne notre
procédure de recherche de sous-chaîne par force brute?
Ce que nous faisons habituellement est: nous partons du 0ème index du texte et du 0ème index
de notre * pattern et nous comparons Text [0] avec Pattern [0] . Comme ils ne correspondent
pas, nous allons au prochain index de notre texte et nous comparons Text [1] avec Pattern [0] .
Comme il s'agit d'une correspondance, nous incrémentons également l'index de notre modèle et
l'index du texte . Nous comparons Text [2] avec Pattern [1] . Ils sont aussi un match. En suivant
la même procédure que précédemment, nous comparons maintenant Text [3] avec Pattern [2] .
Comme ils ne correspondent pas, nous partons de la position suivante où nous avons commencé
à trouver le match. C'est l'index 2 du texte . Nous comparons Text [2] avec Pattern [0] . Ils ne
correspondent pas. Puis en incrémentant l’index du Text , on compare Text [3] avec Pattern [0] .
Ils correspondent. De nouveau Text [4] et Pattern [1] correspondent, Text [5] et Pattern [2]
correspondent et Text [6] et Pattern [3] correspondent. Comme nous avons atteint la fin de notre
modèle , nous retournons maintenant l’index à partir duquel notre correspondance a commencé, à
[Link] 231
savoir 3 . Si notre modèle était: bcgll , cela signifie que si le modèle n'existait pas dans notre texte
, notre recherche devrait renvoyer une exception ou -1 ou toute autre valeur prédéfinie. Nous
pouvons clairement voir que, dans le pire des cas, cet algorithme prendrait O(mn) temps où m est
la longueur du texte et n la longueur du motif . Comment pouvons-nous réduire cette complexité
temporelle? C'est là que l'algorithme de recherche de sous-chaîne KMP entre en jeu.
L' algorithme de recherche de chaînes Knuth-Morris-Pratt ou l' algorithme KMP recherche les
occurrences d'un "motif" dans un "texte" principal en utilisant l'observation selon laquelle, en cas
d'incompatibilité, le mot lui-même contient suffisamment d'informations pour déterminer où ,
contournant ainsi le réexamen des caractères précédemment appariés. L'algorithme a été conçu
en 1970 par Donuld Knuth et Vaughan Pratt et indépendamment par James H. Morris . Le trio l'a
publié conjointement en 1977.
Étendons notre exemple Text and Pattern pour une meilleure compréhension:
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Index |0 |1 |2 |3 |4 |5 |6 |7 |8 |9 |10|11|12|13|14|15|16|17|18|19|20|21|22|
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Text |a |b |c |x |a |b |c |d |a |b |x |a |b |c |d |a |b |c |d |a |b |c |y |
+-------+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+---------+---+---+---+---+---+---+---+---+
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Pattern | a | b | c | d | a | b | c | y |
+---------+---+---+---+---+---+---+---+---+
Au début, nos correspondances textuelles et graphiques jusqu’à l’index 2 . Le texte [3] et le motif
[3] ne correspondent pas. Notre objectif est donc de ne pas revenir en arrière dans ce texte ,
c’est-à-dire qu’en cas de non-concordance, nous ne voulons pas que notre correspondance
reprenne à partir de la position avec laquelle nous avons commencé à correspondre. Pour y
parvenir, nous chercherons un suffixe dans notre Pattern juste avant que notre discordance ne se
produise (sous-chaîne abc ), qui est également un préfixe de la sous-chaîne de notre Pattern .
Pour notre exemple, puisque tous les caractères sont uniques, il n'y a pas de suffixe, c'est le
préfixe de notre sous-chaîne correspondante. Donc, cela signifie que notre prochaine
comparaison commencera à partir de l’index 0 . Attends un peu, tu comprendras pourquoi nous
avons fait ça. Ensuite, nous comparons Text [3] avec Pattern [0] et cela ne correspond pas.
Après cela, pour Text from index 4 to index 9 et pour Pattern de l'index 0 à l'index 5 , nous
trouvons une correspondance. Nous trouvons un décalage dans Text [10] et Pattern [6] . Nous
prenons donc la sous-chaîne de Pattern juste avant le point où la discordance se produit (sous-
chaîne abcdabc ), nous vérifions un suffixe, qui est aussi un préfixe de cette sous-chaîne. Nous
pouvons voir ici que ab est à la fois le suffixe et le préfixe de cette sous-chaîne. Ce que cela
signifie, puisque nous avons comparé jusqu'à Text [10] , les caractères juste avant la disparité
sont ab . On peut en déduire que puisque ab est aussi un préfixe de la sous-chaîne que nous
avons prise, il n'est pas nécessaire de vérifier à nouveau ab et le prochain contrôle peut
commencer par Text [10] et Pattern [2] . Nous n'avons pas eu à revenir sur l'ensemble du texte ,
nous pouvons commencer directement à partir de l'endroit où notre inadéquation s'est produite.
Maintenant, nous vérifions Text [10] et Pattern [2] , car il ne correspond pas, et la sous-chaîne
avant mismatch ( abc ) ne contient pas de suffixe qui est aussi un préfixe, nous vérifions Text [10]
[Link] 232
et Pattern [0] , ils ne correspondent pas. Après cela pour le texte de l'index 11 à l'index 17 et pour
le motif de l'index 0 à l'index 6 . Nous trouvons une disparité dans Text [18] et Pattern [7] . Donc,
encore une fois, nous vérifions la sous-chaîne avant la non-concordance (sous-chaîne abcdabc )
et trouvons que abc est à la fois le suffixe et le préfixe. Donc, puisque nous avons fait
correspondre jusqu'à Pattern [7] , abc doit être avant Text [18] . Cela signifie que nous n'avons
pas besoin de comparer jusqu'à ce que Text [17] et notre comparaison commence par Text [18]
et Pattern [3] . Nous allons donc trouver une correspondance et nous retournerons 15 qui est
notre index de départ du match. Voici comment notre recherche de sous-chaînes KMP fonctionne
en utilisant les informations de suffixe et de préfixe.
Maintenant, comment pouvons-nous calculer efficacement si le suffixe est le même que le préfixe
et à quel moment démarrer le contrôle s’il ya une incohérence de caractère entre le texte et le
motif . Regardons un exemple:
+---------+---+---+---+---+---+---+---+---+
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Pattern | a | b | c | d | a | b | c | a |
+---------+---+---+---+---+---+---+---+---+
Nous allons générer un tableau contenant les informations requises. Appelons le tableau S. La
taille du tableau sera la même que la longueur du motif. Puisque la première lettre du Pattern ne
peut être le suffixe d'aucun préfixe, nous allons mettre S [0] = 0 . Nous prenons d'abord i = 1 et j =
0 . A chaque étape, nous comparons le motif [i] et le motif [j] et incrémentons i . S'il y a une
correspondance, on met S [i] = j + 1 et l'incrément j , s'il y a une discordance, on vérifie la position
précédente de j (si disponible) et on met j = S [j-1] (si j n'est pas égal à 0 ), nous continuons à le
faire jusqu'à ce que S [j] ne corresponde pas à S [i] ou que j ne devienne pas 0 . Pour le dernier,
nous mettons S [i] = 0 . Pour notre exemple:
j i
+---------+---+---+---+---+---+---+---+---+
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Pattern | a | b | c | d | a | b | c | a |
+---------+---+---+---+---+---+---+---+---+
Pattern [j] et Pattern [i] ne correspondent pas, donc nous incrémentons i et comme j vaut 0 ,
nous ne vérifions pas la valeur précédente et mettons Pattern [i] = 0 . Si nous continuons à
incrémenter i , pour i = 4 , nous obtiendrons une correspondance, nous avons donc mis S [i] = S
[4] = j + 1 = 0 + 1 = 1 et incrémenté j et i . Notre tableau ressemblera à:
j i
+---------+---+---+---+---+---+---+---+---+
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---------+---+---+---+---+---+---+---+---+
| Pattern | a | b | c | d | a | b | c | a |
+---------+---+---+---+---+---+---+---+---+
| S | 0 | 0 | 0 | 0 | 1 | | | |
+---------+---+---+---+---+---+---+---+---+
[Link] 233
Comme Pattern [1] et Pattern [5] correspondent, nous mettons S [i] = S [5] = j + 1 = 1 + 1 = 2 .
Si nous continuons, nous trouverons une incompatibilité pour j = 3 et i = 7 . Puisque j n'est pas
égal à 0 , on met j = S [j-1] . Et nous comparerons les caractères à i et j sont identiques ou non,
puisqu'ils sont identiques, nous allons mettre S [i] = j + 1. Notre tableau terminé ressemblera à:
+---------+---+---+---+---+---+---+---+---+
| S | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 1 |
+---------+---+---+---+---+---+---+---+---+
Ceci est notre tableau requis. Ici, une valeur non nulle de S [i] signifie qu'il y a un suffixe de
longueur S [i] identique au préfixe dans cette sous-chaîne (sous-chaîne de 0 à i ) et la
comparaison suivante commencera à partir de S [i] + 1 position du Motif Notre algorithme pour
générer le tableau ressemblerait à ceci:
Procedure GenerateSuffixArray(Pattern):
i := 1
j := 0
n := [Link]
while i is less than n
if Pattern[i] is equal to Pattern[j]
S[i] := j + 1
j := j + 1
i := i + 1
else
if j is not equal to 0
j := S[j-1]
else
S[i] := 0
i := i + 1
end if
end if
end while
La complexité temporelle pour construire ce tableau est O(n) et la complexité de l'espace est
également O(n) . Pour vous assurer que vous avez bien compris l’algorithme, essayez de générer
un tableau pour le motif aabaabaa et vérifiez si le résultat correspond à celui- ci.
+---------+---+---+---+---+---+---+---+---+---+---+---+---+
| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 |
+---------+---+---+---+---+---+---+---+---+---+---+---+---+
| Text | a | b | x | a | b | c | a | b | c | a | b | y |
+---------+---+---+---+---+---+---+---+---+---+---+---+---+
+---------+---+---+---+---+---+---+
| Index | 0 | 1 | 2 | 3 | 4 | 5 |
+---------+---+---+---+---+---+---+
| Pattern | a | b | c | a | b | y |
+---------+---+---+---+---+---+---+
| S | 0 | 0 | 0 | 1 | 2 | 0 |
+---------+---+---+---+---+---+---+
Nous avons un texte , un motif et un tableau pré-calculé S en utilisant notre logique définie
[Link] 234
précédemment. Nous comparons Text [0] et Pattern [0] et ils sont identiques. Le texte [1] et le
motif [1] sont identiques. Le texte [2] et le motif [2] ne sont pas identiques. Nous vérifions la
valeur à la position juste avant la disparité. Puisque S [1] vaut 0 , il n'y a pas de suffixe identique
au préfixe de notre sous-chaîne et notre comparaison commence à la position S [1] , qui est 0 .
Donc, le Pattern [0] n'est pas le même que Text [2] , donc nous continuons. Le texte [3] est
identique au motif [0] et il y a une correspondance jusqu'à Texte [8] et Motif [5] . Nous revenons
en arrière dans le tableau S et trouvons 2 . Cela signifie donc qu'il y a un préfixe de longueur 2 qui
est aussi le suffixe de cette sous-chaîne ( abcab) qui est ab . Cela signifie également qu'il y a un
ab avant Text [8] . Nous pouvons donc ignorer en toute sécurité Pattern [0] et Pattern [1] et
commencer notre prochaine comparaison avec Pattern [2] et Text [8] . Si nous continuons, nous
trouverons le motif dans le texte . Notre procédure ressemblera à:
La complexité temporelle de cet algorithme en dehors du calcul du tableau de suffixes est O(m) .
Puisque GenerateSuffixArray prend O(n) , la complexité temporelle totale de l’algorithme KMP est:
O(m+n) .
PS: Si vous voulez trouver plusieurs occurrences de Pattern dans le texte , au lieu de retourner la
valeur, imprimez-la / stockez-la et définissez j := S[j-1] . Conservez également un flag pour
savoir si vous avez trouvé une occurrence ou non et gérez-la en conséquence.
[Link] 235
Complexité de l'espace : O (m) à cause du tableau de préfixes sur l'aiguille.
def get_prefix_table(needle):
prefix_set = set()
n = len(needle)
prefix_table = [0]*n
delimeter = 1
while(delimeter<n):
prefix_set.add(needle[:delimeter])
j = 1
while(j<delimeter+1):
if needle[j:delimeter+1] in prefix_set:
prefix_table[delimeter] = delimeter - j + 1
break
j += 1
delimeter += 1
return prefix_table
if __name__ == '__main__':
needle = 'abcaby'
haystack = 'abxabcabcaby'
print strstr(haystack, needle)
[Link] 236
Chapitre 44: Recherche en largeur
Examples
Recherche du chemin le plus court de la source aux autres nœuds
Regardons un exemple:
[Link] 237
Supposons que ce graphique représente la connexion entre plusieurs villes, où chaque nœud
indique une ville et une arête entre deux nœuds indique qu’il ya une route les reliant. Nous
voulons passer du noeud 1 au noeud 10 . Donc, le noeud 1 est notre source , qui est le niveau
0 . Nous marquons le noeud 1 comme visité. Nous pouvons aller au noeud 2 , au noeud 3 et au
noeud 4 d'ici. Ils seront donc au niveau (0 + 1) = nœuds de niveau 1 . Nous allons maintenant
les marquer comme visités et travailler avec eux.
[Link] 238
Les nœuds colorés sont visités. Les nœuds avec lesquels nous travaillons actuellement seront
marqués en rose. Nous ne visiterons pas le même nœud deux fois. A partir du nœud 2 , du nœud
3 et du nœud 4 , nous pouvons aller au nœud 6, au nœud 7 et au nœud 8 . Marquons-les
comme visités. Le niveau de ces nœuds sera le niveau (1 + 1) = niveau 2 .
[Link] 239
Si vous ne l'avez pas remarqué, le niveau des nœuds indique simplement la distance de chemin
la plus courte par rapport à la source . Par exemple: nous avons trouvé le noeud 8 au niveau 2 .
La distance entre la source et le noeud 8 est donc de 2 .
Nous n'avons pas encore atteint notre nœud cible, à savoir le nœud 10 . Alors, visitons les
prochains nœuds. on peut aller directement du nœud 6 , du nœud 7 et du nœud 8 .
[Link] 240
Nous pouvons voir cela, nous avons trouvé le nœud 10 au niveau 3 . Donc, le plus court chemin
de la source au noeud 10 est 3. Nous avons recherché le graphique par niveau et trouvé le plus
court chemin. Maintenant, effaçons les bords que nous n'avons pas utilisés:
[Link] 241
Après avoir supprimé les arêtes que nous n'utilisions pas, nous obtenons un arbre appelé
arborescence BFS. Cet arbre montre le chemin le plus court entre la source et tous les autres
nœuds.
Notre tâche consistera donc à passer de la source aux nœuds de niveau 1 . Ensuite, du niveau
1 au niveau 2, nous atteignons notre destination. Nous pouvons utiliser la file d' attente pour
stocker les noeuds que nous allons traiter. Autrement dit, pour chaque nœud avec lequel nous
allons travailler, nous allons pousser tous les autres nœuds pouvant être directement traversés et
non encore traversés dans la file d'attente.
Nous commençons par pousser la source dans la file d'attente. Notre file d'attente ressemblera à:
front
+-----+
| 1 |
+-----+
Le niveau du noeud 1 sera 0. niveau [1] = 0 . Maintenant, nous commençons notre BFS. Au
début, nous ouvrons un nœud de notre file d'attente. Nous obtenons le noeud 1 . Nous pouvons
aller au nœud 4 , au nœud 3 et au nœud 2 de celui-ci. Nous avons atteint ces nœuds à partir du
nœud 1 . Donc, niveau [4] = niveau [3] = niveau [2] = niveau [1] + 1 = 1 . Maintenant, nous les
[Link] 242
marquons comme visités et les mettons dans la file d'attente.
front
+-----+ +-----+ +-----+
| 2 | | 3 | | 4 |
+-----+ +-----+ +-----+
Maintenant, nous ouvrons le noeud 4 et travaillons avec. On peut aller au noeud 7 du noeud 4 .
niveau [7] = niveau [4] + 1 = 2 . Nous marquons le noeud 7 comme visité et le plaçons dans la
file d'attente.
front
+-----+ +-----+ +-----+
| 7 | | 2 | | 3 |
+-----+ +-----+ +-----+
A partir du nœud 3 , on peut aller au nœud 7 et au nœud 8 . Comme nous avons déjà marqué le
nœud 7 comme visité, nous marquons le nœud 8 comme visité, nous modifions le niveau [8] =
niveau [3] + 1 = 2 . Nous poussons le nœud 8 dans la file d'attente.
front
+-----+ +-----+ +-----+
| 6 | | 7 | | 2 |
+-----+ +-----+ +-----+
Ce processus continuera jusqu'à ce que nous atteignions notre destination ou que la file d'attente
devienne vide. Le tableau de niveaux nous fournira la distance du plus court chemin depuis la
source . Nous pouvons initialiser le tableau de niveau avec une valeur infinie , ce qui marquera
que les nœuds ne sont pas encore visités. Notre pseudo-code sera:
Parfois, nous pourrions avoir besoin d'imprimer non seulement la distance la plus courte, mais
aussi le chemin par lequel nous pouvons aller à notre nœud à partir de la source . Pour cela,
[Link] 243
nous devons conserver un tableau parent . parent [source] sera NULL. Pour chaque mise à jour
dans le tableau de niveaux , nous ajouterons simplement le parent[v] := u dans notre pseudo-
code dans la boucle for. Après avoir terminé BFS, pour trouver le chemin, nous allons parcourir le
tableau parent jusqu’à ce que nous ayons atteint la source qui sera désignée par la valeur NULL.
Le pseudo-code sera:
Complexité:
Nous avons visité chaque noeud une fois et chaque fois. La complexité sera donc O (V + E) où V
est le nombre de nœuds et E le nombre d’arêtes.
La plupart du temps, nous devrons trouver le chemin le plus court entre une source unique et tous
les autres nœuds ou un nœud spécifique dans un graphique 2D. Dites par exemple: nous voulons
savoir combien de mouvements sont nécessaires pour qu'un chevalier atteigne un certain carré
dans un échiquier, ou nous avons un tableau où certaines cellules sont bloquées, nous devons
trouver le plus court chemin d'une cellule à une autre . Nous ne pouvons bouger que
horizontalement et verticalement. Même les mouvements diagonaux peuvent être possibles. Dans
ces cas, nous pouvons convertir les carrés ou les cellules dans les nœuds et résoudre facilement
ces problèmes en utilisant BFS. Maintenant, notre site parent et niveau visité sera des tableaux
2D. Pour chaque nœud, nous considérerons tous les mouvements possibles. Pour trouver la
distance à un nœud spécifique, nous vérifierons également si nous avons atteint notre destination.
Il y aura une chose supplémentaire appelée tableau de direction. Cela stockera simplement toutes
les combinaisons possibles de directions vers lesquelles nous pouvons aller. Disons que, pour les
déplacements horizontaux et verticaux, nos tableaux de direction seront:
+----+-----+-----+-----+-----+
| dx | 1 | -1 | 0 | 0 |
+----+-----+-----+-----+-----+
| dy | 0 | 0 | 1 | -1 |
+----+-----+-----+-----+-----+
[Link] 244
La partie supplémentaire que nous devons garder à l’esprit est la suivante:
• Si l'une des cellules est bloquée, pour chaque mouvement possible, nous vérifierons si la
cellule est bloquée ou non.
• Nous allons également vérifier si nous sommes sortis des limites, c'est-à-dire que nous
avons dépassé les limites du tableau.
• Le nombre de lignes et de colonnes sera donné.
Comme nous l'avons vu précédemment, BFS ne fonctionne que pour les graphiques non
pondérés. Pour les graphiques pondérés, nous aurons besoin de l'algorithme de Dijkstra . Pour
les cycles de fronts négatifs, nous avons besoin de l'algorithme de Bellman-Ford . Encore une
fois, cet algorithme est l'algorithme à source unique le plus court. Si nous devons trouver la
distance entre chaque nœud et tous les autres nœuds, nous aurons besoin de l'algorithme de
Floyd-Warshall .
BFS peut être utilisé pour trouver les composants connectés d'un graphique non dirigé . Nous
pouvons également trouver si le graphique donné est connecté ou non. Notre discussion
ultérieure suppose que nous avons affaire à des graphes non orientés. La définition d'un graphe
connecté est la suivante:
Un graphique est connecté s'il existe un chemin entre chaque paire de sommets.
[Link] 245
Le graphique suivant n'est pas connecté et comporte 2 composants connectés:
BFS est un algorithme de parcours graphique. Donc, à partir d'un nœud source aléatoire, si à la
fin de l'algorithme, tous les nœuds sont visités, alors le graphique est connecté, sinon il n'est pas
connecté.
boolean isConnected(Graph g)
{
BFS(v)//v is a random source node.
if(allVisited(g))
{
return true;
}
else return false;
}
[Link] 246
Implémentation C pour savoir si un graphe non orienté est connecté ou non:
#include<stdio.h>
#include<stdlib.h>
#define MAXVERTICES 100
void enqueue(int);
int deque();
int isConnected(char **graph,int noOfVertices);
void BFS(char **graph,int vertex,int noOfVertices);
int count = 0;
//Queue node depicts a single Queue element
//It is NOT a graph node.
struct node
{
int v;
struct node *next;
};
int main()
{
int n,e;//n is number of vertices, e is number of edges.
int i,j;
char **graph;//adjacency matrix
[Link] 247
}
if(isConnected(graph,n))
printf("The graph is connected");
else printf("The graph is NOT connected\n");
}
int deque()
{
if(Qfront == NULL)
{
printf("Q is empty , returning -1\n");
return -1;
}
else
{
int v = Qfront->v;
Nodeptr temp= Qfront;
if(Qfront == Qrear)
{
Qfront = Qfront->next;
Qrear = NULL;
}
else
Qfront = Qfront->next;
free(temp);
return v;
}
}
[Link] 248
return 1;//1 implies true;
}
Pour rechercher tous les composants connectés d'un graphique non dirigé, il suffit d'ajouter 2
lignes de code à la fonction BFS. L'idée est d'appeler la fonction BFS jusqu'à ce que tous les
sommets soient visités.
ET
printf("%d ",vertex+1);
add this as first line of while loop in BFS
}
}
[Link] 249
Chapitre 45: Résolution d'équations
Examples
Équation linéaire
Mise en œuvre en C-
int i, j;
while(!rootFound){
for(i=0; i<n; i++){ //calculation
Nx[i]=b[i];
rootFound=1; //verification
for(i=0; i<n; i++){
if(!( (Nx[i]-x[i])/x[i] > -0.000001 && (Nx[i]-x[i])/x[i] < 0.000001 )){
rootFound=0;
break;
}
}
return ;
}
[Link] 250
//Implementation of Gauss-Seidal Method
void GaussSeidalMethod(int n, double x[n], double b[n], double a[n][n]){
double Nx[n]; //modified form of variables
int rootFound=0; //flag
int i, j;
for(i=0; i<n; i++){ //initialization
Nx[i]=x[i];
}
while(!rootFound){
for(i=0; i<n; i++){ //calculation
Nx[i]=b[i];
rootFound=1; //verification
for(i=0; i<n; i++){
if(!( (Nx[i]-x[i])/x[i] > -0.000001 && (Nx[i]-x[i])/x[i] < 0.000001 )){
rootFound=0;
break;
}
}
return ;
}
return ;
}
int main(){
//equation initialization
int n=3; //number of variables
//assign values
a[0][0]=8; a[0][1]=2; a[0][2]=-2; b[0]=8; //8x₁+2x₂-2x₃+8=0
a[1][0]=1; a[1][1]=-8; a[1][2]=3; b[1]=-4; //x₁-8x₂+3x₃-4=0
a[2][0]=2; a[2][1]=1; a[2][2]=9; b[2]=12; //2x₁+x₂+9x₃+12=0
[Link] 251
int i;
return 0;
}
Une équation du type f(x)=0 est soit algébrique soit transcendantale. Ces types d’équations
peuvent être résolus en utilisant deux types de méthodes:
1. Méthode directe : Cette méthode donne la valeur exacte de toutes les racines directement
dans un nombre fini d'étapes.
2. Méthode indirecte ou itérative : Les méthodes itératives sont les mieux adaptées aux
programmes informatiques pour résoudre une équation. Il est basé sur le concept
d'approximation successive. Dans la méthode itérative, il existe deux manières de résoudre
une équation -
• Méthode Bracketing : Nous prenons deux points initiaux où la racine se situe entre
eux. Exemple: Méthode de bisection, méthode de la fausse position.
• Méthode Open End : Nous prenons une ou deux valeurs initiales où la racine peut
être n'importe où. Exemple - Méthode de Newton-Raphson, méthode d'approximation
successive, méthode sécante.
Mise en œuvre en C-
/**
* Takes two initial values and shortens the distance by both side.
**/
double BisectionMethod(){
double root=0;
[Link] 252
int loopCounter=0;
if(f(a)*f(b) < 0){
while(1){
loopCounter++;
c=(a+b)/2;
}
}
printf("It took %d loops.\n", loopCounter);
return root;
}
/**
* Takes two initial values and shortens the distance by single side.
**/
double FalsePosition(){
double root=0;
int loopCounter=0;
if(f(a)*f(b) < 0){
while(1){
loopCounter++;
return root;
}
/**
* Uses one initial value and gradually takes that value near to the real one.
[Link] 253
**/
double NewtonRaphson(){
double root=0;
double x1=1;
double x2=0;
int loopCounter=0;
while(1){
loopCounter++;
x2 = x1 - (f(x1)/f2(x1));
/*/printf("%lf \t %lf \n", x2, f(x2));/**////test
x1=x2;
}
printf("It took %d loops.\n", loopCounter);
return root;
}
/**
* Uses one initial value and gradually takes that value near to the real one.
**/
double FixedPoint(){
double root=0;
double x=1;
int loopCounter=0;
while(1){
loopCounter++;
x=g(x);
}
printf("It took %d loops.\n", loopCounter);
return root;
}
/**
* uses two initial values & both value approaches to the root.
**/
double Secant(){
double root=0;
double x0=1;
double x1=2;
double x2=0;
[Link] 254
int loopCounter=0;
while(1){
loopCounter++;
x2 = ((x0*f(x1))-(x1*f(x0))) / (f(x1)-f(x0));
x0=x1;
x1=x2;
}
printf("It took %d loops.\n", loopCounter);
return root;
}
int main(){
double root;
root = BisectionMethod();
printf("Using Bisection Method the root is: %lf \n\n", root);
root = FalsePosition();
printf("Using False Position Method the root is: %lf \n\n", root);
root = NewtonRaphson();
printf("Using Newton-Raphson Method the root is: %lf \n\n", root);
root = FixedPoint();
printf("Using Fixed Point Method the root is: %lf \n\n", root);
root = Secant();
printf("Using Secant Method the root is: %lf \n\n", root);
return 0;
}
[Link] 255
Chapitre 46: Seau Sort
Examples
Seau Trier les informations de base
Bucket Sort est un algorithme de tri dans lequel les éléments du tableau d'entrée sont répartis
dans des compartiments. Après avoir distribué tous les éléments, les compartiments sont triés
individuellement par un autre algorithme de tri. Parfois, il est également trié par méthode
récursive.
La plupart du temps, les gens utilisent le paradigme d'insertion pour peu d'optimisation.
Espace auxiliaire: O{n}
Implémentation C #
[Link] 256
public class BucketSort
{
public static void SortBucket(ref int[] input)
{
int minValue = input[0];
int maxValue = input[0];
int k = 0;
[Link] 257
Chapitre 47: Sélection Tri
Examples
Sélection Tri Informations de base
Le tri par sélection est un algorithme de tri, en particulier un tri de comparaison sur place. Il a une
complexité temporelle de O (n2), ce qui le rend inefficace sur les grandes listes, et donne
généralement de meilleurs résultats que le tri par insertion similaire. Le tri par sélection est connu
pour sa simplicité et présente des avantages en termes de performances par rapport à des
algorithmes plus compliqués dans certaines situations, en particulier lorsque la mémoire auxiliaire
est limitée.
L'algorithme divise la liste d'entrée en deux parties: la sous-liste des éléments déjà triés, qui est
construite de gauche à droite au début (gauche) de la liste, et la sous-liste des éléments restant à
trier qui occupent le reste de la liste. liste. Initialement, la sous-liste triée est vide et la sous-liste
non triée est la liste complète des entrées. L'algorithme procède en trouvant l'élément le plus petit
(ou le plus grand, selon l'ordre de tri) dans la sous-liste non triée, en échangeant (échangeant)
l'élément non trié le plus à gauche (en le triant) et en déplaçant les limites .
function select(list[1..n], k)
for i from 1 to k
minIndex = i
minValue = list[i]
for j from i+1 to n
if list[j] < minValue
minIndex = j
minValue = list[j]
swap list[i] and list[minIndex]
return list[k]
[Link] 258
Exemple de tri de sélection:
[Link] 259
Espace auxiliaire: O(n)
Complexité temporelle: O(n^2)
defmodule Selection do
defp min([first|[second|[]]]) do
smaller(first, second)
end
defp min([first|[second|tail]]) do
min([smaller(first, second)|tail])
end
[Link] 260
defp smaller(e1, e2) do
if e1 <= e2 do
e1
else
e2
end
end
end
[Link]([100,4,10,6,9,3])
|> [Link]
[Link] 261
Chapitre 48: Sort de crêpes
Examples
Crêpes Trier les informations de base
Pancake Sort est un terme familier pour désigner le problème mathématique du tri d'une pile
désordonnée de crêpes par ordre de taille lorsqu'une spatule peut être insérée à n'importe quel
point de la pile et utilisée pour retourner toutes les crêpes au-dessus. Un numéro de crêpe est le
nombre minimum de retournements requis pour un nombre donné de crêpes.
Contrairement à un algorithme de tri traditionnel, qui tente de trier avec le moins de comparaisons
possibles, le but est de trier la séquence dans le moins d’inversions possible.
L'idée est de faire quelque chose de similaire à Selection Sort. Nous mettons un à un l’élément
maximum à la fin et réduisons la taille du tableau actuel de un.
Dissection du problème:
1. Besoin de commander les crêpes du plus petit (haut) au plus grand (bas), la pile de départ
peut être rangée dans n'importe quel ordre.
2. Je peux seulement effectuer un retournement complet de la pile.
3. Pour retourner une crêpe spécifique au fond de la pile, vous devez d'abord la retourner vers
le haut (puis la retourner vers le bas).
4. Pour commander chaque crêpe, il vous faudra un rabat vers le haut et un vers le bas pour
atteindre son emplacement final.
Algorithme intuitif:
1. Trouvez le plus grand pancake hors-service et retournez-le vers le bas (vous devrez peut-
être le retourner en haut de la pile en premier).
[Link] 262
Espace auxiliaire: O(1)
Complexité temporelle: O(n^2)
Implémentation C #
[Link] 263
Chapitre 49: Sort étrange
Examples
Informations élémentaires
Un tri impair ou un tri de briques est un algorithme de tri simple, développé pour être utilisé sur
des processeurs parallèles avec interconnexion locale. Cela fonctionne en comparant toutes les
paires d'index adjacents impairs / pairs de la liste et, si une paire est dans le mauvais ordre, les
éléments sont commutés. L'étape suivante répète ceci pour les paires indexées paires / impaires.
Ensuite, il alterne les étapes impaires / paires et paires / impaires jusqu'à ce que la liste soit triée.
if n>2 then
1. apply odd-even merge(n/2) recursively to the even subsequence a0, a2, ..., an-2 and to
the odd subsequence a1, a3, , ..., an-1
2. comparison [i : i+1] for all i element {1, 3, 5, 7, ..., n-3}
else
comparison [0 : 1]
[Link] 264
La mise en oeuvre:
while (!sort)
{
sort = true;
for (var i = 1; i < n - 1; i += 2)
{
if (input[i] <= input[i + 1]) continue;
var temp = input[i];
input[i] = input[i + 1];
input[i + 1] = temp;
sort = false;
}
for (var i = 0; i < n - 1; i += 2)
{
if (input[i] <= input[i + 1]) continue;
var temp = input[i];
input[i] = input[i + 1];
input[i + 1] = temp;
[Link] 265
sort = false;
}
}
}
[Link] 266
Chapitre 50: Transformée de Fourier Rapide
Introduction
La forme réelle et complexe de la DFT (D iscrete F OURRIER T ransforms) peut être utilisé pour
effectuer une analyse de fréquence ou de la synthèse de tous les signaux discrets et périodiques.
La FFT (F ast F OURRIER T RANSFORMER) est une mise en œuvre de la TFD qui peut être
réalisée rapidement sur les processeurs modernes.
Examples
Radix 2 FFT
La méthode la plus simple et peut-être la mieux connue pour calculer la FFT est l'algorithme
Radix-2 Decimation in Time. Le Radix-2 FFT fonctionne en décomposant un signal de domaine
temporel à N points en N signaux de domaine temporel composés chacun d’un seul point
[Link] 267
.
La décomposition du signal, ou «décimation dans le temps», est obtenue en inversant les indices
pour le tableau des données du domaine temporel. Ainsi, pour un signal à 16 points, l'échantillon
1 (binaire 0001) est échangé avec l'échantillon 8 (1000), l'échantillon 2 (0010) est échangé avec 4
(0100), etc. L'échange d'échantillons en utilisant la technique d'inversion de bits peut être réalisé
simplement dans un logiciel, mais limite l'utilisation de la FFT Radix 2 aux signaux de longueur N
[Link] 268
= 2 ^ M.
La valeur d'un signal à 1 point dans le domaine temporel est égale à sa valeur dans le domaine
fréquentiel. Par conséquent, ce tableau de points temporels décomposés ne nécessite aucune
transformation pour devenir un tableau de points de domaine fréquentiel. Les N points uniques;
cependant, doivent être reconstruits dans un spectre de fréquence de N-point. La reconstruction
optimale du spectre de fréquences complet est réalisée à l'aide de calculs papillons. Chaque
étape de reconstruction de la FFT Radix-2 exécute un certain nombre de papillons à deux points,
en utilisant un ensemble similaire de fonctions de pondération exponentielle, Wn ^ R.
La FFT supprime les calculs redondants dans la transformée de Fourier discrète en exploitant la
périodicité de Wn ^ R. La reconstruction spectrale est complétée par les étapes log2 (N) des
calculs papillons donnant X [K]; les données de domaine de fréquence réelles et imaginaires sous
forme rectangulaire. Pour convertir en amplitude et en phase (coordonnées polaires), il faut
trouver la valeur absolue √ (Re2 + Im2) et l'argument tan-1 (Im / Re).
[Link] 269
L'organigramme complet d'un papillon Radix 2 FFT à huit points est présenté ci-dessous. Notez
que les signaux d'entrée ont déjà été réorganisés en fonction de la procédure de décimation dans
le temps décrite précédemment.
[Link] 270
[Link] 271
2. Effectuez la FFT avant sur les données du domaine de fréquence conjuguées.
3. Divisez chaque sortie du résultat de cette FFT par N pour obtenir la valeur réelle du domaine
temporel.
4. Trouvez le conjugué complexe de la sortie en inversant la composante imaginaire des
données du domaine temporel pour toutes les instances de n.
Remarque : les données de fréquence et de domaine temporel sont des variables complexes.
Typiquement, la composante imaginaire du signal de domaine temporel suivant une FFT inverse
est soit nulle, soit ignorée comme erreur d'arrondi. L'augmentation de la précision des variables de
32 bits à 64 bits ou de 128 bits double réduit de manière significative les erreurs d'arrondi
générées par plusieurs opérations FFT consécutives.
#include <math.h>
int i;
complex* x;
for ( i = 0, x = pX; i < N; i++, x++){
x->Re *= NN; // Divide time domain by N for correct amplitude scaling
[Link] 272
x->Im *= -1; // Change the sign of ImX
}
}
[Link] 273
Chapitre 51: Traversées d'arbres binaires
Introduction
La visite d'un nœud d'un arbre binaire dans un ordre particulier est appelée traversées.
Examples
Traversée pré-commande, ordonnée et post-commande d'un arbre binaire
La traversée pré-commande (root) traverse le nœud puis le sous-arbre gauche du nœud puis le
sous-arbre droit du nœud.
1245367
La traversée dans l'ordre (root) traverse le sous-arbre gauche du nœud, puis le nœud puis le
sous-arbre droit du nœud.
4251637
4526731
[Link] 274
La traversée de l'ordre des niveaux sera
1234567
Code:
#include<iostream>
#include<queue>
#include<malloc.h>
struct node{
int data;
node *left;
node *right;
};
queue<node *> Q;
[Link](root);
while(![Link]()){
struct node* curr = [Link]();
cout<< curr->data <<" ";
if(curr->left != NULL) [Link](curr-> left);
if(curr->right != NULL) [Link](curr-> right);
[Link]();
}
}
struct node* newNode(int data)
{
struct node* node = (struct node*)
malloc(sizeof(struct node));
node->data = data;
node->left = NULL;
node->right = NULL;
[Link] 275
return(node);
}
int main(){
return 0;
La structure de données de la file d'attente est utilisée pour atteindre l'objectif ci-dessus.
[Link] 276
Chapitre 52: Traversées graphiques
Examples
Depth First Search Fonction traversale
La fonction prend l'argument de l'index de nœud actuel, la liste de contiguïté (stockée dans le
vecteur de vecteurs dans cet exemple) et le vecteur de booléen pour garder la trace du nœud qui
a été visité.
[Link] 277
Chapitre 53: Tri
Paramètres
Paramètre La description
Un algorithme de tri est stable s'il conserve l'ordre relatif des éléments
La stabilité
égaux après le tri.
Examples
Stabilité en tri
La stabilité dans le tri signifie si un algorithme de tri maintient l'ordre relatif des clés égales de
l'entrée d'origine dans la sortie du résultat.
Ainsi, un algorithme de tri est dit stable si deux objets avec des clés égales apparaissent dans le
même ordre dans la sortie triée, car ils apparaissent dans le tableau non trié en entrée.
Maintenant, nous allons trier la liste en utilisant le premier élément de chaque paire.
[Link] 278
Un tri instable affichera la liste ci-dessous:
Le tri instable peut générer le même résultat que le tri stable, mais pas toujours.
• Tri de tas
• Tri rapide
[Link] 279
Chapitre 54: Tri à bulles
Paramètres
Paramètre La description
Stable Oui
En place Oui
La pire complexité O (n ^ 2)
Examples
Tri à bulles
BubbleSortcompare chaque paire successive d'éléments dans une liste non ordonnée et inverse
les éléments s'ils ne sont pas dans l'ordre.
L'exemple suivant illustre le tri à bulles sur la liste {6,5,3,1,8,7,2,4} (les paires comparées à
chaque étape sont encapsulées dans '**'):
{6,5,3,1,8,7,2,4}
{**5,6**,3,1,8,7,2,4} -- 5 < 6 -> swap
{5,**3,6**,1,8,7,2,4} -- 3 < 6 -> swap
{5,3,**1,6**,8,7,2,4} -- 1 < 6 -> swap
{5,3,1,**6,8**,7,2,4} -- 8 > 6 -> no swap
{5,3,1,6,**7,8**,2,4} -- 7 < 8 -> swap
{5,3,1,6,7,**2,8**,4} -- 2 < 8 -> swap
{5,3,1,6,7,2,**4,8**} -- 4 < 8 -> swap
Après une itération de la liste, nous avons {5,3,1,6,7,2,4,8} . Notez que la plus grande valeur non
triée dans le tableau (8 dans ce cas) atteindra toujours sa position finale. Ainsi, pour être sûr que
la liste est triée, nous devons itérer n-1 fois pour les listes de longueur n.
Graphique:
[Link] 280
Implémentation en Javascript
function bubbleSort(a)
{
var swapped;
do {
swapped = false;
for (var i=0; i < [Link]-1; i++) {
if (a[i] > a[i+1]) {
var temp = a[i];
a[i] = a[i+1];
a[i+1] = temp;
swapped = true;
}
}
} while (swapped);
}
var a = [3, 203, 34, 746, 200, 984, 198, 764, 9];
bubbleSort(a);
[Link](a); //logs [ 3, 9, 34, 198, 200, 203, 746, 764, 984 ]
Implémentation en C #
Le tri à bulles est également connu sous le nom de tri descendant . Il s’agit d’un algorithme de tri
simple qui parcourt de manière répétée la liste à trier, compare chaque paire d’éléments adjacents
et les échange s’ils sont dans le mauvais ordre.
[Link] 281
Implémentation de Bubble Sort
J'ai utilisé le langage C # pour implémenter un algorithme de tri par bulles
Implémentation en C & C ++
void bubbleSort(vector<int>numbers)
{
for(int i = [Link]() - 1; i >= 0; i--) {
for(int j = 1; j <= i; j++) {
if(numbers[j-1] > numbers[j]) {
swap(numbers[j-1],numbers(j));
}
}
}
}
[Link] 282
C Implémentation
t = list[d];
list[d] = list[d+1];
list[d+1] = t;
}
}
}
}
t = * (list + d );
* (list + d ) = * (list + d + 1 );
* (list + d + 1) = t;
}
}
}
}
Implémentation en Java
[Link] 283
}
printNumbers(array);
}
}
int temp;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
Implémentation Python
#!/usr/bin/python
input_list = [10,1,2,11]
for i in range(len(input_list)):
for j in range(i):
if int(input_list[j]) > int(input_list[j+1]):
input_list[j],input_list[j+1] = input_list[j+1],input_list[j]
print input_list
[Link] 284
Chapitre 55: Tri de coquille
Examples
Shell Trier les informations de base
Le tri de shell , également appelé tri par incrément décroissant, est l'un des plus anciens
algorithmes de tri, nommé d'après son inventeur Donald. L. Shell (1959). C'est rapide, facile à
comprendre et à mettre en œuvre. Cependant, son analyse de complexité est un peu plus
sophistiquée.
Le tri shell améliore le tri par insertion. Il commence par comparer des éléments éloignés les uns
des autres, puis des éléments moins éloignés, et finalement en comparant des éléments
adjacents (en fait, un tri par insertion).
L'effet est que la séquence de données est partiellement triée. Le processus ci-dessus est répété,
mais chaque fois avec un tableau plus étroit, c'est-à-dire avec un nombre de colonnes plus petit.
Dans la dernière étape, le tableau ne comprend qu’une seule colonne.
[Link] 285
Pseudo code pour Shell Sort:
input
foreach element in input
{
for(i = gap; i < n; i++)
{
temp = a[i]
for (j = i; j >= gap and a[j - gap] > temp; j -= gap)
{
a[j] = a[j - gap]
}
a[j] = temp
}
}
[Link] 286
Espace auxiliaire: O(n) total, O(1) auxiliary
Complexité temporelle: O(nlogn)
Implémentation C #
[Link] 287
Chapitre 56: Tri de radix
Examples
Informations de base sur le tri Radix
Radix Sort est un algorithme basé sur la comparaison des limites inférieures. C'est un algorithme
de tri d'entiers non comparatif qui trie les données avec des clés entières en regroupant les clés
par des chiffres individuels qui partagent une position et une valeur significatives. Le tri par radix
est un algorithme de tri temporel linéaire qui trie le temps O (n + k) lorsque les éléments sont
compris entre 1 et k. L'idée du tri de Radix est de faire un tri par chiffres en partant du chiffre le
moins significatif au chiffre le plus significatif. Le tri par radix utilise le tri par comptage comme un
sous-programme à trier. Le tri par radix est une généralisation du type de godet.
[Link] 288
Lire Tri de radix en ligne: [Link]
[Link] 289
Chapitre 57: Tri du cycle
Examples
Cycle Trier les informations de base
Cycle Sort est un algorithme de tri utilisant un tri comparatif qui est théoriquement optimal en
termes de nombre total d'écritures dans le tableau d'origine, contrairement à tout autre algorithme
de tri sur place. Le tri par cycle est un algorithme de tri instable. Il est basé sur l'idée de
permutation dans laquelle les permutations sont prises en compte dans les cycles, qui font
individuellement pivoter et renvoient une sortie triée.
(input)
output = 0
for cycleStart from 0 to length(array) - 2
item = array[cycleStart]
pos = cycleStart
for i from cycleStart + 1 to length(array) - 1
if array[i] < item:
pos += 1
if pos == cycleStart:
continue
while item == array[pos]:
pos += 1
[Link] 290
array[pos], item = item, array[pos]
writes += 1
while pos != cycleStart:
pos = cycleStart
for i from cycleStart + 1 to length(array) - 1
if array[i] < item:
pos += 1
while item == array[pos]:
pos += 1
array[pos], item = item, array[pos]
writes += 1
return outout
Implémentation C #
[Link] 291
Chapitre 58: Tri du tas
Examples
Informations de base sur le tri du tas
Le tri du tas est une technique de tri basée sur la comparaison sur la structure de données du tas
binaire. Il est similaire au tri par sélection dans lequel nous trouvons d'abord l'élément maximum et
le mettons à la fin de la structure de données. Répétez ensuite le même processus pour les
éléments restants.
[Link] 292
Complexité temporelle: O(nlogn)
Implémentation C #
if (largest != i)
{
var temp = input[i];
input[i] = input[largest];
input[largest] = temp;
Heapify(input, n, largest);
}
}
[Link] 293
Chapitre 59: Tri par fusion
Examples
Fusionner les bases du tri
Un exemple:
[Link] 294
méthode principale. Il tombe dans le cas II de la méthode maîtresse et la solution de la récurrence
est Θ(nLogn) . La complexité temporelle du tri par fusion est Θ(nLogn) dans les 3 cas ( pire, moyen
et meilleur ) car le tri par fusion divise toujours le tableau en deux moitiés et prend un temps
linéaire pour fusionner deux moitiés.
Stable : Oui
C Fusionner le tri
i=0;
j=0;
for(k=l; k<=h; k++) { //process of combining two sorted arrays
if(arr1[i]<=arr2[j])
arr[k]=arr1[i++];
else
arr[k]=arr2[j++];
}
return 0;
}
[Link] 295
return 0;
}
C # Merge Sort
i = 0;
j = 0;
var k = l;
[Link] 296
static void SortMerge(int[] input, int l, int r)
{
if (l < r)
{
int m = l + (r - l) / 2;
SortMerge(input, l, m);
SortMerge(input, m + 1, r);
Merge(input, l, m, r);
}
}
public class MergeSort < T extends Comparable < T >> implements InPlaceSort < T > {
@Override
public void sort(T[] elements) {
T[] arr = (T[]) new Comparable[[Link]];
sort(elements, arr, 0, [Link] - 1);
}
private void merge(T[] a, T[] b, int low, int high, int mid) {
int i = low;
int j = mid + 1;
// We select the smallest element of the two. And then we put it into b
for (int k = low; k <= high; k++) {
[Link] 297
} else if (j > high && i <= mid) {
b[k] = a[i++];
} else if (i > mid && j <= high) {
b[k] = a[j++];
}
}
def mergeSort(A):
if len(A) <= 1:
return A
if len(A) == 2:
return sorted(A)
mid = len(A) / 2
return merge(mergeSort(A[:mid]), mergeSort(A[mid:]))
if __name__ == "__main__":
# Generate 20 random numbers and sort them
A = [randint(1, 100) for i in xrange(20)]
print mergeSort(A)
public MergeSortBU() {
}
private static void merge(Comparable[] arrayToSort, Comparable[] aux, int lo,int mid, int
hi) {
[Link] 298
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
if (i > mid)
arrayToSort[k] = aux[j++];
else if (j > hi)
arrayToSort[k] = aux[i++];
else if (isLess(aux[i], aux[j])) {
arrayToSort[k] = aux[i++];
} else {
arrayToSort[k] = aux[j++];
}
}
}
public static void sort(Comparable[] arrayToSort, Comparable[] aux, int lo, int hi) {
int N = [Link];
for (int sz = 1; sz < N; sz = sz + sz) {
for (int low = 0; low < N; low = low + sz + sz) {
[Link]("Size:"+ sz);
merge(arrayToSort, aux, low, low + sz -1 ,[Link](low + sz + sz - 1, N - 1));
print(arrayToSort);
}
}
package main
import "fmt"
[Link] 299
return a
}
m := (len(a)) / 2
f := mergeSort(a[:m])
s := mergeSort(a[m:])
return merge(f, s)
}
return a
}
func main() {
a := []int{75, 12, 34, 45, 0, 123, 32, 56, 32, 99, 123, 11, 86, 33}
[Link](a)
[Link](mergeSort(a))
}
[Link] 300
Chapitre 60: Tri par insertion
Remarques
Lorsque nous analysons les performances de l'algorithme de tri, nous nous intéressons
principalement au nombre de comparaison et d'échange.
Échange moyen
Soit E n le nombre total moyen d'échange à trier le tableau de l'élément N. E 1 = 0 (nous n'avons
besoin d'aucun échange pour le tableau avec un élément). Le nombre moyen d'échanges pour
trier le tableau d'éléments N est la somme du nombre moyen de numéros d'échange pour trier le
tableau d'éléments N-1 avec le commutateur moyen pour insérer le dernier élément dans le
tableau d'éléments N-1.
Élargit le terme
Comparaison moyenne
Soit C n le nombre total moyen de comparaison pour trier le tableau de l'élément N. C 1 = 0 (nous
n'avons pas besoin de comparaison sur un tableau d'éléments). Le nombre moyen de
comparaison pour trier le tableau d'éléments N est la somme du nombre moyen de nombres de
comparaison pour trier le tableau d'éléments N-1 avec la comparaison moyenne pour insérer le
dernier élément dans le tableau d'éléments N-1. Si le dernier élément est le plus grand élément,
nous n'avons besoin que d'une seule comparaison, si le dernier élément est le deuxième plus petit
élément, nous avons besoin d'une comparaison N-1. Cependant, si le dernier élément est le plus
petit élément, nous n'avons pas besoin de comparaison N. Nous avons toujours besoin de la
comparaison N-1. C'est pourquoi nous supprimons 1 / N dans l'équation ci-dessous.
[Link] 301
Simplifier la sommation (séries arithmétiques)
Développez le terme
Examples
Bases de l'algorithme
Le tri par insertion est un algorithme de tri très simple, stable et en place. Il fonctionne bien sur les
petites séquences mais il est beaucoup moins efficace sur les grandes listes. A chaque étape,
l'algorithme considère le i-ème élément de la séquence donnée, en le déplaçant vers la gauche
jusqu'à ce qu'il soit dans la bonne position.
Illustration graphique
[Link] 302
Pseudocode
for j = 1 to length(A)
n = A[j]
i = j - 1
while j > 0 and A[i] > n
A[i + 1] = A[i]
i = i - 1
A[i + 1] = n
Exemple
[5, 2, 4, 6, 1, 3]
1. [5, 2, 4, 6, 1, 3]
2. [2, 5, 4, 6, 1, 3]
3. [2, 4, 5, 6, 1, 3]
[Link] 303
4. [2, 4, 5, 6, 1, 3]
5. [1, 2, 4, 5, 6, 3]
6. [1, 2, 3, 4, 5, 6]
Implémentation C #
Implantation Haskell
[Link] 304
Chapitre 61: Tri rapide
Remarques
Parfois, Quicksort est également appelé tri par partition-échange.
Espace auxiliaire: O(n)
Complexité temporelle: pire O(n²) , meilleur O(nlogn)
Examples
Principes de base de Quicksort
Quicksort est un algorithme de tri qui sélectionne un élément ("le pivot") et réorganise le tableau
en formant deux partitions de telle sorte que tous les éléments inférieurs au pivot le précèdent et
que tous les éléments soient plus importants. L'algorithme est ensuite appliqué récursivement aux
partitions jusqu'à ce que la liste soit triée.
Ce schéma choisit un pivot qui est généralement le dernier élément du tableau. L'algorithme
maintient l'index pour placer le pivot dans la variable i et chaque fois qu'il trouve un élément
inférieur ou égal à pivot, cet index est incrémenté et cet élément serait placé avant le pivot.
[Link] 305
2. Schéma de partition Hoare:
Il utilise deux indices qui commencent aux extrémités du tableau en cours de partitionnement, puis
se déplacent l'un vers l'autre jusqu'à ce qu'ils détectent une inversion: une paire d'éléments, l'un
plus grand ou égal au pivot, l'autre plus petit ou égal, qui est incorrect. ordre les uns par rapport
aux autres. Les éléments inversés sont alors échangés. Lorsque les indices se rencontrent,
l'algorithme s'arrête et renvoie l'index final. Le schéma de Hoare est plus efficace que le schéma
de partition de Lomuto, car il effectue trois fois moins de swaps en moyenne et crée des partitions
efficaces, même lorsque toutes les valeurs sont égales.
Cloison :
[Link] 306
pivot := A[lo]
i := lo - 1
j := hi + 1
loop forever
do:
i := i + 1
while A[i] < pivot do
do:
j := j - 1
while A[j] > pivot do
if i >= j then
return j
Implémentation C #
[Link] 307
return input;
}
}
Implantation Haskell
return i;
}
Quicksort en Python
[Link] 308
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) / 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
print quicksort([3,6,8,10,1,2,1])
[Link] 309
Chapitre 62: Trier Pigeonhole
Examples
Pigeonhole Trier les informations de base
Pigeonhole Sort est un algorithme de tri adapté au tri des listes d'éléments dont le nombre
d'éléments (n) et le nombre de valeurs de clé possibles (N) sont approximativement les mêmes. Il
nécessite O (n + Range) temps où n est le nombre d'éléments dans le tableau en entrée et
«Range» est le nombre de valeurs possibles dans le tableau.
1. Recherchez les valeurs minimales et maximales dans le tableau. Laissez les valeurs
minimum et maximum être «min» et «max» respectivement. Vous trouverez également la
plage "max-min-1".
2. Mettre en place un tableau de «pigeonniers» initialement vides de la même taille que la
gamme.
3. Visitez chaque élément du tableau et placez chaque élément dans son casier. Une entrée
d'élément [i] est placée dans le trou à l'entrée de l'index [i] - min.
4. Commencez la boucle dans tout le tableau de casiers et réinsérez les éléments des trous
non vides dans le tableau d'origine.
Le tri des pigeonniers est similaire au tri par comptage, donc voici une comparaison entre le tri des
pigeonniers et le tri par comptage.
[Link] 310
Espace auxiliaire: O(n)
Complexité temporelle: O(n + N)
Implémentation C #
[Link] 311
}
[Link] 312
Chapitre 63: Vendeur ambulant
Remarques
Le problème du vendeur itinérant est le problème de trouver le coût minimum de déplacement à
travers N sommets exactement une fois par sommet. Il y a un coût de cost[i][j] pour passer du
sommet i au sommet j .
Algorithmes exacts
Algorithmes d'approximation
Être ajouté
Examples
Algorithme de force brutale
Un chemin à travers chaque sommet exactement une fois est identique à l'ordre du sommet d'une
certaine manière. Ainsi, pour calculer le coût minimum de déplacement à travers chaque sommet
exactement une fois, nous pouvons forcer chacun des N! permutations des nombres de 1 à N
Psuedocode
minimum = INF
for all permutations P
current = 0
current = current + cost[P[N-1]][P[0]] <- Add the cost of going from last vertex to
the first
output minimum
La complexité du temps
Il y a N! les permutations doivent se faire et le coût de chaque chemin est calculé dans O(N) , donc
[Link] 313
cet algorithme prend le temps O(N * N!) pour sortir la réponse exacte.
(1,2,3,4,6,0,5,7)
et le chemin
(1,2,3,5,0,6,7,4)
Le coût pour passer du sommet 1 au sommet 2 jusqu'au sommet 3 reste le même, alors pourquoi
faut-il le recalculer? Ce résultat peut être enregistré pour une utilisation ultérieure.
Soit dp[bitmask][vertex] le coût minimum de déplacement à travers tous les sommets dont le bit
correspondant dans bitmask est défini sur 1 terminant au vertex . Par exemple:
dp[12][2]
12 = 1 1 0 0
^ ^
vertices: 3 2 1 0
Puisque 12 représente 1100 en binaire, dp[12][2] représente les sommets 2 et 3 du graphe avec le
chemin se terminant au sommet 2.
[Link] 314
Ici, bitmask | (1 << i) place le bit i du bitmask à 1, ce qui représente que le iième sommet a été
visité. Le i après la virgule représente le nouveau pos dans cet appel de fonction, qui représente le
nouveau "dernier" sommet. cost[pos][i] est d'ajouter les frais de déplacement du sommet pos
Vertex i .
Ainsi, cette ligne consiste à mettre à jour la valeur du cost à la valeur minimale possible du
déplacement vers tous les autres sommets non encore visités.
La complexité du temps
La fonction TSP(bitmask,pos) a 2^N valeurs pour le bitmask et N valeurs pour pos . Chaque fonction
prend le temps O(N) pour s'exécuter (la boucle for ). Ainsi, cette implémentation prend du temps
O(N^2 * 2^N) pour sortir la réponse exacte.
[Link] 315
Chapitre 64: Vérifier que deux chaînes sont
des anagrammes
Introduction
Deux chaînes avec le même jeu de caractères sont appelées anagrammes. J'ai utilisé javascript
ici.
Nous allons créer un hachage de str1 et augmenter le nombre +1. Nous allons boucler la 2ème
chaîne et vérifier que tous les caractères sont présents dans le hachage et diminuer la valeur de
la clé de hachage. Vérifiez que toutes les valeurs de la clé de hachage sont nulles sera
anagramme.
Examples
Exemple d'entrée et de sortie
Ex1: -
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
}
Vous pouvez voir que hashKey 'o' contient la valeur 2 car o est 2 fois dans la chaîne.
Maintenant, bouclez sur str2 et vérifiez que chaque caractère est présent dans hashMap, si oui,
diminuez la valeur de la clé hashMap, sinon retournez false (qui indique qu'il ne s'agit pas d'une
anagramme).
[Link] 316
hashMap = {
s : 0,
t : 0,
a : 0,
c : 0,
k : 0,
o : 0,
v : 0,
e : 0,
r : 0,
f : 0,
l : 0,
w : 0
}
Maintenant, bouclez l'objet hashMap et vérifiez que toutes les valeurs sont nulles dans la clé de
hashMap.
Dans notre cas, toutes les valeurs sont nulles, donc c'est une anagramme.
(function(){
// Create hash map of str1 character and increase value one (+1).
createStr1HashMap(str1);
// Check str2 character are key in hash map and decrease value by one(-1);
var valueExist = createStr2HashMap(str2);
[Link] 317
function isStringsAnagram (valueExist) {
if(!valueExist) {
return valueExist;
} else {
var isAnagram;
for(var i in hashMap) {
if(hashMap[i] !== 0) {
isAnagram = false;
break;
} else {
isAnagram = true;
}
}
return isAnagram;
}
}
})();
[Link] 318
Chapitre 65: Vérifier si un arbre est BST ou
non
Examples
Si une arborescence donnée suit la propriété de l'arborescence de recherche
binaire ou non
Par exemple
si l'entrée est:
Si l'entrée est:
Un arbre binaire est BST s'il satisfait à l'une des conditions suivantes:
1. C'est vide
2. Il n'y a pas de sous-arbre
3. Pour chaque nœud x dans l'arborescence, toutes les clés (s'il y en a) dans la sous-
arborescence gauche doivent être inférieures à la clé (x) et toutes les clés (s'il y en a) dans
la sous-arborescence doivent être supérieures à la clé (x).
[Link] 319
Un algorithme récursif simple serait donc:
is_BST(root):
if root == NULL:
return true
L'algorithme récursif ci-dessus est correct mais inefficace, car il traverse chaque nœud à plusieurs
reprises.
Une autre approche pour minimiser les visites multiples de chaque nœud consiste à mémoriser
les valeurs min et max possibles des clés dans le sous-arbre que nous visitons. Soit la valeur
minimale possible de n'importe quelle clé soit K_MIN et la valeur maximale soit K_MAX . Lorsque nous
partons de la racine de l'arborescence, la plage de valeurs de l'arborescence est [K_MIN,K_MAX] .
Soit la clé du noeud racine x . Ensuite, la plage de valeurs dans le sous-arbre de gauche est
[K_MIN,x) et la plage de valeurs dans le sous-arbre de droite est (x,K_MAX] . Nous utiliserons cette
idée pour développer un algorithme plus efficace.
is_BST(my_tree_root,KEY_MIN,KEY_MAX)
Une autre approche consistera à effectuer une traversée de l'ordre de l'arbre binaire. Si la
traversée d'inorder produit une séquence de clés triée, l'arbre donné est un BST. Pour vérifier si la
séquence d'inordres est triée, mémorisez la valeur du nœud précédemment visité et comparez-la
au nœud actuel.
[Link] 320
Crédits
S.
Chapitres Contributeurs
No
A * Algorithme
2 TajyMany
Pathfinding
Algorithme Bellman
5 Bakhtiar Hasan, Sumeet Singh, user2314737, Yerken
– Ford
Algorithme de
7 Keyur Ramoliya
fenêtre coulissante
Algorithme de
9 Keyur Ramoliya
partition entier
Algorithme de
10 somme de chemin Keyur Ramoliya, mnoronha
maximale
Algorithme de sous-
11 Keyur Ramoliya, mnoronha
tableau maximum
Algorithme Floyd-
12 Bakhtiar Hasan, Sayakiss
Warshall
Algorithme Knuth
13 Vishwas
Morris Pratt (KMP)
algorithme lié à un
temps polynomial
14 Alber Tadrous
pour la couverture de
vertex minimum
[Link] 321
15 Algorithmes en ligne Andrii Artamonov, goeddek
Algorithmes
17 Julien Rousé
multithread
Ancêtre commun le
18 plus bas d'un arbre Isha Agarwal
binaire
Applications de la
19 technique EsmaeelE, goeddek, Tejus Prasad, user2314737
gourmande
Applications de
20 programmation Chris, user2314737
dynamique
Arbres de recherche a13ph, EsmaeelE, greatwolf, Isha Agarwal, Ishit Mehta, Mehedi
21
binaire Hasan, nbro, RamenChef, Rashik Hasnat, Tejus Prasad
Déformation
24 temporelle Bakhtiar Hasan, mnoronha, Zubayet Zaman Zico
dynamique
Exponentiation
27 Bakhtiar Hasan, mnoronha
Matricielle
Fonctions de
28 afeldspar, Didgeridoo, mnoronha
hachage
[Link] 322
Dijkstra
L'algorithme de
31 IVlad, Shubham, Yerken
Kruskal
Modifier l'algorithme
35 dynamique de Vishwas
distance
Problème de sac à Bakhtiar Hasan, dtt, Keyur Ramoliya, mnoronha, Tejus Prasad,
38
dos user2314737
Problème de
supersquence
39 Keyur Ramoliya
commun le plus
court
41 Pseudocode Community
Recherche de sous-
43 AnukuL, Bakhtiar Hasan, mnoronha, Rashik Hasnat
chaîne
Recherche en
44 Bakhtiar Hasan, mnoronha, Sumeet Singh, Zubayet Zaman Zico
largeur
Résolution
45 Minhas Kamal
d'équations
[Link] 323
Keyur Ramoliya, lambda, mnoronha, Teodor Kurtev,
47 Sélection Tri
user2314737
Transformée de
50 Dr. ABT, EsmaeelE
Fourier Rapide
Traversées d'arbres
51 Isha Agarwal
binaires
Traversées
52 Dian Bakti
graphiques
Vérifier si un arbre
65 Isha Agarwal, Janaky Murthy
est BST ou non
[Link] 324