Utilisation de tas et de files d'attente prioritaires – Real Python

By | juin 24, 2020

Formation gratuite Python

Des tas et files d'attente prioritaires sont des structures de données peu connues mais étonnamment utiles. Pour de nombreux problèmes qui impliquent de trouver le meilleur élément dans un ensemble de données, ils offrent une solution facile à utiliser et très efficace. Le Python tas module fait partie de la bibliothèque standard. Il implémente toutes les opérations de tas de bas niveau ainsi que certaines utilisations courantes de haut niveau des tas.

Une file d'attente prioritaire est un outil puissant qui peut résoudre des problèmes aussi variés que l'écriture d'un planificateur d'e-mails, la recherche du chemin le plus court sur une carte ou la fusion de fichiers journaux. La programmation est pleine de problèmes d'optimisation dont le but est de trouver le meilleur élément. Files d'attente prioritaires et fonctions dans Python tas module peut souvent aider à cela.

Dans ce didacticiel, vous apprendrez:

  • Quoi tas et files d'attente prioritaires sont et comment ils se rapportent les uns aux autres
  • Quelles sortes de problèmes peut être résolu en utilisant un tas
  • Comment utiliser le Python tas module pour résoudre ces problèmes

Ce tutoriel est destiné aux Pythonistas qui sont à l'aise avec les listes, les dict, les ensembles et les générateurs et qui recherchent des structures de données plus sophistiquées.

Que sont les tas?

Les tas sont béton structures de données, alors que les files d'attente prioritaires sont abstrait structures de données. Une structure de données abstraite détermine l'interface, tandis qu'une structure de données concrète définit l'implémentation.

Les tas sont couramment utilisés pour implémenter des files d'attente prioritaires. Il s'agit de la structure de données concrète la plus populaire pour implémenter la structure de données abstraite de file d'attente prioritaire.

Les structures de données concrètes précisent également garanties de performance. Les garanties de performance définissent la relation entre Taille de la structure et de la temps les opérations prennent. Comprendre ces garanties vous permet de prévoir le temps que prendra le programme à mesure que la taille de ses entrées change.

Structures de données, tas et files d'attente prioritaires

Les structures de données abstraites spécifient les opérations et les relations entre elles. La structure de données abstraite de file d'attente prioritaire, par exemple, prend en charge trois opérations:

  1. est vide vérifie si la file d'attente est vide.
  2. add_element ajoute un élément à la file d'attente.
  3. pop_element affiche l'élément avec la priorité la plus élevée.

Les files d'attente de priorité sont couramment utilisées pour optimiser l'exécution de la tâche, dans laquelle l'objectif est de travailler sur la tâche avec la priorité la plus élevée. Une fois la tâche terminée, sa priorité est réduite et elle est renvoyée dans la file d'attente.

Il existe deux conventions différentes pour déterminer la priorité d'un élément:

  1. le le plus grand L'élément a la priorité la plus élevée.
  2. le le plus petit L'élément a la priorité la plus élevée.

Ces deux conventions sont équivalentes car vous pouvez toujours inverser l'ordre effectif. Par exemple, si vos éléments sont constitués de nombres, l'utilisation de nombres négatifs inversera les conventions.

Le Python tas module utilise la deuxième convention, qui est généralement la plus courante des deux. En vertu de cette convention, le le plus petit L'élément a la priorité la plus élevée. Cela peut sembler surprenant, mais c'est souvent très utile. Dans les exemples réels que vous verrez plus tard, cette convention simplifiera votre code.

Les structures de données concrètes mettent en œuvre les opérations définies dans une structure de données abstraite et spécifient davantage les garanties de performances.

L'implémentation en tas de la file d'attente prioritaire garantit que les éléments push (ajout) et popping (suppression) sont temps logarithmique opérations. Cela signifie que le temps nécessaire pour faire des push et pop est proportionnel à la logarithme en base 2 du nombre d'éléments.

Les logarithmes croissent lentement. Le logarithme en base 2 de quinze est d'environ quatre, tandis que le logarithme en base 2 d'un billion est d'environ quarante. Cela signifie que si un algorithme est assez rapide sur quinze éléments, il ne sera que dix fois plus lent sur un billion d'éléments et sera probablement encore assez rapide.

