0% ont trouvé ce document utile (0 vote)
10 vues338 pages

Introduction aux algorithmes et exemples

Transféré par

Eric
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
10 vues338 pages

Introduction aux algorithmes et exemples

Transféré par

Eric
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

algorithm

#algorithm
Table des matières
À propos 1

Chapitre 1: Commencer avec l'algorithme 2

Remarques 2

Introduction aux algorithmes 2

Examples 2

Un exemple de problème algorithmique 2

Premiers pas avec l'algorithme Simple Fizz Buzz dans Swift 3

Chapitre 2: A * Algorithme Pathfinding 6

Introduction 6

Examples 6

Exemple simple de A * Pathfinding: Un labyrinthe sans obstacles 6

Chapitre 3: A * Pathfinding 14

Examples 14

Introduction à A * 14

Résoudre le problème de 8 casse-tête en utilisant un algorithme A * 14

A * Pathfinding à travers un labyrinthe sans obstacles 16

Chapitre 4: Algo: - Imprime une matrice * en carré 24

Introduction 24

Examples 24

Exemple d'échantillon 24

Ecrire le code générique 24

Chapitre 5: Algorithme Bellman – Ford 26

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

Détection d'un cycle négatif dans un graphique 33

Chapitre 6: Algorithme catalan 36

Examples 36

Algorithme des nombres catalans Informations de base 36


Implémentation C # 37

Chapitre 7: Algorithme de fenêtre coulissante 38

Examples 38

Algorithme de fenêtre coulissante Informations de base 38

Implémentation de l'algorithme de fenêtre coulissante en C # 39

Chapitre 8: Algorithme de ligne 41

Introduction 41

Examples 41

Algorithme de dessin au trait de Bresenham 41

Chapitre 9: Algorithme de partition entier 45

Examples 45

Informations de base sur l'algorithme de partition entière 45

Implémentation de l'algorithme de partition d'interaction en C # 46

Chapitre 10: Algorithme de somme de chemin maximale 48

Examples 48

Informations de base sur la somme de chemin maximale 48

Implémentation C # 49

Chapitre 11: Algorithme de sous-tableau maximum 51

Examples 51

Algorithme du sous-tableau maximum Informations de base 51

Implémentation C # 53

Chapitre 12: Algorithme Floyd-Warshall 54

Examples 54

Algorithme du chemin le plus court de toutes les paires 54

Chapitre 13: Algorithme Knuth Morris Pratt (KMP) 57

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

Algorithme PMinVertexCover (graphique G) 59

Entrée connectée G 59

Ensemble de couverture de sommet minimum de sortie C 59

Chapitre 15: Algorithmes en ligne 61

Remarques 61

Théorie 61

Sources 63

Matériel de base 63

Lectures complémentaires 63

Code source 63

Examples 63

Paging (mise en cache en ligne) 63

Préface 63

Pagination 63

Approche hors ligne 64

Approche en ligne 65

Algorithmes de marquage 66

Chapitre 16: Algorithmes Gourmands 69

Remarques 69

Examples 69

Problème de sac à dos continu 69

Huffman Codage 69

Problème de changement 73

Problème de sélection d'activité 75

Le problème 75

Une analyse 75

La solution 77

Chapitre 17: Algorithmes multithread 78

Introduction 78
Syntaxe 78

Examples 78

Multithread à multiplication carrée 78

Multithread vectoriel de matrice de multiplication 78

fusionner-trier multithread 78

Chapitre 18: Ancêtre commun le plus bas d'un arbre binaire 80

Introduction 80

Examples 80

Trouver le plus petit ancêtre commun 80

Chapitre 19: Applications de la technique gourmande 81

Remarques 81

Sources 81

Examples 81

Automate à billet 81

Planification d'intervalle 84

Minimiser les retards 87

Mise en cache hors ligne 91

Exemple (FIFO) 91

Exemple (LFD) 92

FIFO 94

LIFO 95

LRU 96

LFU 97

LFD 99

Algorithme vs réalité 100

Chapitre 20: Applications de programmation dynamique 101

Introduction 101

Remarques 101

Définitions 101

Examples 101

Numéros de Fibonacci 101

Remarques 104
Chapitre 21: Arbres de recherche binaire 105

Introduction 105

Examples 105

Arbre de recherche binaire - Insertion (Python) 105

Arbre de recherche binaire - Suppression (C ++) 107

Ancêtre commun le plus bas dans un BST 109

Arbre de recherche binaire - Python 110

Chapitre 22: Complexité de l'algorithme 112

Remarques 112

Travail 113

Envergure 113

Examples 114

Notation Big Theta 114

Notation Big-Omega 115

Définition formelle 115

Remarques 115

Les références 116

Comparaison des notations asymptotiques 116

Liens 117

Chapitre 23: Comptage tri 119

Examples 119

Compter les informations de base du tri 119

Implémentation de Psuedocode 119

Implémentation C # 120

Chapitre 24: Déformation temporelle dynamique 121

Examples 121

Introduction à la déformation temporelle dynamique 121

Chapitre 25: Depth First Search 126

Examples 126

Introduction à la recherche en profondeur 126

Chapitre 26: Des arbres 132


Remarques 132

Examples 132

introduction 132

Représentation typique d'un arbre anary 133

Pour vérifier si deux arbres binaires sont identiques ou non 134

Chapitre 27: Exponentiation Matricielle 136

Examples 136

Exponentiation de matrice pour résoudre des problèmes d'exemple 136

Chapitre 28: Fonctions de hachage 141

Examples 141

Introduction aux fonctions de hachage 141

Méthodes de hachage 141

Table de hachage 141

Exemples 142

Liens 143

Codes de hachage pour les types courants en C # 143

Booléen 143

Byte , UInt16 , Int32 , UInt32 , Single 143

SByte 143

Carboniser 143

Int16 144

Int64 , double 144

UInt64 , DateTime , TimeSpan 144

Décimal 144

Objet 144

Chaîne 144

Type de valeur 144

Nullable <T> 145

Tableau 145

Les références 145

Chapitre 29: Graphique 146


Introduction 146

Remarques 146

Examples 146

Tri topologique 146

Instance de problème et sa solution 147

L'algorithme de Thorup 148

Détecter un cycle dans un graphe orienté à l'aide de la méthode Traject First Traversal 148

Introduction à la théorie des graphes 151

Stockage de graphiques (matrice d'adjacence) 155

Stockage de graphiques (liste d'adjacence) 159

Chapitre 30: L'algorithme de Dijkstra 162

Examples 162

Algorithme du plus court chemin de Dijkstra 162

Chapitre 31: L'algorithme de Kruskal 168

Remarques 168

Examples 168

Implémentation simple et plus détaillée 168

Implémentation simple, basée sur des ensembles disjoints 168

Implémentation optimale, basée sur des ensembles disjoints 169

Implémentation simple et de haut niveau 170

Chapitre 32: L'algorithme de Prim 171

Examples 171

Introduction à l'algorithme de Prim 171

Chapitre 33: La plus longue sous-séquence commune 179

Examples 179

Plus longue explication de sous-séquence commune 179

Chapitre 34: Le triangle de Pascal 185

Examples 185

Pascal's Triagle Informations de base 185

Implémentation du triangle de Pascal en C # 186

Triangle de Pascal en C 186


Chapitre 35: Modifier l'algorithme dynamique de distance 188

Examples 188

Modifications minimales requises pour convertir la chaîne 1 en chaîne 2 188

Chapitre 36: Notation Big-O 191

Remarques 191

Examples 192

Une boucle simple 192

Une boucle imbriquée 193

Un exemple O (log n) 194

introduction 194

Approche naïve 194

Dichotomie 194

Explication 195

Conclusion 195

O (log n) types d'algorithmes 195

Chapitre 37: Plus longue sous-séquence croissante 198

Examples 198

Information de base sur la sous-séquence croissante la plus longue 198

Implémentation C # 200

Chapitre 38: Problème de sac à dos 202

Remarques 202

Examples 202

Les bases du problème du sac à dos 202

Solution implémentée en C # 203

Chapitre 39: Problème de supersquence commun le plus court 204

Examples 204

Problème de supersquence commune le plus court 204

Implémentation du plus court problème de supersquence commune en C # 205

Chapitre 40: Programmation dynamique 207

Introduction 207

Remarques 207
Examples 207

Problème de sac à dos 207

Exemple C ++: 208

Python (2.7.11) Exemple: 208

Algorithme de planification des travaux pondérés 209

Modifier la distance 213

La plus longue sous-séquence commune 214

Numéro de Fibonacci 215

Plus longue sous-chaîne commune 216

Chapitre 41: Pseudocode 217

Remarques 217

Examples 217

Affectations variables 217

Dactylographié 217

Aucun type 217

Les fonctions 217

Chapitre 42: Recherche 219

Examples 219

Recherche binaire 219

introduction 219

Exemple de question 219

Exemple d'explication 219

Recherche binaire: sur des numéros triés 220

Recherche linéaire 221

Rabin Karp 222

Analyse de la recherche linéaire (pires, moyens et meilleurs cas) 223

Chapitre 43: Recherche de sous-chaîne 226

Examples 226

Algorithme KMP en C 226

Introduction à l'algorithme de Rabin-Karp 228

Introduction à l'algorithme de Knuth-Morris-Pratt (KMP) 231


Python Implémentation de l'algorithme KMP. 235

Chapitre 44: Recherche en largeur 237

Examples 237

Recherche du chemin le plus court de la source aux autres nœuds 237

Trouver le plus court chemin de la source dans un graphique 2D 244

Composants connectés de graphique non dirigé à l'aide de BFS. 245

Chapitre 45: Résolution d'équations 250

Examples 250

Équation linéaire 250

Équation non linéaire 252

Chapitre 46: Seau Sort 256

Examples 256

Seau Trier les informations de base 256

Implémentation C # 256

Chapitre 47: Sélection Tri 258

Examples 258

Sélection Tri Informations de base 258

Implémentation du tri par sélection en C # 260

Mise en œuvre de l'élixir 260

Chapitre 48: Sort de crêpes 262

Examples 262

Crêpes Trier les informations de base 262

Implémentation C # 263

Chapitre 49: Sort étrange 264

Examples 264

Informations élémentaires 264

Chapitre 50: Transformée de Fourier Rapide 267

Introduction 267

Examples 267

Radix 2 FFT 267

FFT Radix 2 Inverse 271

Chapitre 51: Traversées d'arbres binaires 274


Introduction 274

Examples 274

Traversée pré-commande, ordonnée et post-commande d'un arbre binaire 274

Traversée de niveau - implémentation 274

Chapitre 52: Traversées graphiques 277

Examples 277

Depth First Search Fonction traversale 277

Chapitre 53: Tri 278

Paramètres 278

Examples 278

Stabilité en tri 278

Chapitre 54: Tri à bulles 280

Paramètres 280

Examples 280

Tri à bulles 280

Implémentation en Javascript 281

Implémentation en C # 281

Implémentation en C & C ++ 282

Implémentation en Java 283

Implémentation Python 284

Chapitre 55: Tri de coquille 285

Examples 285

Shell Trier les informations de base 285

Implémentation C # 287

Chapitre 56: Tri de radix 288

Examples 288

Informations de base sur le tri Radix 288

Chapitre 57: Tri du cycle 290

Examples 290

Cycle Trier les informations de base 290

Mise en œuvre du pseudocode 290

Implémentation C # 291
Chapitre 58: Tri du tas 292

Examples 292

Informations de base sur le tri du tas 292

Implémentation C # 293

Chapitre 59: Tri par fusion 294

Examples 294

Fusionner les bases du tri 294

Fusion de l'implémentation du tri dans C & C # 295

Fusion de l'implémentation du tri en Java 297

Fusion de l'implémentation du tri en Python 298

Implémentation Java de bas en haut 298

Fusion de l'implémentation du tri dans Go 299

Chapitre 60: Tri par insertion 301

Remarques 301

Échange moyen 301

Comparaison moyenne 301

Examples 302

Bases de l'algorithme 302

Implémentation C # 304

Implantation Haskell 304

Chapitre 61: Tri rapide 305

Remarques 305

Examples 305

Principes de base de Quicksort 305

Implémentation C # 307

Implantation Haskell 308

Implémentation Java de la partition Lomuto 308

Quicksort en Python 308

Estampes "[1, 1, 2, 3, 6, 8, 10]" 309

Chapitre 62: Trier Pigeonhole 310

Examples 310

Pigeonhole Trier les informations de base 310


Implémentation C # 311

Chapitre 63: Vendeur ambulant 313

Remarques 313

Examples 313

Algorithme de force brutale 313

Algorithme de programmation dynamique 314

Chapitre 64: Vérifier que deux chaînes sont des anagrammes 316

Introduction 316

Examples 316

Exemple d'entrée et de sortie 316

Code générique pour les anagrammes 317

Chapitre 65: Vérifier si un arbre est BST ou non 319

Examples 319

Si une arborescence donnée suit la propriété de l'arborescence de recherche binaire ou non 319

Algorithme pour vérifier si un arbre binaire donné est BST 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

Introduction aux algorithmes


Les algorithmes sont omniprésents en informatique et en génie logiciel. La sélection d'algorithmes
et de structures de données appropriés améliore l'efficacité du programme en termes de coûts et
de temps.

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]

1. Durée . Un algorithme doit toujours se terminer après un nombre fini d'étapes.


2. Définitivité Chaque étape d'un algorithme doit être définie avec précision; les actions à
mener doivent être rigoureusement spécifiées. C'est cette qualité que [Cormen: 2001: IA:
580470] désigne avec le terme "bien défini".
3. Entrée Un algorithme a zéro ou plusieurs entrées . Ce sont des quantités qui sont données à
l'algorithme initialement avant qu'il ne commence ou dynamiquement pendant qu'il
s'exécute.
4. Sortie . Un algorithme a une ou plusieurs sorties . Ce sont des quantités qui ont une relation
spécifiée avec les entrées. Nous nous attendons à ce qu'un algorithme produise le même
résultat lorsqu'il reçoit la même entrée encore et encore.
5. Efficacité Un algorithme devrait également être efficace . Ses opérations doivent être
suffisamment simples pour pouvoir être effectuées exactement en principe et dans un temps
limité par un crayon et du papier.

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

Premiers pas avec l'algorithme Simple Fizz Buzz dans Swift

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.

Imaginez une série d'un nombre de 1 à 10.

1 2 3 4 5 6 7 8 9 10

Fizz et Buzz font référence à un nombre multiple de 3 et 5 respectivement. En d'autres termes, si


un nombre est divisible par 3, il est remplacé par fizz; si un nombre est divisible par 5, il est
remplacé par un buzz. Si un nombre est simultanément un multiple de 3 et 5, le numéro est
remplacé par "fizz buzz". En substance, il émule le célèbre jeu pour enfants "fizz buzz".

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é:

for num in number {


// Body and calculation goes here
}

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