Dans toute discussion sur la performance, la plus grande mise en garde est que ces considérations abstraites sont moins significatives que de mesurer réellement un programme concret et d'apprendre où se trouvent les goulots d'étranglement. Les garanties de performance générales sont toujours importantes pour faire des prévisions utiles sur le comportement du programme, mais ces prévisions doivent être confirmées.

Implémentation de tas

Un tas implémente une file d'attente prioritaire en tant que arbre binaire complet. Dans un arbre binaire, chaque nœud aura au maximum deux enfants. Dans un Achevée arbre binaire, tous les niveaux sauf éventuellement le plus profond sont pleins à tout moment. Si le niveau le plus profond est incomplet, il aura les nœuds aussi à gauche que possible.

le propriété d'exhaustivité signifie que la profondeur de l'arbre est le logarithme en base 2 du nombre d'éléments, arrondi. Voici un exemple d'arbre binaire complet:

Arbre binaire complet satisfaisant la propriété du tas

Dans cet exemple particulier, tous les niveaux sont terminés. Chaque nœud, à l'exception des plus profonds, a exactement deux enfants. Il y a un total de sept nœuds sur trois niveaux. Trois est le logarithme en base 2 de sept, arrondi.

Le nœud unique au niveau de la base est appelé racine nœud. Il peut sembler étrange d'appeler le nœud en haut de l'arbre la racine, mais c'est la convention courante en programmation et en informatique.

Les garanties de performances d'un tas dépendent de la façon dont les éléments s'infiltrent dans l'arborescence. Le résultat pratique de ceci est que le nombre de comparaisons dans un tas est le logarithme en base 2 de la taille de l'arbre.

Dans un arbre de tas, la valeur d'un nœud est toujours inférieure à ses deux enfants. C'est ce qu'on appelle le propriété de tas. Ceci est différent d'un arbre de recherche binaire, dans lequel seul le nœud gauche sera plus petit que la valeur de son parent.

Les algorithmes de poussée et d'éclatement reposent sur une violation temporaire de la propriété de segment de mémoire, puis sur la fixation de la propriété de segment de mémoire via des comparaisons et des remplacements vers le haut ou vers le bas d'une seule branche.

Par exemple, pour pousser un élément sur un tas, Python ajoute le nouveau nœud au prochain emplacement ouvert. Si la couche inférieure n'est pas pleine, le nœud est ajouté au prochain emplacement ouvert en bas. Sinon, un nouveau niveau est créé, puis l'élément est ajouté au nouveau calque inférieur.

Une fois le nœud ajouté, Python le compare à son parent. Si la propriété du segment de mémoire est violée, le nœud et son parent sont commutés et la vérification recommence au niveau du parent. Cela continue jusqu'à ce que la propriété du tas soit conservée ou que la racine soit atteinte.

De même, lors de l'éclatement du plus petit élément, Python sait qu'en raison de la propriété du tas, l'élément est à la racine de l'arborescence. Il remplace l'élément par le dernier élément de la couche la plus profonde, puis vérifie si la propriété du tas est violée dans la branche.

Utilisations des files d'attente prioritaires

Une file d'attente prioritaire et un segment de mémoire en tant qu'implémentation d'une file d'attente prioritaire sont utiles pour les programmes qui impliquent de trouver un élément extrême d'une certaine manière. Par exemple, vous pouvez utiliser une file d'attente prioritaire pour l'une des tâches suivantes:

  • Obtenir les trois articles de blog les plus populaires à partir des données de hit
  • Trouver le moyen le plus rapide pour aller d'un point à l'autre
  • Prédire quel bus sera le premier à arriver à une station en fonction de la fréquence d'arrivée

Une autre tâche pour laquelle vous pouvez utiliser une file d'attente prioritaire est la planification des e-mails. Imaginez un système qui possède plusieurs types d'e-mails, chacun devant être envoyé à une certaine fréquence. Un type d'e-mail doit être diffusé toutes les quinze minutes et un autre doit être envoyé toutes les quarante minutes.

Un planificateur peut ajouter les deux types de courrier électronique à la file d'attente avec un horodatage indiquant quand l'e-mail doit être envoyé. Ensuite, l'ordonnanceur peut regarder l'élément avec le plus petit horodatage – indiquant qu'il est le prochain en ligne à envoyer – et calculer la durée de sommeil avant l'envoi.

Lorsque le planificateur se réveille, il traite l'e-mail concerné, retire l'e-mail de la file d'attente prioritaire, calcule l'horodatage suivant et remet l'e-mail dans la file d'attente au bon emplacement.

Des tas comme listes dans le Python tas Module

Bien que vous ayez vu le tas décrit plus tôt comme un arbre, il est important de se rappeler que Achevée arbre binaire. L'exhaustivité signifie qu'il est toujours possible de déterminer le nombre d'éléments sur chaque couche, à l'exception du dernier. Pour cette raison, des tas peuvent être implémentés sous forme de liste. C'est ce que Python tas module le fait.

Il existe trois règles qui déterminent la relation entre l'élément à l'index k et ses éléments environnants:

  1. Son premier enfant est à 2 * k + 1.
  2. Son deuxième enfant est à 2 * k + 2.
  3. Son parent est à (k + 1) // 2.

Les règles ci-dessus vous indiquent comment visualiser une liste sous forme d'arbre binaire complet. N'oubliez pas qu'un élément a toujours un parent, mais certains éléments n'ont pas d'enfants. Si 2 * k est au-delà de la fin de la liste, alors l'élément n'a pas d'enfants. Si 2 * k est un index valide mais 2 * k + 1 n'est pas, alors l'élément n'a qu'un seul enfant.

La propriété du tas signifie que si h est un tas, alors ce qui suit ne sera jamais Faux:

h[[[[k] <= h[[[[2*k + 1] et h[[[[k] <= h[[[[2*k + 2]

Cela pourrait soulever une IndexError si l'un des indices dépasse la longueur de la liste, mais il ne sera jamais Faux.

En d'autres termes, un élément doit toujours être plus petit que les éléments qui sont à deux fois son indice plus un et deux fois son indice plus deux.

Voici un visuel d'une liste qui satisfait la propriété du tas:

Tas mis en œuvre sous forme de liste

Les flèches vont de l'élément k aux éléments 2 * k + 1 et 2 * k + 2. Par exemple, le premier élément d'une liste Python a l'index 0, donc ses deux flèches pointent vers des indices 1 et 2. Remarquez comment les flèches passent toujours d'une valeur plus petite à une valeur plus grande. C'est ainsi que vous pouvez vérifier que la liste satisfait la propriété du tas.

Opérations de base

Le Python tas module implémente les opérations de tas sur les listes. Contrairement à de nombreux autres modules, il ne ne pas définir une classe personnalisée. Le Python tas Le module a des fonctions qui fonctionnent directement sur les listes.

Habituellement, comme dans l'exemple de messagerie ci-dessus, les éléments seront insérés un par un dans un tas, en commençant par un tas vide. Cependant, s'il existe déjà une liste d'éléments qui doit être un tas, alors le Python tas le module comprend heapify () pour transformer une liste en un tas valide.

Le code suivant utilise heapify () tourner une dans une tas.

>>>

>>> importer tas
>>> une = [[[[3, 5, 1, 2, 6, 8, 7]
>>> tas.heapify(une)
>>> une
[1, 2, 3, 5, 6, 8, 7]

Vous pouvez vérifier cela même si 7 vient après 8, la liste une obéit toujours à la propriété du tas. Par exemple, une[2], lequel est 3, est inférieur à une[2*2 + 2], lequel est 7.

Comme vous pouvez le voir, heapify () modifie la liste en place mais ne la trie pas. Un tas n'a pas besoin d'être trié pour satisfaire la propriété du tas. Cependant, puisque chaque liste triée Est-ce que satisfaire la propriété du tas, exécution heapify () sur une liste triée ne changera pas l'ordre des éléments de la liste.

Les autres opérations de base dans Python tas module suppose que la liste est déjà un tas. Il est utile de noter qu’une liste vide ou une liste de longueur un sera toujours un tas.

Étant donné que la racine de l'arbre est le premier élément, vous n'avez pas besoin d'une fonction dédiée pour lire le plus petit élément de manière non destructive. Le premier élément, une[0], sera toujours le plus petit élément.

Pour faire apparaître le plus petit élément tout en préservant la propriété du tas, le Python tas module définit heappop ().

Voici comment utiliser heappop () pour faire apparaître un élément:

>>>

>>> importer tas
>>> une = [[[[1, 2, 3, 5, 6, 8, 7]
>>> tas.heappop(une)
1
>>> une
[2, 5, 3, 7, 6, 8]

La fonction renvoie le premier élément, 1et conserve la propriété du tas sur une. Par exemple, une[1] est 5 et une[1*2 + 2] est 6.

Le Python tas le module comprend également heappush () pour pousser un élément vers le tas tout en préservant la propriété du tas.

L'exemple suivant montre comment pousser une valeur vers un segment de mémoire:

>>>

>>> importer tas
>>> une = [[[[2, 5, 3, 7, 6, 8]
>>> tas.heappush(une, 4)
>>> une
[2, 5, 3, 7, 6, 8, 4]
>>> tas.heappop(une)
2
>>> tas.heappop(une)
3
>>> tas.heappop(une)
4

Après avoir poussé 4 au tas, vous en faites sortir trois éléments. Puisque 2 et 3 étaient déjà dans le tas et sont plus petits que 4, ils apparaissent en premier.

Le Python tas Le module définit également deux autres opérations:

  1. heapreplace () est équivalent à heappop () suivi par heappush ().
  2. heappushpop () est équivalent à heappush () suivi par heappop ().

Celles-ci sont utiles dans certains algorithmes car elles sont plus efficaces que de faire les deux opérations séparément.

Une opération de haut niveau

Étant donné que les files d'attente prioritaires sont si souvent utilisées pour fusionner des séquences triées, le Python tas module a une fonction prête à l'emploi, fusionner(), pour avoir utilisé des tas pour fusionner plusieurs itérables. fusionner() suppose que ses itérables d'entrée sont déjà triés et renvoie un itérateur, pas une liste.

Comme exemple d'utilisation fusionner(), voici une implémentation du planificateur d'e-mails décrit précédemment:

importer datetime
importer tas

def email(la fréquence, détails):
    actuel = datetime.datetime.maintenant()
    tandis que Vrai:
        actuel + = la fréquence
        rendement actuel, détails

fast_email = email(datetime.timedelta(minutes=15), "email rapide")
slow_email = email(datetime.timedelta(minutes=40), "e-mail lent")

unifié = tas.fusionner(fast_email, slow_email)

Les entrées pour fusionner() dans cet exemple sont des générateurs infinis. La valeur de retour affectée à la variable unifié est également un itérateur infini. Cet itérateur donnera les e-mails à envoyer dans l'ordre des futurs horodatages.

Pour déboguer et confirmer que le code fusionne correctement, vous pouvez imprimer les dix premiers e-mails à envoyer:

>>>

>>> pour _ dans gamme(dix):
...    impression(suivant(élément))
(datetime.datetime (2020, 4, 12, 21, 27, 20, 305358), 'email rapide')
(datetime.datetime (2020, 4, 12, 21, 42, 20, 305358), 'email rapide')
(datetime.datetime (2020, 4, 12, 21, 52, 20, 305360), 'email lent')
(datetime.datetime (2020, 4, 12, 21, 57, 20, 305358), 'email rapide')
(datetime.datetime (2020, 4, 12, 22, 12, 20, 305358), 'email rapide')
(datetime.datetime (2020, 4, 12, 22, 27, 20, 305358), 'email rapide')
(datetime.datetime (2020, 4, 12, 22, 32, 20, 305360), 'email lent')
(datetime.datetime (2020, 4, 12, 22, 42, 20, 305358), 'email rapide')
(datetime.datetime (2020, 4, 12, 22, 57, 20, 305358), 'email rapide')
(datetime.datetime (2020, 4, 12, 23, 12, 20, 305358), 'email rapide')

Remarquez comment email rapide est programmé tous les 15 minutes, le e-mail lent est programmé tous les 40, et les e-mails sont correctement entrelacés afin d'être organisés dans l'ordre de leurs horodatages.

fusionner() ne lit pas toutes les entrées, mais il fonctionne plutôt de manière dynamique. Même si les deux entrées sont des itérateurs infinis, l'impression des dix premiers éléments se termine rapidement.

De la même manière, lorsqu'il est utilisé pour fusionner des séquences triées comme des lignes de fichier journal organisées par horodatage, même si les journaux sont volumineux, cela prendra une quantité raisonnable de mémoire.

Les problèmes que les tas peuvent résoudre

Comme vous l'avez vu ci-dessus, les tas sont bons pour fusionner progressivement des séquences triées. Deux applications pour les tas que vous avez déjà envisagées sont la planification de tâches périodiques et la fusion des fichiers journaux. Cependant, il existe de nombreuses autres applications.

Des tas peuvent également aider à identifier le sommet n ou en bas n des choses. Le Python tas Le module possède des fonctions de haut niveau qui implémentent ce comportement.

Par exemple, ce code obtient en entrée les temps de la finale du 100 mètres femmes aux Jeux olympiques d'été de 2016 et imprime les médaillées, ou les trois meilleures finalistes:

>>>

>>> importer tas
>>> résultats="" "
... Christania Williams 11.80
... Marie-Josée Ta Lou 10.86
... Elaine Thompson 10,71
... Tori Bowie 10.83
... Shelly-Ann Fraser-Pryce 10,86
... Anglais Gardner 10.94
... Michelle-Lee Ahye 10,92
... Dafne Schippers 10.90
... "" "
>>> Top 3 = tas.nsmallest(
...     3, résultats.splitlines(), clé=lambda X: flotte(X.Divisé()[[[[-1])
... )
>>> impression(" n".joindre(Top 3))
Elaine Thompson 10,71
Tori Bowie 10.83
Marie-Josée Ta Lou 10.86

Ce code utilise nsmallest () du Python tas module. nsmallest () renvoie les plus petits éléments d'un itérable et accepte trois arguments:

  1. n indique le nombre d'éléments à retourner.
  2. itérable identifie les éléments ou le jeu de données à comparer.
  3. clé est une fonction appelable qui détermine comment les éléments sont comparés.

Ici le clé La fonction divise la ligne par des espaces, prend le dernier élément et le convertit en nombre à virgule flottante. Cela signifie que le code triera les lignes par durée d'exécution et renverra les trois lignes avec les plus petites durées d'exécution. Ceux-ci correspondent aux trois coureurs les plus rapides, ce qui vous donne les médaillés d'or, d'argent et de bronze.

Le Python tas le module comprend également nlargest (), qui a des paramètres similaires et renvoie les éléments les plus grands. Cela serait utile si vous vouliez obtenir les médaillés de la compétition de lancer de javelot, dans laquelle le but est de lancer le javelot le plus loin possible.

Comment identifier les problèmes

Un tas, en tant qu'implémentation d'une file d'attente prioritaire, est un bon outil pour résoudre des problèmes impliquant des extrêmes, comme le plus ou le moins d'une métrique donnée.

Il y a d'autres mots qui indiquent qu'un tas pourrait être utile:

  • Le plus grand
  • Le plus petit
  • Le plus grand
  • Le plus petit
  • Meilleur
  • Pire
  • Haut
  • Bas
  • Maximum
  • Le minimum
  • Optimal

Chaque fois qu'un énoncé de problème indique que vous recherchez un élément extrême, il vaut la peine de se demander si une file d'attente prioritaire serait utile.

Parfois, la file d'attente prioritaire sera uniquement partie de la solution, et le reste sera une variante de la programmation dynamique. C'est le cas avec l'exemple complet que vous verrez dans la section suivante. La programmation dynamique et les files d'attente prioritaires sont souvent utiles ensemble.

Exemple: recherche de chemins

L'exemple suivant sert de cas d'utilisation réaliste pour Python tas module. L'exemple utilise un algorithme classique qui, en tant que partie, nécessite un segment.

Imaginez un robot qui a besoin de naviguer dans un labyrinthe en deux dimensions. Le robot doit aller de l'origine, positionnée dans le coin supérieur gauche, à la destination dans le coin inférieur droit. Le robot a une carte du labyrinthe en mémoire, il peut donc planifier tout le chemin avant de partir.

L'objectif est que le robot termine le labyrinthe le plus rapidement possible.

Notre algorithme est une variante de l'algorithme de Dijkstra. Trois structures de données sont conservées et mises à jour tout au long de l'algorithme:

  1. provisoire est une carte d'un chemin provisoire de l'origine à une position, pos. Le chemin est appelé provisoire car c'est le plus court connu chemin, mais il pourrait être amélioré.

  2. certain est un ensemble de points pour lesquels le chemin provisoire cartes est certain être le chemin le plus court possible.

  3. candidats est un tas de positions qui ont un chemin. le clé de tri du tas est la longueur du chemin.

À chaque étape, vous effectuez jusqu'à quatre actions:

  1. Pop un candidat de candidats.

  2. Ajoutez le candidat au certain ensemble. Si le candidat est déjà membre du certain définir, puis ignorer les deux actions suivantes.

  3. Trouvez le chemin le plus court connu vers le candidat actuel.

  4. Pour chacun des voisins immédiats du candidat actuel, voyez si le fait de passer par le candidat donne un chemin plus court que l'actuel provisoire chemin. Si oui, mettez à jour le provisoire chemin et le candidats tas avec ce nouveau chemin.

Les étapes sont exécutées en boucle jusqu'à ce que la destination soit ajoutée au certain ensemble. Lorsque la destination est dans le certain ensemble, vous avez terminé. La sortie de l'algorithme est la provisoire chemin vers la destination, qui est maintenant certain être le chemin le plus court possible.

Code de niveau supérieur

Maintenant que vous comprenez l'algorithme, il est temps d'écrire du code pour l'implémenter. Avant d'implémenter l'algorithme lui-même, il est utile d'écrire du code de support.

Tout d'abord, vous devez importer le Python tas module:

Vous utiliserez les fonctions de Python tas module pour maintenir un tas qui vous aidera à trouver la position avec le chemin le plus court connu à chaque itération.

L'étape suivante consiste à définir la carte en tant que variable dans le code:

carte = "" "
.......X..
.......X..
.... XXXX ..
..........
..........
"" "

La carte est une chaîne à trois guillemets qui montre la zone dans laquelle le robot peut se déplacer ainsi que les obstacles.

Bien qu'un scénario plus réaliste vous permette de lire la carte à partir d'un fichier, à des fins pédagogiques, il est plus facile de définir une variable dans le code à l'aide de cette carte simple. Le code fonctionnera sur n'importe quelle carte, mais il est plus facile à comprendre et à déboguer sur une carte simple.

Cette carte est optimisée pour être facile à comprendre pour un lecteur humain du code. Le point (.) est suffisamment léger pour paraître vide, mais il a l'avantage de montrer les dimensions de la zone autorisée. le X les positions marquent des obstacles que le robot ne peut pas franchir.

Code de support

La première fonction convertira la carte en quelque chose de plus facile à analyser dans le code. parse_map () obtient une carte et l'analyse:

def parse_map(carte):
    lignes = carte.splitlines()
    origine = 0, 0
    destination = len(lignes[[[[-1]) - 1, len(lignes) - 1
    revenir lignes, origine, destination

La fonction prend une carte et retourne un tuple de trois éléments:

  1. Une liste de lignes
  2. le origine
  3. le destination

Cela permet au reste du code de fonctionner sur des structures de données conçues pour les ordinateurs, et non pour la capacité des humains à analyser visuellement.

La liste de lignes peut être indexé par (x, y) coordonnées. L'expression lignes[y][x] renvoie la valeur de la position sous forme de l'un des deux caractères:

  1. Un point (".") indique que la position est un espace vide.
  2. La lettre "X" indique que la position est un obstacle.

Cela sera utile lorsque vous souhaitez trouver les positions que le robot peut occuper.

La fonction est valable() calcule si une donnée (x, y) la position est valide:

def est valable(lignes, position):
    X, y = position
    si ne pas (0 <= y < len(lignes) et 0 <= X < len(lignes[[[[y])):
        revenir Faux
    si lignes[[[[y][[[[X] == "X":
        revenir Faux
    revenir Vrai

Cette fonction prend deux arguments:

  1. lignes est la carte sous forme de liste de lignes.
  2. position est la position à vérifier en tant que deux tuple d'entiers indiquant la (x, y) coordonnées.

Pour être valide, une position doit être à l'intérieur des limites de la carte et non un obstacle.

La fonction vérifie que y est valide en vérifiant la longueur du lignes liste. La fonction vérifie ensuite que X est valide en s'assurant qu'il est à l'intérieur lignes[y]. Enfin, maintenant que vous savez que les deux coordonnées sont à l'intérieur de la carte, le code vérifie qu'elles ne sont pas un obstacle en regardant le personnage dans cette position et en comparant le personnage à "X".

Un autre assistant utile est get_neighbors (), qui trouve tous les voisins d'une position:

def get_neighbors(lignes, actuel):
    X, y = actuel
    pour dx dans [[[[-1, 0, 1]:
        pour teindre dans [[[[-1, 0, 1]:
            si dx == 0 et teindre == 0:
                continuer
            position = X + dx, y + teindre
            si est valable(lignes, position):
                rendement position

La fonction renvoie toutes les positions valides entourant la position actuelle.

get_neighbors () veille à ne pas identifier une position comme son propre voisin, mais il autorise les voisins en diagonale. C’est pourquoi au moins un des dx et teindre ne doit pas être nul, mais il est normal que les deux soient différents de zéro.

La fonction d'aide finale est get_shorter_paths (), qui trouve des chemins plus courts:

def get_shorter_paths(provisoire, postes, à travers):
    chemin = provisoire[[[[à travers] + [[[[à travers]
    pour position dans postes:
        si position dans provisoire et len(provisoire[[[[position]) <= len(chemin):
            continuer
        rendement position, chemin

get_shorter_paths () donne des positions pour lesquelles le chemin qui a à travers car sa dernière étape est plus courte que le chemin connu actuel.

get_shorter_paths () a trois paramètres:

  1. provisoire est un dictionnaire mappant une position sur le chemin le plus court connu.
  2. postes est un itérable de positions auxquelles vous souhaitez raccourcir le chemin.
  3. à travers est la position par laquelle, peut-être, un chemin plus court vers le postes peut être trouvé.

L'hypothèse est que tous les éléments postes peut être atteint en une seule étape à travers.

La fonction get_shorter_paths () vérifie si vous utilisez à travers car la dernière étape fera un meilleur chemin pour chaque position. S'il n'y a pas de chemin connu vers une position, alors tout chemin est plus court. S'il existe un chemin connu, vous ne donnez le nouveau chemin que si sa longueur est plus courte. Afin de rendre l'API de get_shorter_paths () plus facile à utiliser, une partie du rendement est également le chemin le plus court.

Toutes les fonctions d'aide ont été écrites pour être fonctions pures, ce qui signifie qu'ils ne modifient aucune structure de données et ne renvoient que des valeurs. Cela facilite le suivi de l'algorithme de base, qui effectue toutes les mises à jour de la structure de données.

Code d'algorithme de base

Pour récapituler, vous recherchez le chemin le plus court entre l'origine et la destination.

Vous conservez trois données:

  1. certain est l'ensemble de certaines positions.
  2. candidats est le tas de candidats.
  3. provisoire est un dictionnaire mappant les nœuds au chemin connu le plus court actuel.

Un poste est en certain si vous pouvez être certain que le chemin le plus court connu est le plus court possible. Si la destination est dans le certain défini, le chemin le plus court connu vers la destination est incontestablement le chemin le plus court possible, et vous pouvez renvoyer ce chemin.

Le tas de candidats est organisé par la longueur du chemin le plus court connu et est géré à l'aide des fonctions de Python tas module.

À chaque étape, vous regardez le candidat avec le chemin le plus court connu. C'est là que le tas est sauté avec heappop (). Il n'y a pas de chemin plus court vers ce candidat – tous les autres chemins passent par un autre nœud dans candidats, et tout cela est plus long. Pour cette raison, le candidat actuel peut être marqué certain.

Vous examinez ensuite tous les voisins qui n'ont pas été visités, et si le passage par le nœud actuel est une amélioration, vous les ajoutez au candidats tas utilisant heappush ().

La fonction find_path () implémente cet algorithme:

    1 def find_path(carte):
    2     lignes, origine, destination = parse_map(carte)
    3     provisoire = origine: []
    4     candidats = [([([([(0, origine)]
    5     certain = ensemble()
    6     tandis que destination ne pas dans connu et len(candidats) > 0:
    7         _ignoré, actuel = tas.heappop(candidats)
    8         si actuel dans certain:
    9             continuer
dix         certain.ajouter(actuel)
11         voisins = ensemble(get_neighbors(lignes, actuel)) - certain
12         plus court = get_shorter_paths(provisoire, voisins, actuel)
13         pour voisin, chemin dans plus court:
14             provisoire[[[[voisin] = chemin
15             tas.heappush(candidats, (len(chemin), voisin))
16     si destination dans provisoire:
17         revenir provisoire[[[[destination] + [[[[destination]
18     autre:
19         élever ValueError("pas de chemin")

find_path () reçoit un carte sous forme de chaîne et renvoie le chemin de l'origine à la destination sous forme de liste de positions.

Cette fonction est un peu longue et compliquée, alors parcourons-la un par un:

  • Lignes 2 à 5 configurer les variables que la boucle examinera et mettra à jour. Vous connaissez déjà un chemin de l'origine à lui-même, qui est le chemin vide, de longueur 0.

  • Ligne 6 définit la condition de terminaison de la boucle. S'il n'y a pas candidats, alors aucun chemin ne peut être raccourci. Si destination est dans certain, puis le chemin vers destination ne peut pas être raccourci.

  • Lignes 7 à 10 obtenir un candidat en utilisant heappop (), sautez la boucle si elle est déjà certain, et sinon ajouter le candidat à certain. Cela garantit que chaque candidat sera traité par la boucle au plus une fois.

  • Lignes 11 à 15 utilisation get_neighbors () et get_shorter_paths () pour trouver des chemins plus courts vers les positions voisines et mettre à jour provisoire dictionnaire et candidats tas.

  • Lignes 16 à 19 gérer le retour du résultat correct. Si un chemin a été trouvé, la fonction le retournera. Bien que calculer les chemins sans pour autant la position finale a rendu l'implémentation de l'algorithme plus simple, c'est une meilleure API pour le renvoyer avec la destination. Si aucun chemin n'est trouvé, une exception est déclenchée.

La division de la fonction en sections distinctes vous permet de la comprendre une partie à la fois.

Code de visualisation

Si l'algorithme était réellement utilisé par un robot, alors le robot fonctionnerait probablement mieux avec une liste de positions qu'il devrait parcourir. Cependant, pour améliorer le résultat en regardant les humains, il serait plus agréable de les visualiser.

show_path () dessine un chemin sur une carte:

def show_path(chemin, carte):
    lignes = carte.splitlines()
    pour X, y dans chemin:
        lignes[[[[y] = lignes[[[[y][:[:[:[:X] + "@" + lignes[[[[y][[[[X + 1 :]
    revenir " n".joindre(lignes) + " n"

La fonction prend la chemin et carte comme paramètres. Il renvoie une nouvelle carte avec le chemin indiqué par le symbole at ("@").

Exécuter le code

Enfin, vous devez appeler les fonctions. Cela peut être fait à partir de l'interpréteur interactif Python.

Le code suivant exécutera l'algorithme et affichera une jolie sortie:

>>>

>>> chemin = find_path(carte)
>>> impression(show_path(chemin, carte))
@@.....X..
..@....X..
... @ XXXX ..
.... @@@@@.
......... @

Vous obtenez d'abord le chemin le plus court find_path (). Ensuite, vous le passez à show_path () pour rendre une carte avec le chemin marqué dessus. Enfin, vous impression() la carte à la sortie standard.

Le chemin se déplace d'un pas à droite, puis de quelques pas en diagonale vers le bas à droite, puis de plusieurs autres pas à droite, et il se termine finalement par un pas en diagonale en bas à droite.

Toutes nos félicitations! Vous avez résolu un problème en utilisant Python tas module.

Ces types de problèmes d'orientation, résolubles par une combinaison de programmation dynamique et de files d'attente prioritaires, sont courants dans les entretiens d'embauche et les défis de programmation. Par exemple, l'avènement du code 2019 incluait un problème qui pouvait être résolu avec les techniques décrites ici.

Conclusion

Vous savez maintenant ce que tas et File d'attente de priorité les structures de données et les types de problèmes qu'elles sont utiles pour résoudre. Vous avez appris à utiliser le Python tas module pour utiliser les listes Python comme tas. Vous avez également appris à utiliser les opérations de haut niveau dans Python tas module, comme fusionner(), qui utilisent un tas en interne.

Dans ce didacticiel, vous avez appris à:

  • Utilisez le fonctions de bas niveau dans le Python tas module pour résoudre les problèmes qui nécessitent un tas ou une file d'attente prioritaire
  • Utilisez le fonctions de haut niveau dans le Python tas module pour fusionner des itérables triés ou trouver les éléments les plus grands ou les plus petits dans un itérable
  • Reconnaître problèmes que les tas et les files d'attente prioritaires peuvent aider à résoudre
  • Prédire le performance de code qui utilise des tas

Avec votre connaissance des tas et du Python tas module, vous pouvez désormais résoudre de nombreux problèmes dont la solution dépend de la recherche du plus petit ou du plus grand élément.