for num in number {


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

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

let number = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

Nous avons augmenté la gamme des nombres de 1 à 10 à 1 à 15 afin de démontrer le concept


d'un «buzz de fizz». Puisque 15 est un multiple de 3 et 5, le nombre doit être remplacé par "buzz".
Essayez par vous-même et vérifiez la réponse!

Voici la solution:

for num in number {


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

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.

Voici le code final:

for num in number {


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

Aussi simple que cela, vous pouvez utiliser n'importe quelle langue de votre choix et commencer

Profitez du codage

Lire Commencer avec l'algorithme en ligne:


[Link]

[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

Disons que nous avons la grille 4 par 4 suivante:

[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:

1. La valeur "g" - C'est à quelle distance ce noeud est du carré vert.


2. La valeur "h" - C'est à quelle distance ce noeud est du carré rouge.
3. La valeur "f" - C'est la somme de la valeur "g" et de la valeur "h". C'est le nombre final qui
nous indique à quel nœud se déplacer.

Pour calculer ces heuristiques, voici la formule que nous allons utiliser: distance = abs(from.x -
to.x) + abs(from.y - to.y)

C'est ce qu'on appelle la formule "Manhattan Distance" .

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

Parfait. Maintenant, obtenons la valeur "f": 1 + 4 = 5

Ainsi, la valeur finale pour ce noeud est "5".

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?

Quel que soit celui qui a la valeur f la plus basse.

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

Lire A * Algorithme Pathfinding en ligne: [Link]


algorithme-pathfinding

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

g (n) est le coût minimum entre le nœud initial et n .

h (n) est le coût minimum de n à l'objectif le plus proche de 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-

Résoudre le problème de 8 casse-tête en utilisant un algorithme A *

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

Fonction coût total :

Donc, la fonction de coût total f(n) est donnée par,

f(n) = g(n) + h(n), where g(n) is the cost required to reach the current state from given
initial state

Solution à l'exemple de problème :

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.

Les états obtenus après avoir déplacé ces mouvements sont:

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.

A * Pathfinding à travers un labyrinthe sans obstacles

Disons que nous avons la grille 4 par 4 suivante:

[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:

1. La valeur "g" - C'est à quelle distance ce noeud est du carré vert.


2. La valeur "h" - C'est à quelle distance ce noeud est du carré rouge.
3. La valeur "f" - C'est la somme de la valeur "g" et de la valeur "h". C'est le nombre final qui
nous indique à quel nœud se déplacer.

Pour calculer ces heuristiques, voici la formule que nous allons utiliser: distance = abs(from.x -
to.x) + abs(from.y - to.y)

C'est ce qu'on appelle la formule "Manhattan Distance" .

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

Parfait. Maintenant, obtenons la valeur "f": 1 + 4 = 5

Ainsi, la valeur finale pour ce noeud est "5".

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?

Quel que soit celui qui a la valeur f la plus basse.

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

Lire A * Pathfinding en ligne: [Link]

[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

Ecrire le code générique

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);

Lire Algo: - Imprime une matrice * en carré en ligne:


[Link]

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

La complexité totale de l'algorithme est O(V*E) , où V - est le nombre de sommets et le nombre E


d'arêtes

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 :

if distance[u] + cost[u][v] < d[v]


d[v] = d[u] + cost[u][v]

Autrement dit, si la distance de la source à un sommet u + le poids du bord uv est inférieur à la


distance entre la source et un autre sommet v , nous mettons à jour la distance de la source à v .

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

Regardons un exemple. Nous avons un graphique:

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.

Nous allons assouplir les bords dans cette séquence:

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

Notre deuxième itération nous fournira le chemin en utilisant 2 nœuds. On a:

1. d [4] + coût [4] [5] = 12 < d [5] . d [5] = 12 . parent [5] = 4 .


2. d [3] + coût [3] [4] = 1 < d [4] . d [4] = 1 . parent [4] = 3 .
3. d [3] reste inchangé.
4. d [4] reste inchangé.
5. d [4] + coût [4] [6] = 6 < d [6] . d [6] = 6 . parent [6] = 4 .
6. d [3] reste inchangé.

Notre graphique ressemblera à:

[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:

Procedure Bellman-Ford(Graph, source):


n := number of vertices in Graph
for i from 1 to n
d[i] := infinity
parent[i] := NULL
end for
d[source] := 0
for i from 1 to n-1
flag := false

[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:

Procedure Bellman-Ford-With-Negative-Cycle-Detection(Graph, source):


n := number of vertices in Graph
for i from 1 to n
d[i] := infinity
parent[i] := NULL
end for
d[source] := 0
for i from 1 to n-1
flag := false
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
for all edges from (u,v) in Graph
if d[u] + cost[u][v] < d[v]
Return "Negative Cycle Detected"
end if
end for
Return d

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.

Pourquoi avons-nous besoin de détendre tous les bords au maximum (V-1)


fois

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:

Nous allons utiliser cette séquence:

+--------+--------+--------+--------+
| Serial | 1 | 2 | 3 |
+--------+--------+--------+--------+
| Edge | 3->4 | 2->3 | 1->2 |
+--------+--------+--------+--------+

Pour notre première itération:

1. d [3] + coût [3] [4] = infini . Cela ne changera rien.


2. d [2] + coût [2] [3] = infini . Cela ne changera rien.
3. d [1] + coût [1] [2] = 2 < d [2] . d [2] = 2 . parent [2] = 1 .

Nous pouvons voir que notre processus de relaxation n'a changé que d [2] . Notre graphique

[Link] 31
ressemblera à:

Deuxième itération:

1. d [3] + coût [3] [4] = infini . Cela ne changera rien.


2. d [2] + coût [2] [3] = 5 < d [3] . d [3] = 5 . parent [3] = 2.
3. Il ne sera pas changé.

Cette fois, le processus de relaxation a changé d [3] . Notre graphique ressemblera à:

Troisième itération:

1. d [3] + coût [3] [4] = 7 < d [4] . d [4] = 7 . parent [4] = 3 .


2. Il ne sera pas changé.
3. Il ne sera pas changé.

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:

1. d [1] + coût [1] [2] = 2 < d [2] . d [2] = 2 .


2. d [2] + coût [2] [3] = 5 < d [3] . d [3] = 5 .

[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) .

Détection d'un cycle négatif dans un graphique

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.

Supposons que nous ayons un graphique:

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:

Procedure NegativeCycleDetector(Graph, source):


n := number of vertices in Graph
for i from 1 to n
d[i] := infinity
end for
d[source] := 0
for i from 1 to n-1
flag := false
for all edges from (u,v) in Graph
if d[u] + cost[u][v] < d[v]
d[v] := d[u] + cost[u][v]
flag := true
end if
end for
if flag == false
break
end for
for all edges from (u,v) in Graph
if d[u] + cost[u][v] < d[v]
Return "Negative Cycle Detected"
end if
end for
Return "No Negative Cycle"

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.

Lire Algorithme Bellman – Ford en ligne: [Link]


bellman---ford

[Link] 35
Chapitre 6: Algorithme catalan
Examples
Algorithme des nombres catalans Informations de base

L'algorithme des nombres catalans est l'algorithme de programmation dynamique.

En mathématiques combinatoires, les nombres catalans forment une séquence de nombres


naturels qui se produisent dans divers problèmes de comptage, impliquant souvent des objets
définis récursivement. Les nombres catalans sur les entiers non négatifs n sont un ensemble de
nombres apparaissant dans les problèmes de dénombrement du type: «De combien de manières
un n-gon ordinaire peut-il être divisé en n-2 triangles si différentes orientations sont comptées
séparément?

Application de l'algorithme des nombres catalans:

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.

Exemple de numéro catalan:

Ici valeur de n = 4. (meilleur exemple - de Wikipedia)

[Link] 36
Espace auxiliaire: O(n)
Complexité temporelle: O(n^2)

Implémentation C #

public class CatalanNumber


{
public static int Main(int number)
{
int result = 0;
if (number <= 1) return 1;
for (int i = 0; i < number; i++)
{
result += Main(i)*Main(number - i - 1);
}
return result;
}
}

Lire Algorithme catalan en ligne: [Link]

[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

Elément maximum de tous les 3 éléments du tableau d'entrée:

+---------------------------------+---------+
| 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 |
+---+---+----+----+---+-----------+---------+

Elément minimum pour chaque élément du tableau d'entrée:

+---------------------------------+---------+
| 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éthodes pour trouver la somme de 3 éléments:

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.

Espace auxiliaire: O(n)

Complexité temporelle: O(n)

Implémentation de l'algorithme de fenêtre coulissante en C #

public class SlidingWindow


{
public static int[] MaxSlidingWindow(int[] input, int k)
{
int[] result = new int[[Link] - k + 1];
for (int i = 0; i <= [Link] - k; i++)
{
var max = input[i];
for (int j = 1; j < k; j++)
{
if (input[i + j] > max) max = input[i + j];
}
result[i] = max;
}
return result;
}

[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;
}

public static int[] MainMax(int[] input, int n)


{
return MaxSlidingWindow(input, n);
}

public static int[] MainMin(int[] input, int n)


{
return MinSlidingWindow(input, n);
}
}

Lire Algorithme de fenêtre coulissante en ligne:


[Link]

[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

Théorie de base: L'algorithme de dessin au trait de Bresenham est un algorithme efficace et


précis de génération de lignes de trame développé par Bresenham. Il ne s'agit que d'un calcul sur
nombres entiers, donc précis et rapide. Il peut également être étendu pour afficher des cercles
supplémentaires.

Dans l'algorithme de dessin au trait de Bresenham:

Pour pente | m | <1:


La valeur de x est augmentée
OU à la fois x et y est augmenté en utilisant le paramètre de décision.

Pour la pente | m |> 1:


La valeur de y est augmentée
OU à la fois x et y est augmenté en utilisant le paramètre de décision.

Algorithme pour la pente | m | <1:

1. Entrez deux points de fin (x1, y1) et (x2, y2) de la ligne.

2. Tracez le premier point (x1, y1).

3. Calculer
Delx = | x2 - x1 |
Dely = | y2 - y1 |

4. Obtenir le paramètre de décision initial comme


P = 2 * dely - delx

5. Pour I = 0 à delx dans l'étape de 1

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:

/* A C program to implement Bresenham line drawing algorithm for |m|<1 */


#include<stdio.h>
#include<conio.h>
#include<graphics.h>
#include<math.h>

int main()
{
int gdriver=DETECT,gmode;
int x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\\BGI");

printf("Enter the intial points: ");


scanf("%d",&x1);
scanf("%d",&y1);
printf("Enter the end points: ");
scanf("%d",&x2);
scanf("%d",&y2);

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
}

Algorithme pour la pente | m |> 1:

1. Entrez deux points de fin (x1, y1) et (x2, y2) de la ligne.


2. Tracez le premier point (x1, y1).
3. Calculer
Delx = | x2 - x1 |
Dely = | y2 - y1 |
4. Obtenir le paramètre de décision initial comme
P = 2 * delx - dely
5. Pour I = 0 à dely à l'étape de 1
Si p <0 alors
y1 = y1 + 1
Pot (x1, y1)
P = p + 2 delx

Autre
X1 = x1 + 1
Y1 = y1 + 1
Terrain (x1, y1)
P = p + 2 delx - 2 * dely
Fin si
Fin pour
6. FIN

Code source:

/* A C program to implement Bresenham line drawing algorithm for |m|>1 */


#include<stdio.h>
#include<conio.h>
#include<graphics.h>
#include<math.h>
int main()
{
int gdriver=DETECT,gmode;
int x1,y1,x2,y2,delx,dely,p,i;
initgraph(&gdriver,&gmode,"c:\\TC\\BGI");
printf("Enter the intial points: ");
scanf("%d",&x1);
scanf("%d",&y1);
printf("Enter the end points: ");
scanf("%d",&x2);
scanf("%d",&y2);
putpixel(x1,y1,RED);
delx=fabs(x2-x1);
dely=fabs(y2-y1);
p=(2*delx)-dely;
for(i=0;i<delx;i++){
if(p<0)
{
y1=y1+1;
putpixel(x1,y1,RED);

[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;
}

Lire Algorithme de ligne en ligne: [Link]

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

Exemple d'algorithme de partition d'entier:

[Link] 45
Espace auxiliaire: O(n^2)
Complexité temporelle: O(n(logn))

Implémentation de l'algorithme de partition d'interaction en C #

public class IntegerPartition


{
public static int[,] Result = new int[100,100];

private static int Partition(int targetNumber, int largestNumber)


{
for (int i = 1; i <= targetNumber; i++)
{
for (int j = 1; j <= largestNumber; j++)
{
if (i - j < 0)
{
Result[i, j] = Result[i, j - 1];
continue;
}

[Link] 46
Result[i, j] = Result[i, j - 1] + Result[i - j, j];
}
}
return Result[targetNumber, largestNumber];
}

public static int Main(int number, int target)


{
int i;
for (i = 0; i <= number; i++)
{
Result[i, 0] = 0;
}
for (i = 1; i <= target; i++)
{
Result[0, i] = 1;
}
return Partition(number, target);
}
}

Lire Algorithme de partition entier en ligne:


[Link]

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

Par exemple, laissez-nous un triangle tel que montré ci-dessous.

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.

Passons à l'approche de la programmation dynamique.

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.

Exemple d'algorithme de somme de chemin maximale:

[Link] 48
Espace auxiliaire: O(n)
Complexité temporelle: O(n)

Implémentation C #

public class Node


{

public int Value;


public Node Left, Right;

public Node(int item)


{
Value = item;
Left = Right = null;
}
}

class Res
{
public int Val;
}

public class MaximumPathSum


{
Node _root;
int FindMaxUtil(Node node, Res res)

[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);
}

int FindMaxSum(Node node)


{
Res res = new Res {Val = [Link]};
FindMaxUtil(node, res);
return [Link];
}

public static int Main()


{
MaximumPathSum tree = new MaximumPathSum
{
_root = new Node(10)
{
Left = new Node(2),
Right = new Node(10)
}
};
tree._root.[Link] = new Node(20);
tree._root.[Link] = new Node(1);
tree._root.[Link] = new Node(-25)
{
Left = new Node(3),
Right = new Node(4)
};
return [Link]();
}
}

Lire Algorithme de somme de chemin maximale en ligne:


[Link]

[Link] 50
Chapitre 11: Algorithme de sous-tableau
maximum
Examples
Algorithme du sous-tableau maximum Informations de base

Le problème du sous-tableau maximum est la méthode permettant de trouver le sous-tableau


contigu dans un tableau de nombres à une dimension ayant la plus grande somme.

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 .

Approche Brute-Force pour la solution:

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.

Pseudo-code pour l’approche de la force brutale:

MaxSubarray(array)
maximum = 0
for i in input
current = 0
for j in input
current += array[j]
if current > maximum
maximum = current
return maximum

La complexité temporelle de la méthode Brute-Force est O(n^2) . Alors passons à diviser et à


conquérir l'approche.

Approche de division et de conquête pour la solution:

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.

Approche de 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.

Pseudo-code de l'algorithme de Kadane:

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

La complexité temporelle de l'algorithme de Kadane est O(n) .

[Link] 52
Implémentation C #

public class MaximumSubarray


{
private static int Max(int a, int b)
{
return a > b ? a : b;
}

static int MaxSubArray(int[] input, int n)


{
int max = input[0];
int currentMax = input[0];
for (int i = 1; i < n; i++)
{
currentMax = Max(input[i], currentMax + input[i]);
max = Max(max, currentMax);
}
return max;
}

public static int Main(int[] input)


{
int n = [Link];
return MaxSubArray(input, n);
}
}

Lire Algorithme de sous-tableau maximum en ligne:


[Link]

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

Regardons un exemple. Nous allons appliquer l'algorithme de Floyd-Warshall sur ce graphique:

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 .

Pour appliquer l'algorithme de Floyd-Warshall, nous allons sélectionner un sommet médian k .


Ensuite, pour chaque sommet i , nous allons vérifier si nous pouvons aller de i à k , puis de k à j ,
où j est un autre sommet et minimiser le coût de passer de i à j . Si la distance actuelle [i] [j] est
supérieure à la distance [i] [k] + distance [k] [j] , nous allons mettre la distance [i] [j] égale à la
somme de ces deux distances . Et le chemin [i] [j] sera mis au chemin [k] [j] , car il vaut mieux
aller de i à k , puis k à j . Tous les sommets seront sélectionnés comme k . Nous aurons 3
boucles imbriquées: pour k allant de 1 à 4, je passe de 1 à 4 et j va de 1 à 4. Nous allons vérifier:

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

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:

Procedure PrintPath(source, destination):


s = Stack()
[Link](destination)
while Path[source][destination] is not equal to source
[Link](Path[source][destination])
destination := Path[source][destination]
end while
print -> source
while S is not empty
print -> [Link]
end while

Trouver un cycle de bord négatif:

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é:

La complexité de l'algorithme de Floyd-Warshall est O (V³) et la complexité de l'espace est: O (V²)


.

Lire Algorithme Floyd-Warshall en ligne: [Link]


floyd-warshall

[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

public class KMP {

[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]()));
}

public int[] computeLPS(char[] str){


int lps[] = new int[[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;
}

public boolean patternExistKMP(char[] text,char[] pat){


int[] lps = computeLPS(pat);
int i=0,j=0;
while(i<[Link] && j<[Link]){
if(text[i] == pat[j]){
i++;
j++;
}else{
if(j!=0){
j = lps[j-1];
}else{
i++;
}
}
}

if(j==[Link])
return true;
return false;
}

Lire Algorithme Knuth Morris Pratt (KMP) en ligne:


[Link]

[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

g Graphique connecté non dirigé d'entrée

X Ensemble de sommets

C Ensemble final des 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

Algorithme PMinVertexCover (graphique G)

Entrée connectée G

Ensemble de couverture de sommet minimum de sortie C

[Link] 59
Set C <- new Set<Vertex>()

Set X <- new Set<Vertex>()

X <- [Link]()

for v in X do
List<Vertex> adjacentVertices1 <- [Link](v)

if !C contains any of adjacentVertices1 then

[Link](v)

for vertex in C do

List<vertex> adjacentVertices2 <- [Link](vertex)

if C contains any of adjacentVertices2 then

[Link](vertex)

return C

C est la couverture minimale du sommet du graphe G

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.

Définition 2: Un algorithme en ligne A pour un problème de minimisation Π a un rapport


compétitif de r ≥ 1 s'il y a une constante τ∈ℜ avec

w A (σ) = f σ (A (σ)) ≤ r ⋅ OPT (& sigma) + τ

pour toutes les instances σ∈Σ Π . A s'appelle un algorithme en ligne r-concurrentiel . Est même

w A (σ) ≤ r ⋅ OPT (& sigma)

pour toutes les instances σ∈Σ Π alors A est appelé un algorithme en ligne strictement compétitif
.

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

Proposition 1.4: Chaque algorithme de marquage est strictement k-compétitif .

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

w A (σ) ≤ l⋅k ≤ (k + l-2) k ≤ OPT (σ) ⋅ k

Corollaire 1.5: LRU et FWF sont strictement k-compétitives .

N'y a-t-il pas de constante pour laquelle un algorithme en ligne A est concurrentiel, nous appelons
A non compétitif .

Proposition 1.6: LFU et LIFO ne sont pas compétitifs .

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

qui est égal à

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

1. Algorithmes en ligne de script (allemand), Heiko Roeglin, Université de Bonn


2. Algorithme de remplacement de page

Lectures complémentaires

1. Calcul en ligne et analyse concurrentielle par Allan Borodin et Ran El-Yaniv

Code source

1. Code source pour la mise en cache hors ligne


2. Code source du jeu adversaire

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 .

Il existe de nombreuses stratégies pour résoudre ce problème.

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

Il y a deux façons d'aborder ce problème:

1. hors ligne : la séquence des demandes de pages est connue à l'avance


2. en ligne : la séquence des demandes de pages n'est pas connue à l'avance

Approche hors ligne

Pour la première approche, examinez le sujet Applications de la technique gourmande . C'est la


troisième fois. Exemple La mise en cache hors ligne considère les cinq premières stratégies ci-
dessus et vous donne un bon point d'entrée pour les éléments suivants.

L'exemple de programme a été étendu à la stratégie FWF :

class FWF : public Strategy {


public:
FWF() : Strategy("FWF")
{
}

int apply(int requestIndex) override


{
for(int i=0; i<cacheSize; ++i)
{
if(cache[i] == request[requestIndex])
return i;

// after first empty page all others have to be empty


else if(cache[i] == emptyPage)
return i;
}

// no free pages
return 0;
}

void update(int cachePos, int requestIndex, bool cacheMiss) override


{

// no pages free -> miss -> clear cache

[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

Cache initial: (a,b,c)

Request cache 0 cache 1 cache 2 cache miss


a a b c
a a b c
d d X X x
e d e X
b d e b
b d e b
a a X X x
c a c X
f a c f
d d X X x
e d e X
a d e a
f f X X x
b f b X
e f b e
c c X X x

Total cache misses: 5

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:

Définition 1.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.

Définition 1.2: Un algorithme en ligne A pour un problème de minimisation Π a un rapport

[Link] 65
compétitif de r ≥ 1 s'il existe une constante τ∈ℜ avec

w A (σ) = f σ (A (σ)) ≤ r ⋅ OPT (σ) + τ

pour toutes les instances σ∈Σ Π . A s'appelle un algorithme en ligne r-concurrentiel . Est même

w A (σ) ≤ r ⋅ OPT (σ)

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:

• La phase 1 est la sous-séquence maximale de σ du début jusqu'à ce que k différentes


pages soient demandées.
• La phase i ≥ 2 est la sous-séquence maximale de σ à partir de la fin du passage i-1 jusqu'à
ce que k différentes pages soient demandées.

Par exemple avec k = 3:

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.

Proposition 1.4: Chaque algorithme de marquage est strictement k-compétitif .

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

w A (σ) ≤ l⋅k ≤ (k + l-2) k ≤ OPT (σ) ⋅ k

Corollaire 1.5: LRU et FWF sont strictement k-compétitives .

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

Proposition 1.6: LFU et LIFO ne sont pas compétitifs .

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

qui est égal à

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

Lire Algorithmes en ligne en ligne: [Link]


ligne

[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:

values[] = [1, 4, 5, 2, 10]


weights[] = [3, 2, 1, 2, 4]
k = 8

Production attendue:

maximumValueOfItemsInK = 20;

Algorithme:

1) Sort values and weights by value/weight.


values[] = [5, 10, 4, 2, 1]
weights[] = [1, 4, 2, 2, 3]
2) currentWeight = 0; currentValue = 0;
3) FOR i = 0; currentWeight < k && i < [Link]; i++ DO:
IF k - currentWeight < weights[i] DO
currentValue = currentValue + values[i];
currentWeight = currentWeight + weights[i];
ELSE
currentValue = currentValue + values[i]*(k - currentWeight)/weights[i]
currentWeight = currentWeight + weights[i]*(k - currentWeight)/weights[i]
END_IF
END_FOR
PRINT "maximumValueOfItemsInK = " + currentValue;

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 .

L'arbre construit nous fournira:

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

Pour notre exemple:

[Link] 71
Le pseudo-code ressemble à:

Procedure Huffman(C): // C is the set of n characters and related information


n = [Link]
Q = priority_queue()
for i = 1 to n
n = node(C[i])
[Link](n)
end for
while [Link]() is not equal to 1
Z = new node()
[Link] = x = [Link]
[Link] = y = [Link]
[Link] = [Link] + [Link]
[Link](Z)
end while
Return Q

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:

Le processus de décompression consiste simplement à traduire le flux de codes de préfixe en


valeur d'octet individuel, généralement en parcourant le nœud d'arborescence Huffman par nœud
à mesure que chaque bit est lu depuis le flux d'entrée. Atteindre un nœud feuille termine
nécessairement la recherche de cette valeur d'octet particulière. La valeur de feuille représente le
caractère souhaité. Habituellement, l'arbre Huffman est construit en utilisant des données ajustées
statistiquement à chaque cycle de compression, la reconstruction est donc assez simple. Sinon,
les informations pour reconstituer l'arborescence doivent être envoyées séparément. Le pseudo-
code:

Procedure HuffmanDecompression(root, S): // root represents the root of Huffman Tree


n := [Link] // S refers to bit-stream to be decompressed
for i := 1 to n
current = root
while [Link] != NULL and [Link] != NULL
if S[i] is equal to '0'
current := [Link]
else
current := [Link]
endif
i := i+1
endwhile
print [Link]
endfor

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 .

Nous pouvons le faire récursivement avec OCaml:

(* assuming the money system is sorted in decreasing order *)


let change_make money_system amount =
let rec loop given amount =
if amount = 0 then given
else
(* we find the first value smaller or equal to the remaining amount *)
let coin = [Link] ((>=) amount) money_system in
loop (coin::given) (amount - coin)
in loop [] amount

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)

let optsucc = function


| Some x -> Some (x+1)
| None -> None

(* 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).

Problème de sélection d'activité

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.

Activité n ° Heure de début heure de fin

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

Activité n ° Heure de début heure de fin

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

Activité n ° Heure de début heure de fin

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.

Activité n ° Heure de début heure de fin

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. Trier les activités par ses heures de fin.


2. Si l'activité à effectuer ne partage pas une heure commune avec les activités
effectuées précédemment, effectuez l'activité.

Permet d'analyser le premier exemple

Activité n ° Heure de début heure de fin

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.

voir ici pour l'aide au codage [Link]


selection-problem/

Lire Algorithmes Gourmands en ligne: [Link]


gourmands

[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

Multithread vectoriel de matrice de multiplication

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)

Voici la fonction auxiliaire qui effectue la fusion en parallèle.


p-merge suppose que les deux sous-tableaux à fusionner sont dans le même tableau mais ne
supposent pas qu'ils sont adjacents dans le tableau. C'est pourquoi nous avons besoin de p1, r1,
p2, r2 .

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

Et voici la fonction auxiliaire dichotomic-search.

x est la clé à rechercher dans le sous-tableau T [p..r].

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

Lire Algorithmes multithread en ligne: [Link]


multithread

[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:

L'ancêtre commun le plus bas des nœuds de valeur 1 et 4 est 2

L'ancêtre commun le plus bas des noeuds avec la valeur 1 et 5 est 3

Le plus petit ancêtre commun des nœuds de valeur 2 et 4 est 4

L'ancêtre commun le plus bas des noeuds avec la valeur 1 et 2 est 2

Lire Ancêtre commun le plus bas d'un arbre binaire en ligne:


[Link]

[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

Premier exemple simple:

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.

La technique gourmande pour l'échange est l'approche pseudo-algorithmique suivante:

Etape 1: pendant que D > 20 distribue 20 pièces et que D = D - 20


Étape 2: pendant que D > 10 distribue une pièce de 10 pièces et que D = D - 10
Etape 3: pendant que D > 5 distribue 5 pièces et que D = D - 5
Etape 4: pendant que D > 2 distribue 2 pièces et que D = D - 2
Etape 5: pendant que D > 1 distribue 1 pièce et que D = D - 1

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

Maintenant, le distributeur automatique de tickets en tant que programme (en C ++):

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

[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

// generate coin values


coinValues = readInCoinValues();

cout << "ticket price: ";


cin >> ticketPrice;

cout << "money paid: ";


cin >> paidMoney;

if(paidMoney <= ticketPrice)


{
cout << "No exchange money" << endl;
return 1;
}

int diffValue = paidMoney - ticketPrice;

// Here starts greedy

// we save how many coins we have to give out


std::vector<unsigned int> coinCount;

for(auto coinValue = [Link]();


coinValue != [Link](); ++coinValue)
{
int countCoins = 0;

while (diffValue >= *coinValue)


{
diffValue -= *coinValue;
countCoins++;
}

coinCount.push_back(countCoins);
}

// print out result


cout << "the difference " << paidMoney - ticketPrice
<< " is paid with: " << endl;

for(unsigned int i=0; i < [Link](); ++i)


{
if(coinCount[i] > 0)
cout << coinCount[i] << " coins with value "
<< coinValues[i] << endl;
}

return 0;
}

[Link] 82
std::vector<unsigned int> readInCoinValues()
{
// coin values
std::vector<unsigned int> coinValues;

// make sure 1 is in vectore


coinValues.push_back(1);

// read in coin values (attention: error handling is omitted)


while(true)
{
int coinValue;

cout << "Coin value (<1 to stop): ";


cin >> coinValue;

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

else
break;
}

// sort values
sort([Link](), [Link](), std::greater<int>());

// erase copies of same value


auto last = std::unique([Link](), [Link]());
[Link](last, [Link]());

// print array
cout << "Coin values: ";

for(auto i : coinValues)
cout << i << " ";

cout << endl;

return coinValues;
}

Sachez qu'il y a maintenant une vérification des entrées pour garder l'exemple simple. Un
exemple de sortie:

Coin value (<1 to stop): 2


Coin value (<1 to stop): 4
Coin value (<1 to stop): 7
Coin value (<1 to stop): 9
Coin value (<1 to stop): 14
Coin value (<1 to stop): 4
Coin value (<1 to stop): 0
Coin values: 14 9 7 4 2 1
ticket price: 34
money paid: 67
the difference 33 is paid with:
2 coins with value 14
1 coins with value 4
1 coins with value 1

[Link] 83
Tant que 1 est dans les valeurs de pièce, nous avons maintenant, que l'algorithme se terminera,
car:

• D diminue à chaque pas


• D est jamais >0 et plus petit que la plus petite pièce 1 en même temps

Mais l'algorithme a deux pièges:

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:

1. Trier les travaux en fonction de l'heure de fin pour que f1<=f2<=...<=fn


2. Soit A un ensemble vide
3. pour j=1 à n si j est compatible avec tous les jobs de A set A=A+{j}
4. A est un sous-ensemble maximum de travaux compatibles entre eux

Ou en tant que programme C ++:

#include <iostream>
#include <utility>
#include <tuple>
#include <vector>

[Link] 84
#include <algorithm>

const int jobCnt = 10;

// Job start times


const int startTimes[] = { 2, 3, 1, 4, 3, 2, 6, 7, 8, 9};

// Job end times


const int endTimes[] = { 4, 4, 3, 5, 5, 5, 8, 9, 9, 10};

using namespace std;

int main()
{
vector<pair<int,int>> jobs;

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


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

// step 1: sort
sort([Link](), [Link](),[](pair<int,int> p1, pair<int,int> p2)
{ return [Link] < [Link]; });

// step 2: empty set A


vector<int> A;

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

L'implémentation de l'algorithme est clairement dans Θ (n ^ 2). Il existe une implémentation Θ (n

[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?

Proposition: L’algorithme glouton le plus tôt possible est optimal.

Preuve: (par contradiction)

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.

Ce second exemple montre qu'il existe généralement de nombreuses stratégies gourmandes


possibles, mais que seules certaines d'entre elles, voire aucune, pourraient trouver la solution
optimale dans chaque cas.

Ci-dessous, un programme Java qui s'exécute dans log (n log n)

import [Link];
import [Link];

class Job
{
int start, finish, profit;

Job(int start, int finish, int profit)


{
[Link] = start;
[Link] = finish;
[Link] = profit;
}
}

class JobComparator implements Comparator<Job>


{
public int compare(Job a, Job b)
{
return [Link] < [Link] ? -1 : [Link] == [Link] ? 0 : 1;
}
}

public class WeightedIntervalScheduling


{
static public int binarySearch(Job jobs[], int index)
{
int lo = 0, hi = index - 1;

while (lo <= hi)


{
int mid = (lo + hi) / 2;

[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;
}

static public int schedule(Job jobs[])


{
[Link](jobs, new JobComparator());

int n = [Link];
int table[] = new int[n];
table[0] = jobs[0].profit;

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


{
int inclProf = jobs[i].profit;
int l = binarySearch(jobs, i);
if (l != -1)
inclProf += table[l];

table[i] = [Link](inclProf, table[i-1]);


}

return table[n-1];
}

public static void main(String[] args)


{
Job jobs[] = {new Job(1, 2, 50), new Job(3, 5, 20),
new Job(6, 19, 100), new Job(2, 100, 200)};

[Link]("Optimal profit is " + schedule(jobs));


}
}

Et le résultat attendu est:

Optimal profit is 250

Minimiser les retards

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

la plus petite solution de pile a des problèmes similaires

1 2

tj 1 5

dj 3 5

la dernière stratégie semble valide, nous commençons donc avec du pseudo-code:

1. Trier n travaux en temps d1<=d2<=...<=dn pour que d1<=d2<=...<=dn


2. Set t=0
3. pour j=1 à n
• Attribuer le travail j à l'intervalle [t,t+tj]
• set sj=t et fj=t+tj
• définir t=t+tj
4. intervalles de retour [s1,f1],[s2,f2],...,[sn,fn]

Et comme implémentation en C ++:

#include <iostream>
#include <utility>

[Link] 88
#include <tuple>
#include <vector>
#include <algorithm>

const int jobCnt = 10;

// Job start times


const int processTimes[] = { 2, 3, 1, 4, 3, 2, 3, 5, 2, 1};

// Job end times


const int dueTimes[] = { 4, 7, 9, 13, 8, 17, 9, 11, 22, 25};

using namespace std;

int main()
{
vector<pair<int,int>> jobs;

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


jobs.push_back(make_pair(processTimes[i], dueTimes[i]));

// step 1: sort
sort([Link](), [Link](),[](pair<int,int> p1, pair<int,int> p2)
{ return [Link] < [Link]; });

// step 2: set t=0


int t = 0;

// step 3:
vector<pair<int,int>> jobIntervals;

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


{
jobIntervals.push_back(make_pair(t,t+jobs[i].first));
t += jobs[i].first;
}

//step 4: print intervals


cout << "Intervals:\n" << endl;

int lateness = 0;

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


{
auto pair = jobIntervals[i];

lateness = max(lateness, [Link]-jobs[i].second);

cout << "(" << [Link] << "," << [Link] << ") "
<< "Lateness: " << [Link]-jobs[i].second << std::endl;
}

cout << "\nmaximal lateness is " << lateness << endl;

return 0;
}

Et la sortie de ce programme est la suivante:

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.

Proposition: La date limite la plus proche du premier horaire S est optimale.

Preuve: (par contradiction)

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.

Mise en cache hors ligne

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 .

Il existe de nombreuses stratégies gourmandes pour ce problème:

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)

Soit la taille du cache k=3 le cache initial a,b,c et la requête a,a,d,e,b,b,a,c,f,d,e,a,f,b,e,c :

Demande une une ré e b b une c F ré e une F b e c

cache 1 une une ré ré ré ré une une une ré ré ré F F F c

cache 2 b b b e e e e c c c e e e b b b

cache 3 c c c c b b b b F F F une une une e e

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)

Soit la taille du cache k=3 le cache initial a,b,c et la requête a,a,d,e,b,b,a,c,f,d,e,a,f,b,e,c :

Demande une une ré e b b une c F ré e une F b e c

cache 1 une une ré e e e e e e e e e e e e c

cache 2 b b b b b b une une une une une une F F F F

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

Huit caches manques sont beaucoup mieux.

Selftest : Faites l'exemple pour LIFO, LFU, RFU et regardez ce qui s'est passé.

L'exemple de programme suivant (écrit en C ++) comprend deux parties:

Le squelette est une application qui résout le problème en fonction de la stratégie gourmande
choisie:

#include <iostream>
#include <memory>

using namespace std;

const int cacheSize = 3;


const int requestLength = 16;

const char request[] = {'a','a','d','e','b','b','a','c','f','d','e','a','f','b','e','c'};


char cache[] = {'a','b','c'};

// for reset
char originalCache[] = {'a','b','c'};

class Strategy {

public:
Strategy(std::string name) : strategyName(name) {}
virtual ~Strategy() = default;

// calculate which cache place should be used


virtual int apply(int requestIndex) = 0;

// updates information the strategy needs


virtual void update(int cachePlace, int requestIndex, bool cacheMiss) = 0;

[Link] 92
const std::string strategyName;
};

bool updateCache(int requestIndex, Strategy* strategy)


{
// calculate where to put request
int cachePlace = strategy->apply(requestIndex);

// proof whether its a cache hit or a cache miss


bool isMiss = request[requestIndex] != cache[cachePlace];

// update strategy (for example recount distances)


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

// write to cache
cache[cachePlace] = request[requestIndex];

return isMiss;
}

int main()
{
Strategy* selectedStrategy[] = { new FIFO, new LIFO, new LRU, new LFU, new LFD };

for (int strat=0; strat < 5; ++strat)


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

cout <<"\nStrategy: " << selectedStrategy[strat]->strategyName << endl;

cout << "\nCache initial: (";


for (int i=0; i < cacheSize-1; ++i) cout << cache[i] << ",";
cout << cache[cacheSize-1] << ")\n\n";

cout << "Request\t";


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

int cntMisses = 0;

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


{
bool isMiss = updateCache(i, selectedStrategy[strat]);
if (isMiss) ++cntMisses;

cout << " " << request[i] << "\t";


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

cout<< "\nTotal cache misses: " << cntMisses << endl;


}

for(int i=0; i<5; ++i) delete selectedStrategy[i];


}

L'idée de base est simple: pour chaque demande, j'ai deux appels deux ma stratégie:

1. apply : La stratégie doit indiquer à l'appelant quelle page utiliser

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

Regardons maintenant des exemples d'implémentations pour nos cinq stratégies:

FIFO

class FIFO : public Strategy {


public:
FIFO() : Strategy("FIFO")
{
for (int i=0; i<cacheSize; ++i) age[i] = 0;
}

int apply(int requestIndex) override


{
int oldest = 0;

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


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

else if(age[i] > age[oldest])


oldest = i;
}

return oldest;
}

void update(int cachePos, int requestIndex, bool cacheMiss) override


{
// nothing changed we dont need to update the ages
if(!cacheMiss)
return;

// all old pages get older, the new one get 0


for(int i=0; i<cacheSize; ++i)
{
if(i != cachePos)
age[i]++;

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

Cache initial: (a,b,c)

Request cache 0 cache 1 cache 2 cache miss


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

Total cache misses: 13

C'est la solution exacte ci-dessus.

LIFO

class LIFO : public Strategy {


public:
LIFO() : Strategy("LIFO")
{
for (int i=0; i<cacheSize; ++i) age[i] = 0;
}

int apply(int requestIndex) override


{
int newest = 0;

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


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

else if(age[i] < age[newest])


newest = i;
}

return newest;
}

void update(int cachePos, int requestIndex, bool cacheMiss) override


{
// nothing changed we dont need to update the ages
if(!cacheMiss)
return;

// all old pages get older, the new one get 0


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

[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

Cache initial: (a,b,c)

Request cache 0 cache 1 cache 2 cache miss


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

Total cache misses: 9

LRU

class LRU : public Strategy {


public:
LRU() : Strategy("LRU")
{
for (int i=0; i<cacheSize; ++i) age[i] = 0;
}

// here oldest mean not used the longest


int apply(int requestIndex) override
{
int oldest = 0;

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


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

[Link] 96
else if(age[i] > age[oldest])
oldest = i;
}

return oldest;
}

void update(int cachePos, int requestIndex, bool cacheMiss) override


{
// all old pages get older, the used one get 0
for(int i=0; i<cacheSize; ++i)
{
if(i != cachePos)
age[i]++;

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

Cache initial: (a,b,c)

Request cache 0 cache 1 cache 2 cache miss


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

Total cache misses: 13

LFU

class LFU : public Strategy {


public:
LFU() : Strategy("LFU")
{

[Link] 97
for (int i=0; i<cacheSize; ++i) requestFrequency[i] = 0;
}

int apply(int requestIndex) override


{
int least = 0;

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


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

else if(requestFrequency[i] < requestFrequency[least])


least = i;
}

return least;
}

void update(int cachePos, int requestIndex, bool cacheMiss) override


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

else
++requestFrequency[cachePos];
}

private:

// how frequently was the page used


int requestFrequency[cacheSize];
};

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

Cache initial: (a,b,c)

Request cache 0 cache 1 cache 2 cache miss


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

[Link] 98
Total cache misses: 10

LFD

class LFD : public Strategy {


public:
LFD() : Strategy("LFD")
{
// precalc next usage before starting to fullfill requests
for (int i=0; i<cacheSize; ++i) nextUse[i] = calcNextUse(-1, cache[i]);
}

int apply(int requestIndex) override


{
int latest = 0;

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


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

else if(nextUse[i] > nextUse[latest])


latest = i;
}

return latest;
}

void update(int cachePos, int requestIndex, bool cacheMiss) override


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

private:

int calcNextUse(int requestPosition, char pageItem)


{
for(int i = requestPosition+1; i < requestLength; ++i)
{
if (request[i] == pageItem)
return i;
}

return requestLength + 1;
}

// next usage of page


int nextUse[cacheSize];
};

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)

Request cache 0 cache 1 cache 2 cache miss


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

Total cache misses: 8

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.

Lire Applications de la technique gourmande en ligne:


[Link]

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

Ce sujet étant intitulé Applications de programmation dynamique , il se concentrera davantage sur


les applications que sur le processus de création d’algorithmes de programmation dynamiques.

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.

Programmation dynamique - une méthode pour résoudre un problème complexe en la


décomposant en un ensemble de sous-problèmes plus simples, résolvant chacun de ces sous-
problèmes une seule fois et en stockant leurs solutions.

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]

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


[Link](result) # f(n) = f(n-1) + f(n-2)
return result

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

for i in range(2, n+1):


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

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

Programmation dynamique itérative avancée O(n) Complexité d'exécution, O(1) Complexité


spatiale, Pas de pile récursive

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

for i in range (2, n):


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

return memo[n%2]

Comme indiqué ci-dessus, l'approche de programmation dynamique itérative commence à partir


des cas de base et fonctionne jusqu'au résultat final. L'observation clé à faire pour arriver à la
complexité d'espace à O(1) (constante) est la même observation que nous avons faite pour la pile
récursive - nous avons seulement besoin de fibonacci(n-1) et de fibonacci(n-2) fibonacci(n) .
Cela signifie que nous avons seulement besoin de sauvegarder les résultats pour fibonacci(n-1)
et fibonacci(n-2) à tout moment de notre itération.

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

Lire Applications de programmation dynamique en ligne:


[Link]

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

Un exemple est présenté ci-dessous:

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.

Il y a trois cas à prendre en compte lors de la suppression d'un nœud.

• 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).

Explication des cas:

1. Lorsque le nœud à supprimer est un nœud feuille, supprimez simplement le nœud et


transmettez nullptr à son nœud parent.

[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).

Remarque: Le minimum dans la sous-arborescence de droite peut avoir au maximum un enfant


et cet enfant si c'est le cas si l'enfant de gauche signifie qu'il ne s'agit pas de la valeur minimale ou
s'il ne suit pas la propriété BST.

La structure d'un nœud dans un arbre et le code pour la suppression:

struct node
{
int data;
node *left, *right;
};

node* delete_node(node *root, int data)


{
if(root == nullptr) return root;
else if(data < root->data) root->left = delete_node(root->left, data);
else if(data > root->data) root->right = delete_node(root->right, data);

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;

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

root->data = temp->data;
root->right = delete_node(root->right, temp->data);
}
}
return root;
}

La complexité temporelle du code ci-dessus est O ( h ), où h est la hauteur de l'arbre.

[Link] 108
Ancêtre commun le plus bas dans un BST

Considérons le BST:

Le plus petit ancêtre commun de 22 et 26 est de 24

Le plus petit ancêtre commun de 26 et 49 est de 46

Le plus petit ancêtre commun de 22 et 24 est de 24

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;

else if(node1->data == root->data || node2->data== root->data)


return root;

else if((node1->data <= root->data && node2->data > root->data)


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

return root;
}

else if(root->data > max(node1->data,node2->data)){


return lowestCommonAncestor(root->left, node1, node2);
}

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

if [Link] < [Link]:


root.r_child = [Link](root.r_child, node)
else:
root.l_child = [Link](root.l_child, node)

return root

def in_order_place(self, root):


if not root:
return None
else:
self.in_order_place(root.l_child)
print [Link]
self.in_order_place(root.r_child)

def pre_order_place(self, root):


if not root:
return None
else:
print [Link]
self.pre_order_place(root.l_child)
self.pre_order_place(root.r_child)

def post_order_place(self, root):


if not root:
return None
else:
self.post_order_place(root.l_child)
self.post_order_place(root.r_child)
print [Link]

"" "Créer un noeud différent et y insérer des données" ""

r = Node(3)
node = BinarySearchTree()
nodeList = [1, 8, 5, 12, 14, 6, 15, 7, 16, 8]

for nd in nodeList:
[Link](r, Node(nd))

print "------In order ---------"


print (node.in_order_place(r))
print "------Pre order ---------"
print (node.pre_order_place(r))

[Link] 110
print "------Post order ---------"
print (node.post_order_place(r))

Lire Arbres de recherche binaire en ligne: [Link]


recherche-binaire

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

Comment se produisent les dépendances? Prenons par exemple le code suivant:

total = 0
for(i = 1; i < 10; i++)
total = total + i

Dans ce pseudo-code, chaque itération de la boucle for dépend du résultat de l'itération


précédente car nous utilisons la valeur calculée dans l'itération précédente dans cette itération
suivante. Le DAG de ce code peut ressembler à ceci:

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

La notation Big-Theta est symétrique: f(x) = Ө(g(x)) <=> g(x) = Ө(f(x))

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.

L'expression mathématique complète de la notation Big-Theta est la suivante:


Ө (f (x)) = {g: N 0 -> R et c 1 , c 2 , n 0 > 0, où c 1 <abs (g (n) / f (n)), pour tout n> n 0 et abs est la
valeur absolue}

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!

Définition mathématique formelle

Ө(g(x)) est un ensemble de fonctions.

Ө(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:

K|g(x)| <= f(x) <= L|g(x)| pour tout x >= x0 .

La définition est égale à:

f(x) = O(g(x)) and f(x) = Ω(g(x))

Une méthode qui utilise des limites

[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))

Classes de complexité communes

prénom Notation n = 10 n = 100

Constant Ө (1) 1 1

Logarithmique Ө (log (n)) 3 7

Linéaire Ө (n) dix 100

Linearithmique Ө (n * log (n)) 30 700

Quadratique Ө (n ^ 2) 100 10 000

Exponentiel Ө (2 ^ n) 1 024 1.267650e + 30

Factorial Ө (n!) 3 628 800 9.332622e + 157

Notation Big-Omega

La notation Ω est utilisée pour la borne inférieure asymptotique.

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:

0 ≤ cg(n) ≤ f(n) for all n ≥ n0 .

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

De la définition des notations suit le théorème:

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

La notation graphique Ω peut être représentée comme suit:

[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".

Comparaison des notations asymptotiques

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.

Notation f (n) = O (g (n)) f (n) = Ω (g (n))

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

Exemple 7n + 10 = O(n^2 + n - 9) n^3 - 34 = Ω(10n^2 - 7n + 1)

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.

Lire Complexité de l'algorithme en ligne: [Link]


de-l-algorithme

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

Exemple de tri par comptage

Espace auxiliaire: O(n+k)


Complexité temporelle: pire cas: O(n+k) , meilleur cas: O(n) , cas moyen O(n+k)

Implémentation de Psuedocode

Contraintes:

1. Entrée (un tableau à trier)


2. Nombre d'éléments dans l'entrée (n)
3. Touches dans la gamme de 0..k-1 (k)

[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 #

public class CountingSort


{
public static void SortCounting(int[] input, int min, int max)
{
var count = new int[max - min + 1];
var z = 0;

for (var i = 0; i < [Link]; i++)


count[i] = 0;

foreach (int i in input)


count[i - min]++;

for (var i = min; i <= max; i++)


{
while (count[i - min]-- > 0)
{
input[z] = i;
++z;
}
}
}

public static int[] Main(int[] input)


{
SortCounting(input, [Link](), [Link]());
return input;
}
}

Lire Comptage tri en ligne: [Link]

[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}

Nous voulons trouver la correspondance optimale entre ces deux séquences.

Au début, nous définissons la distance entre deux points, d (x, y) où x et y représentent les deux
points. Laisser,

d(x, y) = |x - y| //absolute difference

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,

Table[i][j] := d(i, j) + min(Table[i-1][j], Table[i-1][j-1], Table[i][j-1])

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:

if Table[i-1][j-1] <= Table[i-1][j] and Table[i-1][j-1] <= Table[i][j-1]


i := i - 1
j := j - 1
else if Table[i-1][j] <= Table[i-1][j-1] and Table[i-1][j] <= Table[i][j-1]
i := i - 1
else
j := j - 1
end if

Nous continuerons jusqu'à ce que nous atteignions (0, 0) . Chaque mouvement a sa propre
signification:

• Un mouvement horizontal représente la suppression. Cela signifie que notre séquence de


test s'est accélérée pendant cet intervalle.
• Un déplacement vertical représente une insertion. Cela signifie que la séquence de test est
décélérée pendant cet intervalle.
• Un mouvement en diagonale représente une correspondance. Pendant cette période, Test
et Sample étaient les mêmes.

[Link] 123
Notre pseudo-code sera:

Procedure DTW(Sample, Test):


n := [Link]
m := [Link]
Create Table[n + 1][m + 1]
for i from 1 to n
Table[i][0] := infinity
end for
for i from 1 to m
Table[0][i] := infinity
end for
Table[0][0] := 0
for i from 1 to n
for j from 1 to m
Table[i][j] := d(Sample[i], Test[j])
+ minimum(Table[i-1][j-1], //match
Table[i][j-1], //insertion
Table[i-1][j]) //deletion
end for
end for
Return Table[n + 1][m + 1]

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é:

La complexité du calcul de DTW est O (m * n) où m et n représentent la longueur de chaque


séquence. Les techniques plus rapides de calcul de DTW incluent PrunedDTW, SparseDTW et
FastDTW.

Applications:

[Link] 124
• Reconnaissance de mots parlés
• Analyse de puissance de corrélation

Lire Déformation temporelle dynamique en ligne:


[Link]

[Link] 125
Chapitre 25: Depth First Search
Examples
Introduction à la recherche en profondeur

La recherche en profondeur est un algorithme permettant de parcourir ou de rechercher des


structures de données arborescentes ou graphiques. On commence à la racine et explore le plus
loin possible le long de chaque branche avant de revenir en arrière. Le mathématicien français
Charles Pierre Trémaux, un mathématicien français du XIXe siècle, a étudié une version de la
recherche en profondeur pour résoudre les labyrinthes.

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

Regardons un exemple. Nous allons parcourir ce graphique:

Nous allons parcourir le graphique en suivant ces règles:

• Nous allons commencer par la source.


• Aucun nœud ne sera visité deux fois.
• Les nœuds que nous n'avons pas encore visités seront colorés en blanc.
• Le nœud que nous avons visité, mais qui n'a pas visité tous ses nœuds enfants, sera coloré

[Link] 126
en gris.
• Les nœuds complètement parcourus seront colorés en noir.

Regardons ça pas à pas:

[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).

1. Lorsqu'un nœud v passe du blanc au gris, l'heure est enregistrée en d [v] .


2. Lorsqu'un nœud v passe du gris au noir, l'heure est enregistrée dans f [v] .

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.

Applications de la recherche en profondeur d'abord:

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

Pour être un arbre, un graphique doit satisfaire à deux exigences:

• C'est acyclique. Il ne contient aucun cycle (ou "boucles").


• Il est connecté. Pour tout nœud donné du graphe, chaque nœud est accessible. Tous les

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

il est courant de faire référence à un arbre en tant Rooted Tree par:

choosing 1 cell to be called `Root`


painting the `Root` at the top
creating lower layer for each cell in the graph depending on their distance from the root -the
bigger the distance, the lower the cells (example above)

symbole commun pour les arbres: T

Représentation typique d'un arbre anary

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;
}

void printtree_r(struct node *node, int depth)


{
int i;

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;
}
}
}

void printtree(node *root)


{
printree_r(root, 0);
}

Pour vérifier si deux arbres binaires sont identiques ou non

1. Par exemple si les entrées sont:

Exemple 1

une)

b)

La sortie devrait être vraie.

Exemple: 2

Si les entrées sont:

une)

[Link] 134
b)

La sortie devrait être fausse.

Pseudo-code pour le même:

boolean sameTree(node root1, node root2){

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


return true;

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


return false;

if(root1->data == root2->data
&& sameTree(root1->left,root2->left)
&& sameTree(root1->right, root2->right))
return true;

Lire Des arbres en ligne: [Link]

[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].

| ----- | X | f(n) | = | ------ |


| 1 0 | | f(n-1) | | f(n) |

Ensuite , nous obtenons notre 2 X 2 désiré matrice M.

| 1 1 | X | f(n) | = | f(n+1) |
| 1 0 | | f(n-1) | | f(n) |

Ces matrices sont simplement dérivées à l'aide de la multiplication matricielle.

Type 2:

Rendons-le un peu complexe: trouver f(n) = a X f(n-1) + b X f(n-2) , où a et b sont des


constantes.
Cela nous dit, f(n+1) = a X f(n) + b X f(n-1) .
A ce stade, il convient de préciser que la dimension des matrices sera égale au nombre de
dépendances, c’est-à-dire dans cet exemple, à nouveau 2. Ainsi, pour A et B , nous pouvons
construire deux matrices de taille 2 X 1 :

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.

f(n) = f(n-1) + f(n-2) + c


f(n+1) = f(n) + f(n-1) + c
f(n+2) = f(n+1) + f(n) + c
.................... so on

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:

Disons-le simplement: trouver f(n) = a X f(n-1) + c X f(n-3) + d X f(n-4) + e . Laissons-le

[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:

Parfois, la récurrence est donnée comme ceci:

f(n) = f(n-1) -> if n is odd


f(n) = f(n-2) -> if n is even

En bref:

f(n) = (n&1) X f(n-1) + (!(n&1)) X f(n-2)

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:

g(n) = 2g(n-1) + 2g(n-2) + f(n)

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:

• fonctions de hachage se comportent comme une distribution uniforme

• les fonctions de hachage sont déterministes. h(x) devrait toujours retourner la même valeur
pour un x donné

• calcul rapide (a l'exécution O (1))

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

Méthode Fonction de hachage

Méthode de division h(x) = x mod m

Méthode de multiplication h(x) = ⌊m (xA mod 1)⌋, A ∈ {z ∈ ℝ: 0 < z < 1}

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

2. Adressage ouvert: zéro ou un élément est stocké dans chaque emplacement

Les méthodes suivantes sont utilisées pour calculer les séquences de sonde requises pour
l'adressage ouvert

Méthode Formule

Palpage linéaire h(x, i) = (h'(x) + i) mod m

Sondage quadratique h(x, i) = (h'(x) + c1*i + c2*i^2) mod m

Double hachage h(x, i) = (h1(x) + i*h2(x)) mod m

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.

X m = 100 (pas premier) m = 101 (prime)

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.

• Vue d'ensemble des tables de hachage

• Wolfram MathWorld - Fonction de hachage

Codes de hachage pour les types courants en C #

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

1 si la valeur est vraie, 0 sinon.

Byte , UInt16 , Int32 , UInt32 , Single

Valeur (si nécessaire converti en Int32).

SByte

((int)m_value ^ (int)m_value << 8);

Carboniser

(int)m_value ^ ((int)m_value << 16);

[Link] 143
Int16

((int)((ushort)m_value) ^ (((int)m_value) << 16));

Int64 , double

Xor entre les 32 bits inférieur et supérieur du numéro 64 bits

(unchecked((int)((long)m_value)) ^ (int)(m_value >> 32));

UInt64 , DateTime , TimeSpan

((int)m_value) ^ (int)(m_value >> 32);

Décimal

((((int *)&dbl)[0]) & 0xFFFFFFF0) ^ ((int *)&dbl)[1];

Objet

[Link](this);

L'implémentation par défaut est utilisée pour l' index du bloc de synchronisation .

Chaîne

Le calcul du code de hachage dépend du type de plate-forme (Win32 ou Win64), caractéristique


de l'utilisation du hachage de chaîne aléatoire, du mode Debug / Release. En cas de plateforme
Win64:

int hash1 = 5381;


int hash2 = hash1;
int c;
char *s = src;
while ((c = s[0]) != 0) {
hash1 = ((hash1 << 5) + hash1) ^ c;
c = s[1];
if (c == 0)
break;
hash2 = ((hash2 << 5) + hash2) ^ c;
s += 2;
}
return hash1 + (hash2 * 1566083941);

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>

return hasValue ? [Link]() : 0;

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

Lire Fonctions de hachage en ligne: [Link]


hachage

[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 graphiques peuvent être dirigés ou non.

• 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 est important de noter que chaque DAG a au moins un tri topologique.

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.

Un tri topologique peut être effectué en temps (V + E) , puisque l'algorithme de recherche en


profondeur prend (V + E) et nécessite Ω(1) (temps constant) pour insérer chacun de |V| sommets
dans le front d'une liste chaîné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

Instance de problème et sa solution


Soit un vertice v décrire une Task(hours_to_complete: int) , c. -à- Task(4) décrit un Task de qui
prend 4 heures pour terminer, et un bord e décrivent un Cooldown(hours: int) de Cooldown(3)
Cooldown(hours: int) de telle sorte que de Cooldown(3) présente une durée de temps de refroidir
après une tâche terminée.

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));

il y a alors trois ordres topologiques possibles entre A et E ,

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


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

[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).

Détecter un cycle dans un graphe orienté à l'aide de la méthode Traject First

[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>

using namespace std;

#define NUM_V 4

bool helper(list<int> *graph, int u, bool* visited, bool* recStack)


{
visited[u]=true;
recStack[u]=true;
list<int>::iterator i;
for(i = graph[u].begin();i!=graph[u].end();++i)
{
if(recStack[*i]) //if vertice v is found in recursion stack of this DFS traversal
return true;
else if(*i==u) //if there's an edge from the vertex to itself
return true;
else if(!visited[*i])
{ if(helper(graph, *i, visited, recStack))
return true;
}
}
recStack[u]=false;
return false;
}
/*
/The wrapper function calls helper function on each vertices which have not been visited.
Helper function returns true if it detects a back edge in the subgraph(tree) or false.
*/
bool isCyclic(list<int> *graph, int V)
{
bool visited[V]; //array to track vertices already visited
bool recStack[V]; //array to track vertices in recursion stack of the traversal.

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.

Chemin du chevalier dans un échiquier

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.

Graphique dirigé et non dirigé:

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.

Graphique pondéré et non pondéré:

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

capacités, en fonction du problème rencontré.

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:

• In-degree: nombre d'arêtes pointant vers le nœud.


• Out-degree: nombre de fronts qui pointent du nœud vers d'autres nœuds.

Pour les graphiques non orientés, ils sont simplement appelés degré.

[Link] 154
Quelques algorithmes liés à la théorie des graphes

• Algorithme Bellman – Ford


• L'algorithme de Dijkstra
• Algorithme Ford – Fulkerson
• L'algorithme de Kruskal
• Algorithme du plus proche voisin
• L'algorithme de Prim
• Profondeur première recherche
• Recherche en largeur

Stockage de graphiques (matrice d'adjacence)

Pour stocker un graphique, deux méthodes sont communes:

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

Le pseudo-code pour créer la matrice:

Procedure AdjacencyMatrix(N): //N represents the number of nodes

[Link] 156
Matrix[N][N]
for i from 1 to N
for j from 1 to N
Take input -> Matrix[i][j]
endfor
endfor

Nous pouvons également remplir la matrice en utilisant cette méthode commune:

Procedure AdjacencyMatrix(N, E): // N -> number of nodes


Matrix[N][E] // E -> number of edges
for i from 1 to E
input -> n1, n2, cost
Matrix[n1][n2] = cost
Matrix[n2][n1] = cost
endfor

Pour les graphes dirigés, nous pouvons supprimer Matrix [n2] [n1] = ligne de coût .

Les inconvénients de l'utilisation d'Adjacency Matrix:

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.

Code Java implémenté en utilisant le pseudo-code ci-dessus:

import [Link];

public class Represent_Graph_Adjacency_Matrix


{
private final int vertices;
private int[][] adjacency_matrix;

public Represent_Graph_Adjacency_Matrix(int v)
{
vertices = v;
adjacency_matrix = new int[vertices + 1][vertices + 1];
}

public void makeEdge(int to, int from, int edge)


{
try
{
adjacency_matrix[to][from] = edge;
}
catch (ArrayIndexOutOfBoundsException index)
{

[Link] 157
[Link]("The vertices does not exists");
}
}

public int getEdge(int to, int from)


{
try
{
return adjacency_matrix[to][from];
}
catch (ArrayIndexOutOfBoundsException index)
{
[Link]("The vertices does not exists");
}
return -1;
}

public static void main(String args[])


{
int v, e, count = 1, to = 0, from = 0;
Scanner sc = new Scanner([Link]);
Represent_Graph_Adjacency_Matrix graph;
try
{
[Link]("Enter the number of vertices: ");
v = [Link]();
[Link]("Enter the number of edges: ");
e = [Link]();

graph = new Represent_Graph_Adjacency_Matrix(v);

[Link]("Enter the edges: <to> <from>");


while (count <= e)
{
to = [Link]();
from = [Link]();

[Link](to, from, 1);


count++;
}

[Link]("The adjacency matrix for the given graph is: ");


[Link](" ");
for (int i = 1; i <= v; i++)
[Link](i + " ");
[Link]();

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


{
[Link](i + " ");
for (int j = 1; j <= v; j++)
[Link]([Link](i, j) + " ");
[Link]();
}

}
catch (Exception E)
{
[Link]("Somthing went wrong");
}

[Link] 158
[Link]();
}
}

Exécution du code: Enregistrez le fichier et compilez en utilisant javac


Represent_Graph_Adjacency_Matrix.java

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

Stockage de graphiques (liste d'adjacence)

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.

Voyons un graphique et sa matrice d'adjacence:

Maintenant, nous créons une liste en utilisant ces valeurs.

[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:

Procedure Adjacency-List(maxN, E): // maxN denotes the maximum number of nodes


edge[maxN] = Vector() // E denotes the number of edges
for i from 1 to E
input -> x, y // Here x, y denotes there is an edge between x, y
edge[x].push(y)
edge[y].push(x)
end for
Return edge

Comme celui-ci est un graphe non orienté, il y a un bord de x à y , il y a aussi un bord de y à x . Si


c'était un graphique dirigé, nous omettions le second. Pour les graphiques pondérés, nous devons
également stocker les coûts. Nous allons créer un autre vecteur ou une liste nommée cost []
pour les stocker. Le pseudo-code:

Procedure Adjacency-List(maxN, E):


edge[maxN] = Vector()
cost[maxN] = Vector()
for i from 1 to E
input -> x, y, w

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

Lire Graphique en ligne: [Link]

[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,

d[A] + cost[A][B] < d[B]

Ensuite, nous mettons à jour,

d[B] = d[A] + cost[A][B]

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:

Supposons que le nœud 1 soit la source . Alors,

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:

procedure BFSmodified(G, source):


Q = queue()
distance[] = infinity
[Link](source)
distance[source]=0
while Q is not empty
u <- [Link]()
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]
end if
end for
end while
Return distance

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:

procedure dijkstra(G, source):


Q = priority_queue()
distance[] = infinity
[Link](source)
distance[source] = 0
while Q is not empty
u <- nodes in Q with minimum distance[]

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

L'algorithme de Dijkstra fonctionne-t-il maintenant lorsqu'il y a un bord négatif? S'il y a un cycle


négatif, la boucle infinie se produira, car elle réduira le coût à chaque fois. Même s'il y a un
avantage négatif, Dijkstra ne fonctionnera pas, à moins que nous ne revenions juste après le
déclenchement de la cible. Mais ce ne sera pas un algorithme de Dijkstra. Nous aurons besoin de
l'algorithme Bellman – Ford pour traiter les fronts / cycles négatifs.

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;

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


if (sptSet[v] == false && dist[v] <= min)
{
min = dist[v];
min_index = v;
}

return min_index;
}

void printSolution(int dist[], int n)


{
[Link]("Vertex Distance from Source");
for (int i = 0; i < V; i++)
[Link](i+" \t\t "+dist[i]);
}

[Link] 165
void dijkstra(int graph[][], int src)
{

Boolean sptSet[] = new Boolean[V];

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


{
dist[i] = Integer.MAX_VALUE;
sptSet[i] = false;
}

dist[src] = 0;

for (int count = 0; count < V-1; count++)


{
int u = minDistance(dist, sptSet);

sptSet[u] = true;

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

if (!sptSet[v] && graph[u][v]!=0 &&


dist[u] != Integer.MAX_VALUE &&
dist[u]+graph[u][v] < dist[v])
dist[v] = dist[u] + graph[u][v];
}

printSolution(dist, V);
}

public static void main (String[] args)


{
int graph[][] = new int[][]{{0, 4, 0, 0, 0, 0, 0, 8, 0},
{4, 0, 8, 0, 0, 0, 0, 11, 0},
{0, 8, 0, 7, 0, 4, 0, 0, 2},
{0, 0, 7, 0, 9, 14, 0, 0, 0},
{0, 0, 0, 9, 0, 10, 0, 0, 0},
{0, 0, 4, 14, 10, 0, 2, 0, 0},
{0, 0, 0, 0, 0, 2, 0, 1, 6},
{8, 11, 0, 0, 0, 0, 1, 0, 7},
{0, 0, 2, 0, 0, 0, 6, 7, 0}
};
ShortestPath t = new ShortestPath();
[Link](graph, 0);
}
}

Le résultat attendu du programme est

Vertex Distance from Source


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

[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:

1. Un MST d'un graphe avec n sommets aura exactement n-1 arêtes.


2. Il existe un chemin unique de chaque sommet à chaque autre sommet.

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.

algorithm kruskalMST'(G: a graph)


sort G's edges by their value
MST = a forest of trees, initially each tree is a node in the graph
for each edge e in G:
if the root of the tree that [Link] belongs to is not the same
as the root of the tree that [Link] belongs to:
connect one of the roots to the other, thus merging two trees

return MST, which now a single-tree forest

Implémentation simple, basée sur des ensembles disjoints

La méthodologie forestière ci-dessus est en réalité une structure de données disjointe, qui
implique trois opérations principales:

subalgo makeSet(v: a node):


[Link] = v <- make a new tree rooted at v

subalgo findSet(v: a node):


if [Link] == v:
return v
return findSet([Link])

[Link] 168
subalgo unionSet(v, u: nodes):
vRoot = findSet(v)
uRoot = findSet(u)

[Link] = vRoot

algorithm kruskalMST''(G: a graph):


sort G's edges by their value
for each node n in G:
makeSet(n)
for each edge e in G:
if findSet([Link]) != findSet([Link]):
unionSet([Link], [Link])

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.

Implémentation optimale, basée sur des ensembles disjoints

Nous pouvons faire deux choses pour améliorer les sous-algorithmes simples et sous-optimaux
des ensembles disjoints:

1. Heuristique de compression de chemin : findSet ne doit jamais manipuler un arbre de


hauteur supérieure à 2 . S'il finit par itérer un tel arbre, il peut lier directement les nœuds
inférieurs à la racine, optimisant ainsi les traversées futures;

subalgo findSet(v: a node):


if [Link] != v
[Link] = findSet([Link])
return [Link]

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.

subalgo unionSet(u, v: nodes):


vRoot = findSet(v)
uRoot = findSet(u)

if vRoot == uRoot:
return

if [Link] < [Link]:


[Link] = uRoot
else if [Link] > [Link]:
[Link] = vRoot
else:
[Link] = vRoot
[Link] = [Link] + 1

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

La compression de chemin peut réduire la hauteur de l’arbre, par conséquent, la comparaison de


la hauteur des arbres au cours de l’union peut ne pas être une tâche triviale. Par conséquent, pour
éviter la complexité de stocker et de calculer la hauteur des arbres, le parent résultant peut être
choisi de manière aléatoire:

subalgo unionSet(u, v: nodes):


vRoot = findSet(v)
uRoot = findSet(u)

if vRoot == uRoot:
return
if random() % 2 == 0:
[Link] = uRoot
else:
[Link] = vRoot

En pratique, cet algorithme randomisé associé à la compression de chemin pour l'opération


findSet produira des performances comparables, mais beaucoup plus simples à mettre en œuvre.

Implémentation simple et de haut niveau

Triez les arêtes par valeur et ajoutez-en chacune au MST dans l'ordre trié, si cela ne crée pas de
cycle.

algorithm kruskalMST(G: a graph)


sort G's edges by their value
MST = an empty graph
for each edge e in G:
if adding e to MST does not create a cycle:
add e to MST

return MST

Lire L'algorithme de Kruskal en ligne: [Link]


kruskal

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

La prochaine étape est importante. À partir du nœud 1 , du nœud 2 , du nœud 5 et du nœud 4 ,


le bord minimal est de 2 à 4 . Mais si nous sélectionnons celui-là, cela créera un cycle dans notre
sous-graphe. Ceci est dû au fait que le noeud 2 et le noeud 4 sont déjà dans notre sous-graphe.
Donc, prendre l'avantage 2-4 ne nous profite pas. Nous allons sélectionner les arêtes de manière
à ajouter un nouveau noeud dans notre sous-graphe . Nous sélectionnons donc l'arête 4-8 .

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

Le pseudo-code de l'algorithme est donné ci-dessous:

Procedure PrimsMST(Graph): // here Graph is a non-empty connected weighted graph


Vnew[] = {x} // New subgraph Vnew with source node x
Enew[] = {}
while Vnew is not equal to V
u -> a node from Vnew
v -> a node that is not in Vnew such that edge u-v has the minimum cost
// if two nodes have same weight, pick any of them
add v to Vnew
add edge (u, v) to Enew
end while
Return Vnew and Enew

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

Le pseudo-code utilisant Priority Queue est indiqué ci-dessous:

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

Voici un programme simple en Java:

import [Link].*;

public class Graph


{
private static int infinite = 9999999;
int[][] LinkCost;
int NNodes;
Graph(int[][] mat)
{
int i, j;
NNodes = [Link];
LinkCost = new int[NNodes][NNodes];
for ( i=0; i < NNodes; i++)
{
for ( j=0; j < NNodes; j++)
{
LinkCost[i][j] = mat[i][j];
if ( LinkCost[i][j] == 0 )
LinkCost[i][j] = infinite;
}
}
for ( i=0; i < NNodes; i++)
{
for ( j=0; j < NNodes; j++)
if ( LinkCost[i][j] < infinite )
[Link]( " " + LinkCost[i][j] + " " );
else
[Link](" * " );
[Link]();
}
}
public int unReached(boolean[] r)
{
boolean done = true;
for ( int i = 0; i < [Link]; i++ )

[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]();
}
}

Compilez le code ci-dessus en utilisant javac [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

Lire L'algorithme de Prim en ligne: [Link]


prim

[Link] 178
Chapitre 33: La plus longue sous-séquence
commune
Examples
Plus longue explication de sous-séquence commune

L'une des implémentations les plus importantes de la programmation dynamique consiste à


trouver la sous- séquence commune la plus longue . Définissons d'abord certaines des
terminologies de base.

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.

Plus longue sous-séquence commune:

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.

Méthode de programmation dynamique:

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

La colonne 0-th représente la sous-séquence vide de s1 . De même, la 0ème ligne représente la


sous-séquence vide de s2 . Si nous prenons une sous-séquence vide d'une chaîne et essayons
de la faire correspondre à une autre chaîne, quelle que soit la longueur de la seconde sous-
chaîne, la sous-séquence commune aura une longueur de 0. Nous pouvons donc remplir les
lignes 0 et 0 et les colonnes 0 avec des 0. 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 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
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 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Donc, notre première formule sera:

if s2[i] is not equal to s1[j]


Table[i][j] = max(Table[i-1][j], Table[i][j-1]
endif

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 | | | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+

Donc, notre deuxième formule sera:

if s2[i] equals to s1[j]


Table[i][j] = Table[i-1][j-1] + 1
endif

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:

Procedure LCSlength(s1, s2):

[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]]

La complexité temporelle de cet algorithme est la suivante: O (mn) où m et n désignent la


longueur de chaque chaîne.

Comment pouvons-nous trouver la plus longue sous-séquence commune? Nous allons


commencer par le coin inférieur droit. Nous allons vérifier d'où vient la valeur. Si la valeur provient
de la diagonale, c'est-à-dire si la table [i-1] [j-1] est égale à la table [i] [j] - 1 , on appuie sur s2 [i]
ou s1 [j] sont les mêmes) et se déplacer en diagonale. Si la valeur vient du haut, cela signifie que
si la table [i-1] [j] est égale à la table [i] [j] , nous passons au sommet. Si la valeur vient de
gauche, cela signifie que si la table [i] [j-1] est égale à la table [i] [j] , nous nous déplaçons vers
la gauche. Lorsque nous atteignons la colonne la plus à gauche ou la plus haute, notre recherche
se termine. Ensuite, nous affichons les valeurs de la pile et les imprimons. Le pseudo-code:

Procedure PrintLCS(LCSlength, s1, s2)


temp := LCSlength
S = stack()
i := [Link]
j := [Link]
while i is not equal to 0 and j is not equal to 0
if Table[i-1][j-1] == Table[i][j] - 1 and s1[j]==s2[i]
[Link](s1[j]) //or [Link](s2[i])
i := i - 1
j := j - 1
else if Table[i-1][j] == Table[i][j]
i := i-1
else
j := j-1
endif
endwhile
while S is not empty
print([Link])
endwhile

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

Lire La plus longue sous-séquence commune en ligne:


[Link]

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

En mathématiques, le triangle de Pascal est un tableau triangulaire de coefficients binomiaux. Les


lignes du triangle de Pascal sont classiquement énumérées en commençant par la ligne n = 0 en
haut (la 0ème ligne). Les entrées de chaque ligne sont numérotées à partir de la gauche en
commençant par k = 0 et sont généralement décalées par rapport aux nombres figurant dans les
lignes adjacentes.

Le triangle est construit de la manière suivante:

• Dans la rangée supérieure, il y a une entrée non nulle unique 1.


• Chaque entrée de chaque ligne suivante est construite en ajoutant le nombre ci-dessus et à
gauche avec le nombre au-dessus et à droite, en traitant les entrées vides comme 0.

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.

Équation pour générer chaque entrée en triangle Pascal:

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.

Exemple de triangle de Pascal:

[Link] 185
Implémentation du triangle de Pascal en C #

public class PascalsTriangle


{
static void PascalTriangle(int n)
{
for (int line = 1; line <= n; line++)
{
int c = 1;
for (int i = 1; i <= line; i++)
{
[Link](c);
c = c * (line - i) / i;
}
[Link]("\n");
}
}

public static int Main(int input)


{
PascalTriangle(input);
return input;
}
}

Triangle de Pascal en C

int i, space, rows, k=0, count = 0, count1 = 0;


row=5;
for(i=1; i<=rows; ++i)
{
for(space=1; space <= rows-i; ++space)
{
printf(" ");
++count;
}

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

Lire Le triangle de Pascal en ligne: [Link]


pascal

[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

Input: str1 = "geek", str2 = "gesek"


Output: 1
We only need to insert s in first string

Input: str1 = "march", str2 = "cart"


Output: 3
We need to replace m with c and remove character c and then replace h with t

Pour résoudre ce problème, nous utiliserons un tableau 2D dp [n + 1] [m + 1] où n est la longueur


de la première chaîne et m la longueur de la deuxième chaîne. Pour notre exemple, si str1 est
azcef et str2 est abcdef, notre tableau sera dp [6] [7] et notre réponse finale sera stockée dans dp
[5] [6].

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
+---+---+---+---+---+---+---+
(a)| 1 | | | | | | |
+---+---+---+---+---+---+---+
(z)| 2 | | | | | | |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

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.

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | 1 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(c)| 3 | | | | | | |
+---+---+---+---+---+---+---+
(e)| 4 | | | | | | |
+---+---+---+---+---+---+---+
(f)| 5 | | | | | | |
+---+---+---+---+---+---+---+

Donc, notre formule ressemblera à

if characters are same


dp[i][j] = dp[i-1][j-1];
else
dp[i][j] = 1 + Min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

Après la dernière itération, notre tableau dp [] ressemblera à

(a) (b) (c) (d) (e) (f)


+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
+---+---+---+---+---+---+---+
(a)| 1 | 0 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(z)| 2 | 1 | 1 | 2 | 3 | 4 | 5 |
+---+---+---+---+---+---+---+
(c)| 3 | 2 | 2 | 1 | 2 | 3 | 4 |
+---+---+---+---+---+---+---+
(e)| 4 | 3 | 3 | 2 | 2 | 2 | 3 |

[Link] 189
+---+---+---+---+---+---+---+
(f)| 5 | 4 | 4 | 2 | 3 | 3 | 3 |
+---+---+---+---+---+---+---+

Implémentation en Java

public int getMinConversions(String str1, String str2){


int dp[][] = new int[[Link]()+1][[Link]()+1];
for(int i=0;i<=[Link]();i++){
for(int j=0;j<=[Link]();j++){
if(i==0)
dp[i][j] = j;
else if(j==0)
dp[i][j] = i;
else if([Link](i-1) == [Link](j-1))
dp[i][j] = dp[i-1][j-1];
else{
dp[i][j] = 1 + [Link](dp[i-1][j], [Link](dp[i][j-1], dp[i-1][j-1]));
}
}
}
return dp[[Link]()][[Link]()];
}

La complexité du temps

O(n^2)

Lire Modifier l'algorithme dynamique de distance en ligne:


[Link]

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

Qu'est-ce que ça veut dire ?

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 fonction suivante trouve l'élément maximal dans un tableau:

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


int max = INT_MIN;
for (size_t i = 0; i < len; i++) {
if (max < array[i]) {
max = array[i];
}
}
return max;
}

La taille d'entrée est la taille du tableau, que j'ai appelée len dans le code.

Comptons les opérations.

[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:

if (max < array[i])


i++;
max = array[i]

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!

Une boucle imbriquée

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

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


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

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 .

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


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

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

Appelons x le nombre d'opérations: on sait que 1 = n / (2 ^ x).

Donc 2 ^ x = n, alors x = log n

Conclusion
Face à des divisions successives (que ce soit par deux ou par un nombre quelconque), rappelez-
vous que la complexité est logarithmique.

O (log n) types d'algorithmes

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

Ainsi, à chaque étape, notre problème devient moitié.

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

1. Disons à la kième étape ou au nombre d'opérations:

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

4. Prenez le journal des deux côtés

log e n = k log e 2

ou

k = log e n / log e 2

5. Utilisation de log log x m / log x n = log n m

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:

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


{
// perform some operation
}

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 = log 2 2 8 (=> log a a = 1)

k=8

[Link] 196
Un autre très bon exemple pour un cas similaire est l' algorithme de recherche binaire .

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


int low=0;
int high=size-1;

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
}

Lire Notation Big-O en ligne: [Link]

[Link] 197
Chapitre 37: Plus longue sous-séquence
croissante
Examples
Information de base sur la sous-séquence croissante la plus longue

Le problème de la sous-séquence croissante la plus longue consiste à trouver la sous-séquence à


partir de la séquence d'entrée de la sous-séquence dans laquelle les éléments de la sous-
séquence sont classés par ordre croissant . Toutes les sous-séquences ne sont ni contiguës ni
uniques.

Application de la plus longue sous-séquence croissante:

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.

Forme simple de l'algorithme:

1. Trouvez des lignes uniques communes aux deux documents.


2. Prenez toutes ces lignes du premier document et commandez-les en fonction de leur
apparence dans le deuxième document.
3. Calculez le LIS de la séquence résultante (en effectuant un tri de patience ), en obtenant la
séquence de lignes correspondant le plus longtemps, une correspondance entre les lignes
de deux documents.
4. Reconstruire l'algorithme sur chaque plage de lignes entre celles déjà correspondantes.

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:

1. Décrivez un tableau de valeurs que nous voulons calculer.


Pour 1 <= i <= n , soit A (i) la longueur d'une plus longue séquence d'entrées croissante.
Notez que la longueur à laquelle nous sommes finalement intéressés est max{A(i)|1 ≤ i ≤ n}
.

[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

La complexité temporelle dans l'approche 1: O(n*2^n)

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], ∞)

Complexité temporelle en approche 2: O(n^2)

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])

Complexité temporelle en approche 3: O(n^2)

Algorithme itératif:

Calcule les valeurs itérativement de manière ascendante.

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

La complexité temporelle dans l'approche itérative: O(n^2)

Espace auxiliaire: O(n)

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 #

public class LongestIncreasingSubsequence


{
private static int Lis(int[] input, int n)
{
int[] lis = new int[n];

[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;
}

public static int Main(int[] input)


{
int n = [Link];
return Lis(input, n);
}
}

Lire Plus longue sous-séquence croissante en ligne:


[Link]

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

Espace auxiliaire: O(nw)


Complexité temporelle O(nw)

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

Pseudo-code pour problème de sac à dos

Donné:

1. Valeurs (tableau v)
2. Poids (tableau w)
3. Nombre d'éléments distincts (n)
4. Capacité (W)

for j from 0 to W do:


m[0, j] := 0
for i from 1 to n do:
for j from 0 to W do:
if w[i] > j then:
m[i, j] := m[i-1, j]
else:
m[i, j] := max(m[i-1, j], m[i-1, j-w[i]] + v[i])

Une implémentation simple du pseudo-code ci-dessus en utilisant Python:

def knapSack(W, wt, val, n):


K = [[0 for x in range(W+1)] for x in range(n+1)]
for i in range(n+1):
for w in range(W+1):
if i==0 or w==0:
K[i][w] = 0
elif wt[i-1] <= w:
K[i][w] = max(val[i-1] + K[i-1][w-wt[i-1]], K[i-1][w])
else:
K[i][w] = K[i-1][w]
return K[n][W]

[Link] 202
val = [60, 100, 120]
wt = [10, 20, 30]
W = 50
n = len(val)
print(knapSack(W, wt, val, n))

Exécution du code: Enregistrez-le dans un fichier nommé [Link]

$ 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 #

public class KnapsackProblem


{
private static int Knapsack(int w, int[] weight, int[] value, int n)
{
int i;
int[,] k = new int[n + 1, w + 1];
for (i = 0; i <= n; i++)
{
int b;
for (b = 0; b <= w; b++)
{
if (i==0 || b==0)
{
k[i, b] = 0;
}
else if (weight[i - 1] <= b)
{
k[i, b] = [Link](value[i - 1] + k[i - 1, b - weight[i - 1]], k[i - 1,
b]);
}
else
{
k[i, b] = k[i - 1, b];
}
}
}
return k[n, w];
}

public static int Main(int nItems, int[] weights, int[] values)


{
int n = [Link];
return Knapsack(nItems, weights, values, n);
}
}

Lire Problème de sac à dos en ligne: [Link]


sac-a-dos

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

Étant donné deux séquences X = <x1,...,xm> et Y = <y1,...,yn> , une séquence U = <u1,...,uk>


est une super séquence commune de X et Y si U est une super séquence de X et Y En d'autres
termes, une super-séquence commune la plus courte de chaînes x et y est la chaîne la plus courte
z telle que x et y soient des sous-séquences de z .

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.

Exemple de problème de supersquence commun le plus court:

[Link] 204
Complexité temporelle: O(max(m,n))

Implémentation du plus court problème de supersquence commune en C #

public class ShortestCommonSupersequence


{
private static int Max(int a, int b)
{
return a > b ? a : b;
}

private static int Lcs(string x, string y, int m, int n)


{
var l = new int[m + 1, n + 1];
for (var i = 0; i <= m; i++)
{
for (var j = 0; j <= n; j++)
{
if (i == 0 || j == 0) l[i, j] = 0;
else if (x[i - 1] == y[j - 1]) l[i, j] = l[i - 1, j - 1] + 1;
else l[i, j] = Max(l[i - 1, j], l[i, j - 1]);
}
}
return l[m, n];
}

private static int Scs(string x, string y)


{
int m = [Link], n = [Link];
int l = Lcs(x, y, m, n);
return m + n - l;
}

[Link] 205
public static int Main(string x, string y)
{
return Scs(x, y);
}
}

Lire Problème de supersquence commun le plus court en ligne:


[Link]

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

Une solution de programmation dynamique a deux exigences principales:

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.

Une solution de programmation dynamique a 2 composants principaux, l' état et la transition

L’ État fait référence à un sous-problème du problème initial.

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

0-1 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 :

int knapsack(vector<int> &value, vector<int> &weight, int N, int C){


int dp[C+1];
for (int i = 1; i <= C; ++i){
dp[i] = -100000000;
}
dp[0] = 0;
for (int i = 0; i < N; ++i){
for (int j = C; j >= weight[i]; --j){
dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
}
}
return dp[C];
}

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

Sac à dos sans limite

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.

Python (2.7.11) Exemple:


Mise en œuvre :

def unbounded_knapsack(w, v, c): # weight, value and capactiy


m = [0]

[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

La complexité de cette implémentation est O(nC) , n étant le nombre d'éléments.

Test :

w = [2, 3, 4, 5, 6]
v = [2, 4, 6, 8, 9]

print unbounded_knapsack(w, v, 13)

Sortie :

20

Cela signifie que la valeur maximale pouvant être atteinte est de 20, obtenue en choisissant (5, 8),
(5, 8) et (3, 4).

Algorithme de planification des travaux pondérés

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?

Celui-ci ressemble à la sélection d'activité à l'aide de l'algorithme gourmand, mais il y a une


torsion supplémentaire. Autrement dit, au lieu de maximiser le nombre d’emplois terminés, nous
mettons l’accent sur le profit maximum. Le nombre d'emplois effectués n'a pas d'importance ici.

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:

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

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 |
+-------------------------+---------+---------+---------+---------+---------+---------+

* Quelques étapes ont été sautées pour raccourcir le document.

Si nous parcourons le tableau Acc_Prof , nous pouvons trouver le profit maximum à 17 ! Le


pseudo-code:

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

La complexité de remplissage du tableau Acc_Prof est O (n 2 ). La traversée du tableau prend


O (n) . La complexité totale de cet algorithme est donc O (n 2 ).

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 à:

Procedure FindingPerformedJobs(Job, Acc_Prof, maxProfit):


S = stack()
for i -> n down to 0 and maxProfit > 0
if maxProfit is equal to Acc_Prof[i]
[Link](Job[i].name
maxProfit = maxProfit - Job[i].profit
endif
endfor

La complexité de cette procédure est la suivante: O (n) .

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

public class EditDistance {

public static void main(String[] args) {


// TODO Auto-generated method stub
String str1 = "march";
String str2 = "cart";

EditDistance ed = new EditDistance();


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

public int getMinConversions(String str1, String str2){


int dp[][] = new int[[Link]()+1][[Link]()+1];
for(int i=0;i<=[Link]();i++){
for(int j=0;j<=[Link]();j++){
if(i==0)
dp[i][j] = j;
else if(j==0)
dp[i][j] = i;
else if([Link](i-1) == [Link](j-1))
dp[i][j] = dp[i-1][j-1];

[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

La plus longue sous-séquence commune

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

public class LCS {

public static void main(String[] args) {


// TODO Auto-generated method stub
String str1 = "AGGTAB";
String str2 = "GXTXAYB";
LCS obj = new LCS();
[Link]([Link](str1, str2, [Link](), [Link]()));
[Link](obj.lcs2(str1, str2));
}

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

Approche ascendante pour imprimer le nième numéro de Fibonacci en utilisant la programmation


dynamique.

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)

Chevauchement des sous-problèmes

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

public int fib(int n){


int f[] = new int[n+1];
f[0]=0;f[1]=1;
for(int i=2;i<=n;i++){
f[i]=f[i-1]+f[i-2];
}
return f[n];
}

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

Entrée: X = "abcdxyz", y = "xyzabcd" Sortie: 4

La plus longue sous-chaîne commune est "abcd" et est de longueur 4.

Entrée: X = "zxabcdezy", y = "yzabcdezx" Sortie: 6

La plus longue sous-chaîne commune est "abcdez" et est de longueur 6.

Implémentation en Java

public int getLongestCommonSubstring(String str1,String str2){


int arr[][] = new int[[Link]()+1][[Link]()+1];
int max = Integer.MIN_VALUE;
for(int i=1;i<=[Link]();i++){
for(int j=1;j<=[Link]();j++){
if([Link](j-1) == [Link](i-1)){
arr[i][j] = arr[i-1][j-1]+1;
if(arr[i][j]>max)
max = arr[i][j];
}
else
arr[i][j] = 0;
}
}
return max;
}

La complexité du temps

O (m * n)

Lire Programmation dynamique en ligne:


[Link]

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

C'est pourquoi ces exemples peuvent s'avérer utiles

Examples
Affectations variables

Vous pouvez décrire l'affectation variable de différentes manières.

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

function incr (n)


return n + 1

sont tous assez clairs, vous pouvez donc les utiliser. Essayez de ne pas être ambigu avec une
affectation variable

Lire Pseudocode en ligne: [Link]

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

Le psuedocode suivant illustre l'utilisation de la recherche binaire:

high = 100000000000000000 <- Upper bound of search space


low = 1 <- Lower bound of search space
while high - low > 1
mid = (high + low) / 2 <- Take the middle value
supply = getSupply(mid)
demand = getDemand(mid)
if supply > demand
high = mid <- Solution is in lower half of search space
else if demand > supply
low = mid <- Solution is in upper half of search space
else <- supply==demand condition
return mid <- Found solution

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] ).

C Implémentation de la recherche binaire avec récursivité

int binsearch(int a[], int x, int low, int high) {


int mid;

if (low > high)


return -1;

mid = (low + high) / 2;

if (x == a[mid]) {
return (mid);
} else
if (x < a[mid]) {
binsearch(a, x, low, mid - 1);
} else {
binsearch(a, x, mid + 1, high);
}
}

Recherche binaire: sur des numéros triés

Il est plus facile de montrer une recherche binaire sur des nombres en utilisant un pseudo-code

int array[1000] = { sorted list of numbers };


int N = 100; // number of entries in search space;
int high, low, mid; // our temporaries

[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:

while(low < high)


{
mid = low + ((high - low) / 2);
if(array[mid] < x || (array[mid] == x && array[mid + 1] == x))
low = mid + 1;
else
high = mid;
}

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.

Voici une implémentation Python:

[Link] 221
def linear_search(searchable_list, query):
for x in searchable_list:
if query == x:
return True
return False

linear_search(['apple', 'banana', 'carrot', 'fig', 'garlic'], 'fig') #returns True

Rabin Karp

L'algorithme de Rabin – Karp ou l'algorithme de Karp – Rabin est un algorithme de recherche de


chaîne qui utilise le hachage pour trouver l'un quelconque d'un ensemble de chaînes de motif
dans un texte. Son temps moyen et meilleur est O (n + m) dans l'espace O ( p), mais le pire des
cas est O (nm) où n est la longueur du texte et m la longueur du motif.

Implémentation d'algorithme en Java pour la correspondance de chaîne

void RabinfindPattern(String text,String pattern){


/*
q a prime number
p hash value for pattern
t hash value for text
d is the number of unique characters in input alphabet
*/
int d=128;
int q=100;
int n=[Link]();
int m=[Link]();
int t=0,p=0;
int h=1;
int i,j;
//hash value calculating function
for (i=0;i<m-1;i++)
h = (h*d)%q;
for (i=0;i<m;i++){
p = (d*p + [Link](i))%q;
t = (d*t + [Link](i))%q;
}
//search for the pattern
for(i=0;i<end-m;i++){
if(p==t){
//if the hash value matches match them character by character
for(j=0;j<m;j++)
if([Link](j+i)!=[Link](j))
break;
if(j==m && i>=start)
[Link]("Pattern match found at index "+i);
}
if(i<end-m){
t =(d*(t - [Link](i)*h) + [Link](i+m))%q;
if(t<0)
t=t+q;
}
}
}

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.

t = (d * ([Link] (i) * h) + [Link] (i + m))% q;

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.

Analyse de la recherche linéaire (pires, moyens et meilleurs cas)

Nous pouvons avoir trois cas pour analyser un algorithme:

1. Pire cas

2. Cas moyen

3. Meilleur cas

#include <stdio.h>

// Linearly search x in arr[]. If x is present then return the index,

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

/ * Programme pilote pour tester les fonctions ci-dessus * /

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;
}

Pire analyse de cas (habituellement faite)

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)

Analyse de cas moyenne (parfois effectuée)

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.

Meilleure analyse de cas (Bogus)

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.

Lire Recherche en ligne: [Link]

[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:

txt[] = "THIS IS A TEST TEXT"


pat[] = "TEST"

sortie:

Pattern found at index 10

Contribution:

txt[] = "AABAACAADAABAAABAA"
pat[] = "AABA"

sortie:

Pattern found at index 0


Pattern found at index 9
Pattern found at index 13

Implémentation du langage C:

// C program for implementation of KMP pattern searching


// algorithm
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

void computeLPSArray(char *pat, int M, int *lps);

void KMPSearch(char *pat, char *txt)


{
int M = strlen(pat);
int N = strlen(txt);

// create lps[] that will hold the longest prefix suffix


// values for pattern
int *lps = (int *)malloc(sizeof(int)*M);
int j = 0; // index for pat[]

[Link] 226
// Preprocess the pattern (calculate lps[] array)
computeLPSArray(pat, M, lps);

int i = 0; // index for txt[]


while (i < N)
{
if (pat[j] == txt[i])
{
j++;
i++;
}

if (j == M)
{
printf("Found pattern at index %d \n", i-j);
j = lps[j-1];
}

// mismatch after j matches


else if (i < N && pat[j] != txt[i])
{
// Do not match lps[0..lps[j-1]] characters,
// they will match anyway
if (j != 0)
j = lps[j-1];
else
i = i+1;
}
}
free(lps); // to avoid memory leak
}

void computeLPSArray(char *pat, int M, int *lps)


{
int len = 0; // length of the previous longest prefix suffix
int i;

lps[0] = 0; // lps[0] is always 0


i = 1;

// the loop calculates lps[i] for i = 1 to M-1


while (i < M)
{
if (pat[i] == pat[len])
{
len++;
lps[i] = len;
i++;
}
else // (pat[i] != pat[len])
{
if (len != 0)
{
// This is tricky. Consider the example
// AAACAAAA and i = 7.
len = lps[len-1];

// Also, note that we do not increment i here


}
else // if (len == 0)

[Link] 227
{
lps[i] = 0;
i++;
}
}
}
}

// Driver program to test above function


int main()
{
char *txt = "ABABDABACDABABCABAB";
char *pat = "ABABCABAB";
KMPSearch(pat, txt);
return 0;
}

Sortie:

Found pattern at index 10

Référence:

[Link]

Introduction à l'algorithme de Rabin-Karp

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:

a -> 1 g -> 7 m -> 13 s -> 19 y -> 25

[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

La valeur de hachage de nsa sera:

14 X 11⁰ + 19 X 11¹ + 1 X 11² = 344

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:

25 X 11⁰ + 5 X 11¹ + 13 X 11² = 1653

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.

• Nous soustrayons la valeur de la première lettre de la chaîne précédente de notre valeur


de hachage actuelle. Dans ce cas, y . Nous obtenons, 1653 - 25 = 1628 .
• Nous divisons la différence avec notre nombre premier , qui est 11 pour cet exemple. Nous
obtenons, 1628 / 11 = 148 .
• Nous ajoutons la nouvelle lettre X (premier) ⁻¹ , où m est la longueur du motif, avec le
quotient qui est i = 9 . Nous obtenons, 148 + 9 X 11² = 1237 .

La nouvelle valeur de hachage n'est pas égale à la valeur de hachage de nos modèles. Sur la
route , pour n nous obtenons:

Previous String: emi


First Letter of Previous String: e(5)
New Letter: n(14)
New String: "min"
1237 - 5 = 1232
1232 / 11 = 112
112 + 14 X 11² = 1806

Ça ne correspond pas Après cela, pour s , nous obtenons:

Previous String: min


First Letter of Previous String: m(13)
New Letter: s(19)
New String: "ins"
1806 - 13 = 1793
1793 / 11 = 163
163 + 19 X 11² = 2462

Ça ne correspond pas Ensuite, pour un , 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:

Procedure Calculate-Hash(String, Prime, x):


hash := 0 // Here x denotes the length to be considered
for m from 1 to x // to find the hash value
hash := hash + (Value of String[m]) ⁻¹
end for
Return hash

Recalcul de hachage:

Procedure Recalculate-Hash(String, Curr, Prime, Hash):


Hash := Hash - Value of String[Curr] //here Curr denotes First Letter of Previous String
Hash := Hash / Prime
m := [Link]
New := Curr + m - 1
Hash := Hash + (Value of String[New]) ⁻¹
Return Hash

Match de cordes:

Procedure String-Match(Text, Pattern, m):


for i from m to Pattern-length + m - 1
if Text[i] is not equal to Pattern[i]
Return false
end if
end for
Return true

Rabin-Karp:

Procedure Rabin-Karp(Text, Pattern, Prime):


m := [Link]
HashValue := Calculate-Hash(Pattern, Prime, m)
CurrValue := Calculate-Hash(Pattern, Prime, m)
for i from 1 to [Link] - m
if HashValue == CurrValue and String-Match(Text, Pattern, i) is true
Return i
end if
CurrValue := Recalculate-Hash(String, i+1, Prime, CurrValue)
end for

[Link] 230
Return -1

Si l'algorithme ne trouve aucune correspondance, il renvoie simplement -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.

Pour le texte des motifs de longueur n et p de longueur combinée m , sa durée de fonctionnement


moyenne et meilleure est O (n + m) dans l'espace O (p) , mais sa pire période est O (nm) .

Introduction à l'algorithme de Knuth-Morris-Pratt (KMP)

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.

Faisons maintenant une recherche de sous-chaîne à l'aide de l'exemple suivant:

+---------+---+---+---+---+---+---+---+---+---+---+---+---+
| 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 à:

Procedure KMP(Text, Pattern)


GenerateSuffixArray(Pattern)
m := [Link]
n := [Link]
i := 0
j := 0
while i is less than m
if Pattern[j] is equal to Text[i]
j := j + 1
i := i + 1
if j is equal to n
Return (j-i)
else if i < m and Pattern[j] is not equal t Text[i]
if j is not equal to 0
j = S[j-1]
else
i := i + 1
end if
end if
end while
Return -1

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.

Python Implémentation de l'algorithme KMP.

Botte de foin : Chaîne dans laquelle le modèle doit être recherché.


Aiguille : Le motif à rechercher.

Complexité du temps: partie de la recherche (méthode de strstr) a la complexité O (n) , où n est


la longueur de la meule mais est nécessaire que l' aiguille est pré analysé également pour la
construction de tableau de préfixes O (m) pour la construction de tableau de préfixes où m est la
longueur de l'aiguille.
Par conséquent, la complexité temporelle globale de KMP est O (n + m)

[Link] 235
Complexité de l'espace : O (m) à cause du tableau de préfixes sur l'aiguille.

Note: La suite de l'implémentation retourne la position de départ de la correspondance dans une


botte de foin (s'il y a une correspondance) sinon renvoie -1, pour les cas limites comme si l'aiguille
/ haystack est une chaîne vide ou une 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

def strstr(haystack, needle):


# m: denoting the position within S where the prospective match for W begins
# i: denoting the index of the currently considered character in W.
haystack_len = len(haystack)
needle_len = len(needle)
if (needle_len > haystack_len) or (not haystack_len) or (not needle_len):
return -1
prefix_table = get_prefix_table(needle)
m = i = 0
while((i<needle_len) and (m<haystack_len)):
if haystack[m] == needle[i]:
i += 1
m += 1
else:
if i != 0:
i = prefix_table[i-1]
else:
m += 1
if i==needle_len and haystack[m-1] == needle[i-1]:
return m - needle_len
else:
return -1

if __name__ == '__main__':
needle = 'abcaby'
haystack = 'abxabcabcaby'
print strstr(haystack, needle)

Lire Recherche de sous-chaîne en ligne: [Link]


de-sous-chaine

[Link] 236
Chapitre 44: Recherche en largeur
Examples
Recherche du chemin le plus court de la source aux autres nœuds

Breadth-first-search (BFS) est un algorithme permettant de parcourir ou de rechercher des


structures de données arborescentes ou graphiques. Il commence à la racine de l'arbre (ou un
nœud arbitraire d'un graphique, parfois appelé «clé de recherche») et explore les nœuds voisins
en premier, avant de passer aux voisins de niveau supérieur. BFS a été inventé à la fin des
années 1950 par Edward Forrest Moore , qui l'a utilisé pour trouver le chemin le plus court d'un
labyrinthe et a été découvert indépendamment par CY Lee en tant qu'algorithme de routage en
1961.

Les processus de l'algorithme BFS fonctionnent sous ces hypothèses:

1. Nous ne traverserons aucun noeud plus d'une fois.


2. Le noeud source ou le noeud à partir duquel nous nous trouvons est situé dans le niveau 0.
3. Les nœuds que nous pouvons atteindre directement depuis le nœud source sont des nœuds
de niveau 1, les nœuds que nous pouvons atteindre directement à partir des nœuds de
niveau 1 sont des nœuds de niveau 2 et ainsi de suite.
4. Le niveau indique la distance du plus court chemin de la source.

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.

La simulation de notre exemple:

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:

Procedure BFS(Graph, source):


Q = queue();
level[] = infinity
level[source] := 0
[Link](source)
while Q is not empty
u -> [Link]()
for all edges from u to v in Adjacency list
if level[v] == infinity
level[v] := level[u] + 1
[Link](v)
end if
end for
end while
Return level

En parcourant le tableau de niveaux , nous pouvons déterminer la distance de chaque nœud à


partir de la source. Par exemple: la distance entre le nœud 10 et la source sera stockée dans le
niveau [10] .

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:

Procedure PrintPath(u): //recursive | Procedure PrintPath(u): //iterative


if parent[u] is not equal to null | S = Stack()
PrintPath(parent[u]) | while parent[u] is not equal to null
end if | [Link](u)
print -> u | u := parent[u]
| end while
| while S is not empty
| print -> [Link]
| end while

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.

Trouver le plus court chemin de la source dans un graphique 2D

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 |
+----+-----+-----+-----+-----+

Ici, dx représente le déplacement en abscisse et dy représente le déplacement en ordonnée.


Encore une fois, cette partie est facultative. Vous pouvez également écrire toutes les
combinaisons possibles séparément. Mais il est plus facile de le gérer en utilisant un tableau de
direction. Il peut y avoir des combinaisons plus nombreuses, voire différentes, pour les
mouvements en diagonale ou les mouvements de chevalier.

[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é.

Notre pseudo-code sera:

Procedure BFS2D(Graph, blocksign, row, column):


for i from 1 to row
for j from 1 to column
visited[i][j] := false
end for
end for
visited[source.x][source.y] := true
level[source.x][source.y] := 0
Q = queue()
[Link](source)
m := [Link]
while Q is not empty
top := [Link]
for i from 1 to m
temp.x := top.x + dx[i]
temp.y := top.y + dy[i]
if temp is inside the row and column and top doesn't equal to blocksign
visited[temp.x][temp.y] := true
level[temp.x][temp.y] := level[top.x][top.y] + 1
[Link](temp)
end if
end for
end while
Return level

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 .

Composants connectés de graphique non dirigé à l'aide de BFS.

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.

Voici un graphique connecté .

[Link] 245
Le graphique suivant n'est pas connecté et comporte 2 composants connectés:

1. Composant connecté 1: {a, b, c, d, e}


2. Composant connecté 2: {f}

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

PseudoCode pour l'algorithme.

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;
};

typedef struct node Node;


typedef struct node *Nodeptr;

Nodeptr Qfront = NULL;


Nodeptr Qrear = NULL;
char *visited;//array that keeps track of visited vertices.

int main()
{
int n,e;//n is number of vertices, e is number of edges.
int i,j;
char **graph;//adjacency matrix

printf("Enter number of vertices:");


scanf("%d",&n);

if(n < 0 || n > MAXVERTICES)


{
fprintf(stderr, "Please enter a valid positive integer from 1 to %d",MAXVERTICES);
return -1;
}

graph = malloc(n * sizeof(char *));


visited = malloc(n*sizeof(char));

for(i = 0;i < n;++i)


{
graph[i] = malloc(n*sizeof(int));
visited[i] = 'N';//initially all vertices are not visited.
for(j = 0;j < n;++j)
graph[i][j] = 0;
}

printf("enter number of edges and then enter them in pairs:");


scanf("%d",&e);

for(i = 0;i < e;++i)


{
int u,v;
scanf("%d%d",&u,&v);
graph[u-1][v-1] = 1;
graph[v-1][u-1] = 1;

[Link] 247
}

if(isConnected(graph,n))
printf("The graph is connected");
else printf("The graph is NOT connected\n");
}

void enqueue(int vertex)


{
if(Qfront == NULL)
{
Qfront = malloc(sizeof(Node));
Qfront->v = vertex;
Qfront->next = NULL;
Qrear = Qfront;
}
else
{
Nodeptr newNode = malloc(sizeof(Node));
newNode->v = vertex;
newNode->next = NULL;
Qrear->next = newNode;
Qrear = newNode;
}
}

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;
}
}

int isConnected(char **graph,int noOfVertices)


{
int i;

//let random source vertex be vertex 0;


BFS(graph,0,noOfVertices);

for(i = 0;i < noOfVertices;++i)


if(visited[i] == 'N')
return 0;//0 implies false;

[Link] 248
return 1;//1 implies true;
}

void BFS(char **graph,int v,int noOfVertices)


{
int i,vertex;
visited[v] = 'Y';
enqueue(v);
while((vertex = deque()) != -1)
{
for(i = 0;i < noOfVertices;++i)
if(graph[vertex][i] == 1 && visited[i] == 'N')
{
enqueue(i);
visited[i] = 'Y';
}
}
}

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.

Les lignes à ajouter sont:

printf("\nConnected component %d\n",++count);


//count is a global variable initialized to 0
//add this as first line to BFS function

ET

printf("%d ",vertex+1);
add this as first line of while loop in BFS

et nous définissons la fonction suivante:

void listConnectedComponents(char **graph,int noOfVertices)


{
int i;
for(i = 0;i < noOfVertices;++i)
{
if(visited[i] == 'N')
BFS(graph,i,noOfVertices);

}
}

Lire Recherche en largeur en ligne: [Link]


largeur

[Link] 249
Chapitre 45: Résolution d'équations
Examples
Équation linéaire

Il existe deux classes de méthodes pour résoudre les équations linéaires:

1. Méthodes directes : Les méthodes courantes ont pour caractéristiques communes de


transformer l'équation originale en équations équivalentes qui peuvent être résolues plus
facilement, ce qui signifie que nous obtenons une résolution directement à partir d'une
équation.

2. Méthode itérative : Méthodes itératives ou indirectes: commencez par deviner la solution,


puis affinez la solution jusqu'à ce qu'un certain critère de convergence soit atteint. Les
méthodes itératives sont généralement moins efficaces que les méthodes directes car un
grand nombre d'opérations sont nécessaires. Exemple - Méthode d'itération de Jacobi,
méthode d'itération de Gauss-Seidal.

Mise en œuvre en C-

//Implementation of Jacobi's Method


void JacobisMethod(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;
while(!rootFound){
for(i=0; i<n; i++){ //calculation
Nx[i]=b[i];

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


if(i!=j) Nx[i] = Nx[i]-a[i][j]*x[j];
}
Nx[i] = Nx[i] / a[i][i];
}

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;
}
}

for(i=0; i<n; i++){ //evaluation


x[i]=Nx[i];
}
}

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];

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


if(i!=j) Nx[i] = Nx[i]-a[i][j]*Nx[j];
}
Nx[i] = Nx[i] / a[i][i];
}

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;
}
}

for(i=0; i<n; i++){ //evaluation


x[i]=Nx[i];
}
}

return ;
}

//Print array with comma separation


void print(int n, double x[n]){
int i;
for(i=0; i<n; i++){
printf("%lf, ", x[i]);
}
printf("\n\n");

return ;
}

int main(){
//equation initialization
int n=3; //number of variables

double x[n]; //variables

double b[n], //constants


a[n][n]; //arguments

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

for(i=0; i<n; i++){ //initialization


x[i]=0;
}
JacobisMethod(n, x, b, a);
print(n, x);

for(i=0; i<n; i++){ //initialization


x[i]=0;
}
GaussSeidalMethod(n, x, b, a);
print(n, x);

return 0;
}

Équation non linéaire

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-

/// Here define different functions to work with


#define f(x) ( ((x)*(x)*(x)) - (x) - 2 )
#define f2(x) ( (3*(x)*(x)) - 1 )
#define g(x) ( cbrt( (x) + 2 ) )

/**
* Takes two initial values and shortens the distance by both side.
**/
double BisectionMethod(){
double root=0;

double a=1, b=2;


double c=0;

[Link] 252
int loopCounter=0;
if(f(a)*f(b) < 0){
while(1){
loopCounter++;
c=(a+b)/2;

if(f(c)<0.00001 && f(c)>-0.00001){


root=c;
break;
}

if((f(a))*(f(c)) < 0){


b=c;
}else{
a=c;
}

}
}
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;

double a=1, b=2;


double c=0;

int loopCounter=0;
if(f(a)*f(b) < 0){
while(1){
loopCounter++;

c=(a*f(b) - b*f(a)) / (f(b) - f(a));

/*/printf("%lf\t %lf \n", c, f(c));/**////test


if(f(c)<0.00001 && f(c)>-0.00001){
root=c;
break;
}

if((f(a))*(f(c)) < 0){


b=c;
}else{
a=c;
}
}
}
printf("It took %d loops.\n", 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

if(f(x2)<0.00001 && f(x2)>-0.00001){


root=x2;
break;
}

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++;

if( (x-g(x)) <0.00001 && (x-g(x)) >-0.00001){


root = x;
break;
}

/*/printf("%lf \t %lf \n", g(x), x-(g(x)));/**////test

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++;

/*/printf("%lf \t %lf \t %lf \n", x0, x1, f(x1));/**////test

if(f(x1)<0.00001 && f(x1)>-0.00001){


root=x1;
break;
}

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;
}

Lire Résolution d'équations en ligne: [Link]


equations

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

Pseudo-code pour le tri par godet

1. Soit n la longueur de la liste d'entrée L;


2. Pour chaque élément i de L
3. Si B [i] n'est pas vide
4. Mettez A [i] dans B [i];
5. Sinon B [i]: = A [i]
6. retourner Concat B [i .. n] dans une liste triée;

Exemple de type de seau:

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;

for (int i = [Link] - 1; i >= 1; i--)


{
if (input[i] > maxValue) maxValue = input[i];
if (input[i] < minValue) minValue = input[i];
}

List<int>[] bucket = new List<int>[maxValue - minValue + 1];

for (int i = [Link] - 1; i >= 0; i--)


{
bucket[i] = new List<int>();
}

foreach (int i in input)


{
bucket[i - minValue].Add(i);
}

foreach (List<int> b in bucket)


{
if ([Link] > 0)
{
foreach (int t in b)
{
input[k] = t;
k++;
}
}
}
}

public static int[] Main(int[] input)


{
SortBucket(ref input);
return input;
}
}

Lire Seau Sort en ligne: [Link]

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

Pseudo-code pour le tri de sélection:

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]

Visualisation du tri de sélection:

[Link] 258
Exemple de tri de sélection:

[Link] 259
Espace auxiliaire: O(n)
Complexité temporelle: O(n^2)

Implémentation du tri par sélection en C #

J'ai utilisé le langage C # pour implémenter un algorithme de tri de sélection.

public class SelectionSort


{
private static void SortSelection(int[] input, int n)
{
for (int i = 0; i < n - 1; i++)
{
var minId = i;
int j;
for (j = i + 1; j < n; j++)
{
if (input[j] < input[minId]) minId = j;
}
var temp = input[minId];
input[minId] = input[i];
input[i] = temp;
}
}

public static int[] Main(int[] input)


{
SortSelection(input, [Link]);
return input;
}
}

Mise en œuvre de l'élixir

defmodule Selection do

def sort(list) when is_list(list) do


do_selection(list, [])
end

def do_selection([head|[]], acc) do


acc ++ [head]
end

def do_selection(list, acc) do


min = min(list)
do_selection(:[Link](min, list), acc ++ [min])
end

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]

Lire Sélection Tri en ligne: [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).

2. Répétez l'étape 1 jusqu'à ce que la pile soit commandée.

3. Ça y est, un algorithme en deux étapes fonctionnera.

Exemple d'algorithme de tri Pancake:

[Link] 262
Espace auxiliaire: O(1)
Complexité temporelle: O(n^2)

Implémentation C #

public class PancakeSort


{
private static void SortPancake(int[] input, int n)
{
for (var bottom = n - 1; bottom > 0; bottom--)
{
var index = bottom;
var maxIndex = input[bottom];
int i;
for (i = bottom - 1; i >= 0; i--)
{
if (input[i] > maxIndex)
{
maxIndex = input[i];
index = i;
}
}
if (index == bottom) continue;
var temp = new int[n];
var j = 0;
for (i = bottom; i > index; i--,j++)
{
temp[j] = input[i];
}
for (i = 0; i < index; i++, j++)
{
temp[j] = input[i];
}
if ([Link] > j) temp[j] = input[index];
for (i = 0; i <= bottom; i++)
{
input[i] = temp[i];
}
}
}

public static int[] Main(int[] input)


{
SortPancake(input, [Link]);
return input;
}
}

Lire Sort de crêpes en ligne: [Link]

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

Pseudo code pour Odd-Even Sort:

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]

Wikipedia a la meilleure illustration du genre Odd-Even:

Exemple de tri impair:

[Link] 264
La mise en oeuvre:

J'ai utilisé le langage C # pour implémenter l'algorithme de tri Odd-Even.

public class OddEvenSort


{
private static void SortOddEven(int[] input, int n)
{
var sort = false;

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;
}
}
}

public static int[] Main(int[] input)


{
SortOddEven(input, [Link]);
return input;
}
}

Espace auxiliaire: O(n)


Complexité temporelle: O(n)

Lire Sort étrange en ligne: [Link]

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

Exemple de code (C / C ++)

#include <math.h>

#define PI 3.1415926535897932384626433832795 // PI for sine/cos calculations


#define TWOPI 6.283185307179586476925286766559 // 2*PI for sine/cos calculations
#define Deg2Rad 0.017453292519943295769236907684886 // Degrees to Radians factor
#define Rad2Deg 57.295779513082320876798154814105 // Radians to Degrees factor
#define log10_2 0.30102999566398119521373889472449 // Log10 of 2
#define log10_2_INV 3.3219280948873623478703194294948 // 1/Log10(2)

// complex variable structure (double precision)


struct complex
{
public:
double Re, Im; // Not so complicated after all
};

void rad2InverseFFT(int N, complex *x, complex *DFT)


{
// M is number of stages to perform. 2^M = N
double Mx = (log10((double)N) / log10((double)2));
int a = (int)(ceil(pow(2.0, Mx)));
int status = 0;
if (a != N) // Check N is a power of 2
{
x = 0;
DFT = 0;
throw "rad2InverseFFT(): N must be a power of 2 for Radix 2 Inverse FFT";
}

complex *pDFT = DFT; // Reset vector for DFT pointers


complex *pX = x; // Reset vector for x[n] pointer
double NN = 1 / (double)N; // Scaling factor for the inverse FFT

for (int i = 0; i < N; i++, DFT++)


DFT->Im *= -1; // Find the complex conjugate of the Frequency Spectrum

DFT = pDFT; // Reset Freq Domain Pointer


rad2FFT(N, DFT, x); // Calculate the forward FFT with variables switched (time & freq)

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
}
}

Lire Transformée de Fourier Rapide en ligne:


[Link]

[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

Considérons l'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.

La traversée de pré-commande de l'arbre ci-dessus sera donc:

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.

Ainsi, la traversée dans l'ordre de l'arbre ci-dessus sera:

4251637

La traversée post-commande (root) traverse le sous-arbre gauche du nœud puis le sous-arbre


droit puis le nœud.

Ainsi, la traversée post-commande de l'arbre ci-dessus sera:

4526731

Traversée de niveau - implémentation

Par exemple si l'arbre donné est:

[Link] 274
La traversée de l'ordre des niveaux sera

1234567

Impression des données du noeud par niveau.

Code:

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

using namespace std;

struct node{

int data;
node *left;
node *right;
};

void levelOrder(struct node *root){

if(root == NULL) return;

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(){

struct node *root = newNode(1);


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

printf("Level Order traversal of binary tree is \n");


levelOrder(root);

return 0;

La structure de données de la file d'attente est utilisée pour atteindre l'objectif ci-dessus.

Lire Traversées d'arbres binaires en ligne:


[Link]

[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é.

void dfs(int node, vector<vector<int>>* graph, vector<bool>* visited) {


// check whether node has been visited before
if((*visited)[node])
return;

// set as visited to avoid visiting the same node twice


(*visited)[node] = true;

// perform some action here


cout << node;

// traverse to the adjacent nodes in depth-first manner


for(int i = 0; i < (*graph)[node].size(); ++i)
dfs((*graph)[node][i], graph, visited);
}

Lire Traversées graphiques en ligne: [Link]


graphiques

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

Un algorithme de tri est en place s'il trie en utilisant uniquement la


En place
mémoire auxiliaire O(1) (sans compter le tableau à trier).

Un algorithme de tri a la meilleure complexité temporelle en O(T(n)) si


Meilleure
son temps d'exécution est au moins T(n) pour toutes les entrées
complexité de cas
possibles.

Un algorithme de tri a une complexité moyenne en temps de cas de


Complexité
O(T(n)) si son temps d'exécution, moyenné sur toutes les entrées
moyenne des cas
possibles , est T(n) .

Un algorithme de tri a la complexité temporelle la plus défavorable de


La pire complexité
O(T(n)) si son temps d'exécution est au maximum T(n) .

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.

Considérons une liste de paires:

(1, 2) (9, 7) (3, 4) (8, 6) (9, 3)

Maintenant, nous allons trier la liste en utilisant le premier élément de chaque paire.

Un tri stable de cette liste affichera la liste ci-dessous:

(1, 2) (3, 4) (8, 6) (9, 7) (9, 3)

Car (9, 3) apparaît également après (9, 7) dans la liste originale.

[Link] 278
Un tri instable affichera la liste ci-dessous:

(1, 2) (3, 4) (8, 6) (9, 3) (9, 7)

Le tri instable peut générer le même résultat que le tri stable, mais pas toujours.

Sortes d'écuries bien connues:

• Tri par fusion


• Tri par insertion
• Tri par radix
• Tri tri
• Tri à bulles

Sortes instables bien connues:

• Tri de tas
• Tri rapide

Lire Tri en ligne: [Link]

[Link] 279
Chapitre 54: Tri à bulles
Paramètres

Paramètre La description

Stable Oui

En place Oui

Meilleure complexité de cas Sur)

Complexité moyenne des cas O (n ^ 2)

La pire complexité O (n ^ 2)

Complexité de l'espace O (1)

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.

Exemple de tri à bulles

[Link] 281
Implémentation de Bubble Sort
J'ai utilisé le langage C # pour implémenter un algorithme de tri par bulles

public class BubbleSort


{
public static void SortBubble(int[] input)
{
for (var i = [Link] - 1; i >= 0; i--)
{
for (var j = [Link] - 1 - 1; j >= 0; j--)
{
if (input[j] <= input[j + 1]) continue;
var temp = input[j + 1];
input[j + 1] = input[j];
input[j] = temp;
}
}
}

public static int[] Main(int[] input)


{
SortBubble(input);
return input;
}
}

Implémentation en C & C ++

Un exemple d'implémentation de BubbleSort en 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

void bubble_sort(long list[], long n)


{
long c, d, t;

for (c = 0 ; c < ( n - 1 ); c++)


{
for (d = 0 ; d < n - c - 1; d++)
{
if (list[d] > list[d+1])
{
/* Swapping */

t = list[d];
list[d] = list[d+1];
list[d+1] = t;
}
}
}
}

Tri à bulles avec pointeur

void pointer_bubble_sort(long * list, long n)


{
long c, d, t;

for (c = 0 ; c < ( n - 1 ); c++)


{
for (d = 0 ; d < n - c - 1; d++)
{
if ( * (list + d ) > *(list+d+1))
{
/* Swapping */

t = * (list + d );
* (list + d ) = * (list + d + 1 );
* (list + d + 1) = t;
}
}
}
}

Implémentation en Java

public class MyBubbleSort {

public static void bubble_srt(int array[]) {//main logic


int n = [Link];
int k;
for (int m = n; m >= 0; m--) {
for (int i = 0; i < n - 1; i++) {
k = i + 1;
if (array[i] > array[k]) {
swapNumbers(i, k, array);
}

[Link] 283
}
printNumbers(array);
}
}

private static void swapNumbers(int i, int j, int[] array) {

int temp;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}

private static void printNumbers(int[] input) {

for (int i = 0; i < [Link]; i++) {


[Link](input[i] + ", ");
}
[Link]("\n");
}

public static void main(String[] args) {


int[] input = { 4, 2, 9, 6, 23, 12, 34, 0, 1 };
bubble_srt(input);

}
}

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

Lire Tri à bulles en ligne: [Link]

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

L'idée du tri de Shell est la suivante:

1. Organiser la séquence de données dans un tableau à deux dimensions


2. Trier les colonnes du tableau

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.

Exemple de tri Shell:

[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 #

public class ShellSort


{
static void SortShell(int[] input, int n)
{
var inc = 3;
while (inc > 0)
{
int i;
for (i = 0; i < n; i++)
{
var j = i;
var temp = input[i];
while ((j >= inc) && (input[j - inc] > temp))
{
input[j] = input[j - inc];
j = j - inc;
}
input[j] = temp;
}
if (inc / 2 != 0)
inc = inc / 2;
else if (inc == 1)
inc = 0;
else
inc = 1;
}
}

public static int[] Main(int[] input)


{
SortShell(input, [Link]);
return input;
}
}

Lire Tri de coquille en ligne: [Link]

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

Pseudo code pour Bucket Sort:

1. Créez un tableau d'éléments [0..n-1].


2. Appeler un seau Trier de manière répétée sur les chiffres les moins significatifs de chaque
élément en tant que clé.
3. Renvoie le tableau trié.

Exemple de tri de Radix:

Espace auxiliaire: O(n)


Complexité temporelle: O(n)

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

Exemple de cycle Trier:

Espace auxiliaire: O(1)


Complexité temporelle: O(n^2)

Mise en œuvre du pseudocode

(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 #

public class CycleSort


{
public static void SortCycle(int[] input)
{
for (var i = 0; i < [Link]; i++)
{
var item = input[i];
var position = i;
do
{
var k = [Link]((t, j) => position != j && t < item).Count();
if (position == k) continue;
while (position != k && item == input[k])
{
k++;
}
var temp = input[k];
input[k] = item;
item = temp;
position = k;
} while (position != i);
}
}

public static int[] Main(int[] input)


{
SortCycle(input);
return input;
}
}

Lire Tri du cycle en ligne: [Link]

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

Pseudo-code pour le tri du tas:

function heapsort(input, count)


heapify(a,count)
end <- count - 1
while end -> 0 do
swap(a[end],a[0])
end<-end-1
restore(a, 0, end)

function heapify(a, count)


start <- parent(count - 1)
while start >= 0 do
restore(a, start, count - 1)
start <- start - 1

Exemple de tri de tas:

Espace auxiliaire: O(1)

[Link] 292
Complexité temporelle: O(nlogn)

Implémentation C #

public class HeapSort


{
public static void Heapify(int[] input, int n, int i)
{
int largest = i;
int l = i + 1;
int r = i + 2;

if (l < n && input[l] > input[largest])


largest = l;

if (r < n && input[r] > input[largest])


largest = r;

if (largest != i)
{
var temp = input[i];
input[i] = input[largest];
input[largest] = temp;
Heapify(input, n, largest);
}
}

public static void SortHeap(int[] input, int n)


{
for (var i = n - 1; i >= 0; i--)
{
Heapify(input, n, i);
}
for (int j = n - 1; j >= 0; j--)
{
var temp = input[0];
input[0] = input[j];
input[j] = temp;
Heapify(input, j, 0);
}
}

public static int[] Main(int[] input)


{
SortHeap(input, [Link]);
return input;
}
}

Lire Tri du tas en ligne: [Link]

[Link] 293
Chapitre 59: Tri par fusion
Examples
Fusionner les bases du tri

Fusionner le tri est un algorithme de division et de conquête. Il divise successivement la liste


d'entrée de longueur n en deux jusqu'à ce qu'il y ait n listes de taille 1. Ensuite, les paires de listes
sont fusionnées avec le premier élément plus petit parmi la paire de listes ajoutée à chaque étape.
Grâce à la fusion successive et à la comparaison des premiers éléments, la liste triée est
construite.

Un exemple:

Complexité temporelle : T(n) = 2T(n/2) + Θ(n)

La récurrence ci-dessus peut être résolue à l'aide de la méthode d'arbre récurrent ou de la

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

Espace auxiliaire : O(n)

Paradigme algorithmique : diviser et conquérir

Tri en place : pas dans une implémentation typique

Stable : Oui

Fusion de l'implémentation du tri dans C & C #

C Fusionner le tri

int merge(int arr[],int l,int m,int h)


{
int arr1[10],arr2[10]; // Two temporary arrays to
hold the two arrays to be merged
int n1,n2,i,j,k;
n1=m-l+1;
n2=h-m;

for(i=0; i<n1; i++)


arr1[i]=arr[l+i];
for(j=0; j<n2; j++)
arr2[j]=arr[m+j+1];

arr1[i]=9999; // To mark the end of each temporary array


arr2[j]=9999;

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;
}

int merge_sort(int arr[],int low,int high)


{
int mid;
if(low<high) {
mid=(low+high)/2;
// Divide and Conquer
merge_sort(arr,low,mid);
merge_sort(arr,mid+1,high);
// Combine
merge(arr,low,mid,high);
}

[Link] 295
return 0;
}

C # Merge Sort

public class MergeSort


{
static void Merge(int[] input, int l, int m, int r)
{
int i, j;
var n1 = m - l + 1;
var n2 = r - m;

var left = new int[n1];


var right = new int[n2];

for (i = 0; i < n1; i++)


{
left[i] = input[l + i];
}

for (j = 0; j < n2; j++)


{
right[j] = input[m + j + 1];
}

i = 0;
j = 0;
var k = l;

while (i < n1 && j < n2)


{
if (left[i] <= right[j])
{
input[k] = left[i];
i++;
}
else
{
input[k] = right[j];
j++;
}
k++;
}

while (i < n1)


{
input[k] = left[i];
i++;
k++;
}

while (j < n2)


{
input[k] = right[j];
j++;
k++;
}
}

[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 static int[] Main(int[] input)


{
SortMerge(input, 0, [Link] - 1);
return input;
}
}

Fusion de l'implémentation du tri en Java

Ci-dessous, il y a l'implémentation en Java en utilisant une approche générique. C'est le même


algorithme, qui est présenté ci-dessus.

public interface InPlaceSort<T extends Comparable<T>> {


void sort(final T[] elements); }

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);
}

// We check both our sides and then merge them


private void sort(T[] elements, T[] arr, int low, int high) {
if (low >= high) return;
int mid = low + (high - low) / 2;
sort(elements, arr, low, mid);
sort(elements, arr, mid + 1, high);
merge(elements, arr, low, high, mid);
}

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++) {

if (i <= mid && j <= high) {


if (a[i].compareTo(a[j]) >= 0) {
b[k] = a[j++];
} else {
b[k] = a[i++];
}

[Link] 297
} else if (j > high && i <= mid) {
b[k] = a[i++];
} else if (i > mid && j <= high) {
b[k] = a[j++];
}
}

for (int n = low; n <= high; n++) {


a[n] = b[n];
}}}

Fusion de l'implémentation du tri en Python

def merge(X, Y):


" merge two sorted lists "
p1 = p2 = 0
out = []
while p1 < len(X) and p2 < len(Y):
if X[p1] < Y[p2]:
[Link](X[p1])
p1 += 1
else:
[Link](Y[p2])
p2 += 1
out += X[p1:] + Y[p2:]
return out

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)

Implémentation Java de bas en haut

public class MergeSortBU {


private static Integer[] array = { 4, 3, 1, 8, 9, 15, 20, 2, 5, 6, 30, 70,
60,80,0,9,67,54,51,52,24,54,7 };

public MergeSortBU() {
}

private static void merge(Comparable[] arrayToSort, Comparable[] aux, int lo,int mid, int
hi) {

for (int index = 0; index < [Link]; index++) {


aux[index] = arrayToSort[index];
}

[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);
}
}

public static boolean isLess(Comparable a, Comparable b) {


return [Link](b) <= 0;
}

private static void print(Comparable[] array)


{[Link]
StringBuffer buffer = new
StringBuffer();[Link]
for (Comparable value : array) {
[Link](value);
[Link](' ');
}
[Link](buffer);
}

public static void main(String[] args) {


Comparable[] aux = new Comparable[[Link]];
print(array);
[Link](array, aux, 0, [Link] - 1);
}
}

Fusion de l'implémentation du tri dans Go

package main

import "fmt"

func mergeSort(a []int) []int {


if len(a) < 2 {

[Link] 299
return a
}
m := (len(a)) / 2

f := mergeSort(a[:m])
s := mergeSort(a[m:])

return merge(f, s)
}

func merge(f []int, s []int) []int {


var i, j int
size := len(f) + len(s)

a := make([]int, size, size)

for z := 0; z < size; z++ {


lenF := len(f)
lenS := len(s)

if i > lenF-1 && j <= lenS-1 {


a[z] = s[j]
j++
} else if j > lenS-1 && i <= lenF-1 {
a[z] = f[i]
i++
} else if f[i] < s[j] {
a[z] = f[i]
i++
} else {
a[z] = s[j]
j++
}
}

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))
}

Lire Tri par fusion en ligne: [Link]

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

Simplifier la sommation (séries arithmétiques)

Élargit le terme

Simplifier la sommation à nouveau (séries arithmétiques)

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

Simplifier la sommation à nouveau (séries arithmétiques et nombre harmonique)

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

Considérons la liste suivante d'entiers:

[5, 2, 4, 6, 1, 3]

L'algorithme effectuera les étapes suivantes:

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 #

public class InsertionSort


{
public static void SortInsertion(int[] input, int n)
{
for (int i = 0; i < n; i++)
{
int x = input[i];
int j = i - 1;
while (j >= 0 && input[j] > x)
{
input[j + 1] = input[j];
j = j - 1;
}
input[j + 1] = x;
}
}

public static int[] Main(int[] input)


{
SortInsertion(input, [Link]);
return input;
}
}

Espace auxiliaire: O(1)


Complexité temporelle: O(n)

Implantation Haskell

insertSort :: Ord a => [a] -> [a]


insertSort [] = []
insertSort (x:xs) = insert x (insertSort xs)

insert :: Ord a => a-> [a] -> [a]


insert n [] = [n]
insert n (x:xs) | n <= x = (n:x:xs)
| otherwise = x:insert n xs

Lire Tri par insertion en ligne: [Link]

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

1. Mécanisme de schéma de partition Lomuto:

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.

partition(A, low, high) is


pivot := A[high]
i := low
for j := low to high – 1 do
if A[j] ≤ pivot then
swap A[i] with A[j]
i := i + 1
swap A[i] with A[high]
return i

Mécanisme de tri rapide:

quicksort(A, low, high) is


if low < high then
p := partition(A, low, high)
quicksort(A, low, p – 1)
quicksort(A, p + 1, high)

Exemple de tri rapide:

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

quicksort(A, lo, hi) is


if lo < hi then
p := partition(A, lo, hi)
quicksort(A, lo, p)
quicksort(A, p + 1, hi)

Cloison :

partition(A, lo, hi) is

[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

swap A[i] with A[j]

Implémentation C #

public class QuickSort


{
private static int Partition(int[] input, int low, int high)
{
var pivot = input[high];
var i = low - 1;
for (var j = low; j <= high - 1; j++)
{
if (input[j] <= pivot)
{
i++;
var temp = input[i];
input[i] = input[j];
input[j] = temp;
}
}
var tmp = input[i + 1];
input[i + 1] = input[high];
input[high] = tmp;
return (i + 1);
}

private static void SortQuick(int[] input, int low, int high)


{
while (true)
{
if (low < high)
{
var pi = Partition(input, low, high);
SortQuick(input, low, pi - 1);
low = pi + 1;
continue;
}
break;
}
}

public static int[] Main(int[] input)


{
SortQuick(input, 0, [Link] - 1);

[Link] 307
return input;
}
}

Implantation Haskell

quickSort :: Ord a => [a] -> [a]


quickSort [] = []
quickSort (x:xs) = quickSort [ y | y <- xs, y <= x ]
++ [x]
++ quickSort [ z | z <- xs, z > x ]

Implémentation Java de la partition Lomuto

public class Solution {

public static void main(String[] args) {


Scanner sc = new Scanner([Link]);
int n = [Link]();
int[] ar = new int[n];
for(int i=0; i<n; i++)
ar[i] = [Link]();
quickSort(ar, 0, [Link]-1);
}

public static void quickSort(int[] ar, int low, int high)


{
if(low<high)
{
int p = partition(ar, low, high);
quickSort(ar, 0 , p-1);
quickSort(ar, p+1, high);
}
}
public static int partition(int[] ar, int l, int r)
{
int pivot = ar[r];
int i =l;
for(int j=l; j<r; j++)
{
if(ar[j] <= pivot)
{
int t = ar[j];
ar[j] = ar[i];
ar[i] = t;
i++;
}
}
int t = ar[i];
ar[i] = ar[r];
ar[r] = t;

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])

Estampes "[1, 1, 2, 3, 6, 8, 10]"


Lire Tri rapide en ligne: [Link]

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

Travailler (Pseudo Code) pour Pigeonhole Trier:

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.

Exemple de Pigeonhole Trier:

[Link] 310
Espace auxiliaire: O(n)
Complexité temporelle: O(n + N)

Implémentation C #

public class PigeonholeSort


{
private static void SortPigeonhole(int[] input, int n)
{
int min = input[0], max = input[n];
for (int i = 1; i < n; i++)
{
if (input[i] < min) min = input[i];
if (input[i] > max) max = input[i];
}
int range = max - min + 1;
int[] holes = new int[range];

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


{
holes[input[i] - min] = input[i];
}
int index = 0;

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


{
foreach (var value in holes)
{
input[index++] = value;
}
}
}

public static int[] Main(int[] input)


{
SortPigeonhole(input, [Link]);
return input;
}

[Link] 311
}

Lire Trier Pigeonhole en ligne: [Link]

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

Il existe 2 types d’algorithmes pour résoudre ce problème: Algorithmes exacts et algorithmes


d’approximation

Algorithmes exacts

1. Algorithme de force brutale


2. Algorithme de programmation dynamique

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

for i from 0 to N-2


current = current + cost[P[i]][P[i+1]] <- Add the cost of going from 1 vertex to the
next

current = current + cost[P[N-1]][P[0]] <- Add the cost of going from last vertex to
the first

if current < minimum <- Update minimum if necessary


minimum = current

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.

Algorithme de programmation dynamique

Notez que si nous considérons le chemin (dans l'ordre):

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

On peut donc avoir l'algorithme suivant (implémentation C ++):

int cost[N][N]; //Adjust the value of N if needed


int memo[1 << N][N]; //Set everything here to -1
int TSP(int bitmask, int pos){
int cost = INF;
if (bitmask == ((1 << N) - 1)){ //All vertices have been explored
return cost[pos][0]; //Cost to go back
}
if (memo[bitmask][pos] != -1){ //If this has already been computed
return memo[bitmask][pos]; //Just return the value, no need to recompute
}
for (int i = 0; i < N; ++i){ //For every vertex
if ((bitmask & (1 << i)) == 0){ //If the vertex has not been visited
cost = min(cost,TSP(bitmask | (1 << i) , i) + cost[pos][i]); //Visit the vertex
}
}
memo[bitmask][pos] = cost; //Save the result
return cost;
}
//Call TSP(1,0)

Cette ligne peut être un peu déroutante, alors parcourons-la lentement:

cost = min(cost,TSP(bitmask | (1 << i) , i) + cost[pos][i]);

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

Lire Vendeur ambulant en ligne: [Link]

[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: -

let str1 = 'stackoverflow';


let str2 = 'flowerovstack';

Ces chaînes sont des anagrammes.

// Crée un hachage à partir de str1 et augmente un compte.

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.

Code générique pour les anagrammes

(function(){

var hashMap = {};

function isAnagram (str1, str2) {

if([Link] !== [Link]){


return false;
}

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

// Check all value of hashMap keys are zero, so it will be anagram.


return isStringsAnagram(valueExist);
}

function createStr1HashMap (str1) {


[].[Link](str1, function(value, index, array){
hashMap[value] = value in hashMap ? (hashMap[value] + 1) : 1;
return value;
});
}

function createStr2HashMap (str2) {


var valueExist = [].[Link](str2, function(value, index, array){
if(value in hashMap) {
hashMap[value] = hashMap[value] - 1;
}
return value in hashMap;
});
return valueExist;
}

[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;
}
}

isAnagram('stackoverflow', 'flowerovstack'); // true


isAnagram('stackoverflow', 'flowervvstack'); // false

})();

Complexité temporelle: - 3n soit O (n).

Lire Vérifier que deux chaînes sont des anagrammes en ligne:


[Link]

[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:

La sortie doit être fausse:

Comme 4 dans le sous-arbre de gauche est supérieur à la valeur de la racine (3)

Si l'entrée est:

La sortie devrait être vraie

Algorithme pour vérifier si un arbre binaire donné est BST

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

// Check values in left subtree


if root->left != NULL:
max_key_in_left = find_max_key(root->left)
if max_key_in_left > root->key:
return false

// Check values in right subtree


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

return is_BST(root->left) && is_BST(root->right)

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(root, min, max):


if root == NULL:
return true

// is the current node key out of range?


if root->key < min || root->key > max:
return false

// check if left and right subtree is BST


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

Il s'appellera initialement comme:

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.

Lire Vérifier si un arbre est BST ou non en ligne:


[Link]

[Link] 320
Crédits
S.
Chapitres Contributeurs
No

Abdul Karim, Austin Conlon, C L K Kissane, Community,


Commencer avec
1 EsmaeelE, Filip Allberg, Hesham Attia, Jonathan Landrum,
l'algorithme
msohng, Sayakiss, user2314737

A * Algorithme
2 TajyMany
Pathfinding

kiner_shah, Minhas Kamal, mnoronha, Roberto Fernandez,


3 A * Pathfinding
TajyMany

Algo: - Imprime une


4 Creative John
matrice * en carré

Algorithme Bellman
5 Bakhtiar Hasan, Sumeet Singh, user2314737, Yerken
– Ford

6 Algorithme catalan Keyur Ramoliya, mnoronha

Algorithme de
7 Keyur Ramoliya
fenêtre coulissante

8 Algorithme de ligne Dipesh Poudel, Martin Frank

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 Bakhtiar Hasan, Cameron, Community, ghilesZ, M S Hossain,


16
Gourmands theJollySin, xenteros

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

A. Raza, Daniel Nugent, Didgeridoo, EsmaeelE, fgb, Juxhin


Complexité de Metaj, Miljen Mikic, Nick Larsen, Peter K, Sayakiss, Tejus
22
l'algorithme Prasad, user23013, user2314737, VermillionAzure, xenteros,
Yair Twito

23 Comptage tri Bakhtiar Hasan, Keyur Ramoliya, mnoronha

Déformation
24 temporelle Bakhtiar Hasan, mnoronha, Zubayet Zaman Zico
dynamique

25 Depth First Search Bakhtiar Hasan

Isha Agarwal, Malcolm McLean, mnoronha, VermillionAzure,


26 Des arbres
yd1

Exponentiation
27 Bakhtiar Hasan, mnoronha
Matricielle

Fonctions de
28 afeldspar, Didgeridoo, mnoronha
hachage

Ahmed Mazher, Bakhtiar Hasan, EsmaeelE, Filip Allberg,


29 Graphique hurricane, JJTO, John Odom, ldog, Sayakiss, Tejus Prasad,
user23013, VermillionAzure

30 L'algorithme de Bakhtiar Hasan, Tejus Prasad

[Link] 322
Dijkstra

L'algorithme de
31 IVlad, Shubham, Yerken
Kruskal

32 L'algorithme de Prim Bakhtiar Hasan, Tejus Prasad

La plus longue sous-


33 Bakhtiar Hasan, Keyur Ramoliya
séquence commune

34 Le triangle de Pascal EsmaeelE, Keyur Ramoliya

Modifier l'algorithme
35 dynamique de Vishwas
distance

Community, EsmaeelE, mnoronha, msohng, Nick the coder,


36 Notation Big-O
Samuel Peter, user2314737, WitVault

Plus longue sous-


37 Keyur Ramoliya, mnoronha
séquence croissante

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

Programmation Bakhtiar Hasan, Benson Lin, kraskevich, Muyide Ibukun, nbro,


40
dynamique RamenChef, Razik, Sayakiss, Vishwas

41 Pseudocode Community

Anagh Hegde, Benson Lin, brijs, Community, EsmaeelE, Iwan,


42 Recherche Khaled.K, Malcolm McLean, Miljen Mikic, msohng, RamenChef,
ShreePool, Timothy G., umop apisdn, xenteros

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

46 Seau Sort Keyur Ramoliya, mnoronha

[Link] 323
Keyur Ramoliya, lambda, mnoronha, Teodor Kurtev,
47 Sélection Tri
user2314737

48 Sort de crêpes Keyur Ramoliya, mnoronha

49 Sort étrange Keyur Ramoliya

Transformée de
50 Dr. ABT, EsmaeelE
Fourier Rapide

Traversées d'arbres
51 Isha Agarwal
binaires

Traversées
52 Dian Bakti
graphiques

Ahmad Faiyaz, Carlton, Filip Allberg, Frank, ganesshkumar,


53 Tri IVlad, Iwan, Kedar Mhaswade, Miljen Mikic, mok, Patrick87,
RamenChef, Rob Fagen, Set

Anagh Hegde, Deepak, EsmaeelE, Ijaz Khan, Keyur Ramoliya,


54 Tri à bulles
mnoronha, optimistanoop, samgak, xenteros, YoungHobbit

55 Tri de coquille Keyur Ramoliya, mnoronha

56 Tri de radix Keyur Ramoliya, mnoronha, Zopesconk

57 Tri du cycle Keyur Ramoliya, mnoronha

58 Tri du tas Keyur Ramoliya, mnoronha

EsmaeelE, Iwan, Juxhin Metaj, Keyur Ramoliya, Luv Agarwal,


59 Tri par fusion
mnoronha, Santiago Gil, SHARMA

Bakhtiar Hasan, invisal, Keyur Ramoliya, Lymphatus, mnoronha


60 Tri par insertion
, RamenChef

Bakhtiar Hasan, Keyur Ramoliya, Malav, mnoronha,


61 Tri rapide
optimistanoop

62 Trier Pigeonhole Keyur Ramoliya, mnoronha

63 Vendeur ambulant Benson Lin

Vérifier que deux


64 chaînes sont des Creative John
anagrammes

Vérifier si un arbre
65 Isha Agarwal, Janaky Murthy
est BST ou non

[Link] 324

Vous aimerez peut-être aussi