Données Graphes Voc.

Identification

Infoforall

29 - Implémenter un graphe en Python


Un peu de Python aujourd'hui. Et du Pyxel pour les plus rapides.

Graphe et Pyxel

Documents de cours : open document ou pdf

1 - Implémentation des graphes en Python

1.1 Implémentation basique de graphe en Python

Implémenter rapidement un graphe en Python

Lorsqu'on doit implémenter des graphes en Python, on utilise souvent un modèle dictionnaire.

Le graphe est un dictionnaire où

  • la clé est l'étiquette du sommet ;
  • la valeur associée est la liste d'adjacence de ce sommet, c'est à dire un tableau contenant les étiquettes des sommets successeurs lors d'une marche avant.
1 2 3 4 5 6
graphe = {} graphe['a'] = ['b', 'c'] graphe['b'] = ['a', 'c'] graphe['c'] = ['a', 'b', 'd'] graphe['d'] = ['c']

Traçons la représentation graphique d'un graphe orienté qui aurait cette implémentation :

Représentation sous forme d'un graphe orienté

Cette implémentation ne différencie pas formellement arc et arête.

Si les liens sont asymétriques, on est certain que le code représente un graphe orienté. Par contre, si tous les arcs ont un arc associé inverse, on ne peut pas savoir si on travaille sur un graphe non orienté ou un graphe orienté.

Représentation sous forme d'un graphe non orienté

C'est la façon d'utiliser le liens du modèle qui va nous permettre de choisir notre type de graphe : orienté ou non orienté.

01° Fournir l'implémentation Python de ce graphe non orienté.

La valeur associée une clé doit être un tableau des sommets adjacents, les successeurs accessibles depuis ce sommet lors d'une marche en avant.

Déplacement

02° Fournir l'implémentation Python de ce graphe.

Déplacement

03° Compléter le programme pour que ce graphe non orienté correspond au programme :

Comme il faut bien donner les adjacents en tentant de respecter une sorte de logique, nous avons pris l'ordre lexicographique.

Exploration d'un graphe
1 2 3 4 5 6 7 8 9 10 11 12 13
graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = [..., ...] graphe['h'] = [..., ..., ..., ...] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j']

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13
graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j']

Notre implémentation Python utilise donc un modèle dict[int, list], un dictionnaire dont les clés sont des entiers et les valeurs des listes.

Cela ne ressemble pas vraiment à la définition mathématique g = (S, A) qui est donc un tuple[list, list], un couple de list.

Vous allez voir qu'on peut en réalité passer facilement d'une représentation à une autre.

04° Commençons par voir comme passer d'un graphe fourni sous forme dict[int, list] utilisé depuis le début de l'activité en graphe "plus mathématique" comportant l'ensemble S des sommets et l'ensemble A des arêtes.

Voici un code console permettant de voir que la liste d'adjacence caractérise bien le graphe puisqu'on parvient à retrouver les ensemble S et A.

>>> graphe = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3]} >>> s = list(graphe.keys()) >>> s [0, 1, 2, 3, 4] >>> temporaire = [] >>> for s, lst_adj in graphe.items(): for s2 in lst_adj: if not (s2, s) in temporaire: temporaire.append( (s, s2) ) >>> a = tuple(temporaire) >>> a ((0, 1), (0, 2), (1, 3), (2, 3), (3, 4)) >>> g = (s, a)

On voit donc qu'on peut passer du graphe "dictionnaire" au graphe version "mathématique".

Question : expliquer en français comment on réalise ce transtypage.

Voyons maintenant omment passer du graphe "mathématique" au graphe "dictionnaire".

05° Donner le dictionnaire dont les clés sont les étiquettes des sommets et les valeurs sont les listes d'adjacence de ce sommet.

1 2 3
s = (0, 1, 2, 3, 4) # Ensemble s des sommets du graphe a = ((0,1), (0,2), (1,3), (2,3), (3,4)) # Ensemble a des arêtes g = (s, a) # Graphe défini comme un couple

...CORRECTION...

{0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3] }

06° Lancer le programme puis expliquer comment fonctionne cette fonction en fournissant un commentaire sur les lignes de la fonction.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
"""Implémentation du parcours en largeur""" # Fonctions ===================================================== def transtyper_graphe_tuple_en_dict(g:tuple) -> 'dict[int, list]': """Renvoie un graphe-dico à partir du graphe-couple (non orienté, non pondéré)""" s, a = g graphe = {} for sommet in s: graphe[sommet] = [] for arete in a: s1, s2 = arete graphe[s1].append(s2) graphe[s2].append(s1) return graphe # Programme principal ================================================= if __name__ == "__main__": s = (0, 1, 2, 3, 4) # Ensemble s des sommets du graphe a = ((0,1), (0,2), (1,3), (2,3), (3,4)) # Ensemble a des arêtes g = (s, a) # Graphe défini comme un couple graphe = transtyper_graphe_tuple_en_dict(g) print("Graphe en tant que dictionnaire :") print(graphe)

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
"""Implémentation du parcours en largeur""" # Fonctions ===================================================== def transtyper_graphe_tuple_en_dict(g:tuple) -> 'dict[int, list]': """Renvoie un graphe-dico à partir du graphe-couple (non orienté, non pondéré)""" s, a = g # Désempaquetage de s et a depuis g graphe = {} # Dictionnaire vide for sommet in s: # pour chaque sommet de l'ensemble des sommets graphe[sommet] = [] # on crée une clé correspond au sommet menant à une liste vide for arete in a: # pour chaque arête présente dans l'ensemble a s1, s2 = arete # Désempaquetage de s1 et s2 depuis le couple arete graphe[s1].append(s2) # Rajout de s2 dans la liste d'adj. de s1 graphe[s2].append(s1) # Rajout de s1 dans la liste d'adj. de s2 return graphe # Programme principal ================================================= if __name__ == "__main__": s = (0, 1, 2, 3, 4) # Ensemble s des sommets du graphe a = ((0,1), (0,2), (1,3), (2,3), (3,4)) # Ensemble a des arêtes g = (s, a) # Graphe défini comme un couple graphe = transtyper_graphe_tuple_en_dict(g) print("Graphe en tant que dictionnaire :") print(graphe)

2 - Exploration naïve : toujours la première à gauche !

Avant de réellement explorer les graphes de façon réfléchie, voyons quelques programmes permettant de mimer deux comportements typiques d'un humain perdu dans un labyrinthe :

  • Celui qui tente de suivre un mécanisme répétitif, comme partir toujours à gauche ;
  • Celui qui panique : c'est l'exploration aléatoire, on prend le couloir suivant et on avance.

Nous allons voir que ces deux techniques ne fonctionnent pas vraiment, surtout sur les grands graphes.

Commençons par la technique connue : on prend toujours la première à gauche. Nous allons commencer par une version bien plus simple : on prend toujours le premier sommet dans la liste des successeurs possibles.

07° Compléter le programme pour permettre l'exploration du graphe en explorant systématiquement le premier embranchement qui vient : c'est à dire celui le plus à gauche dans la liste d'adjacence du sommet actif : l'indice 0.

Lancer le programme puis expliquer pourquoi il ne permet pas de résoudre notre problème.

Exploration d'un graphe
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
def choisir_sommet(graphe:dict, actif:str) -> str: """Renvoie l'étiquette de la première destination qu'on trouve partant du sommet actif""" successeurs = ... return successeurs[...] def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche naïve""" actif = ... # Sommet actif, le départ au début while actif != ...: print(f"Position actuelle : {actif}") input("ENTREE pour le mouvement suivant") actif = choisir_sommet(graphe, actif) print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j'] marche_avant(graphe, 'd', 'i')

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
def choisir_sommet(graphe:dict, actif:str) -> str: """Renvoie l'étiquette de la première destination qu'on trouve partant du sommet actif""" successeurs = graphe[actif] return successeurs[0] def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche naïve""" actif = depart # Sommet actif, le départ au début while actif != sortie: print(f"Position actuelle : {actif}") input("ENTREE pour le mouvement suivant") actif = choisir_sommet(graphe, actif) print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j'] marche_avant(graphe, 'd', 'i')

08° On ne veut plus faire d'aller-retour type ping-pong : si on va de A vers B, on ne veut pas repartir directement de B vers A, sauf si on a pas le choix (cul de sac).

Actions à réaliser

  • Lire les explications ci-dessous et Aller voir les modifications qui correspondent dans marche_avant() :
  • Explications

    Le principe de la "marche avant sans aller-retour" est le suivant :

    1. L19 : mémoriser dans avant_dpct le sommet actif qu'on va quitter ;
    2. L20 : modifier le sommet actif à l'aide de la fonction choisir_sommet() ;
    3. L21 : mettre jour le sommet prédécesseur pred, celui d'où on provient, en y plaçant le sommet avant_dpct.

    Cela devrait empêcher l'ordinateur de choisir comme destination le sommet d'où on vient tout juste d'arriver.

  • Compléter la fonction choisir_sommet() pour obtenir le résultat voulu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
def choisir_sommet(graphe:dict, actif:str, pred:str) -> str: """Renvoie l'étiquette de la première destination disponible""" successeurs = graphe[actif] for ...: # Pour chaque sommet dans la liste d'adjacence reçue if ...: # Si ce sommet n'est pas celui d'où on vient ... # Renvoyer ce sommet return pred # Sinon, c'est un cul de sac, on repart d'où on vient... def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche (un peu moins) naïve""" actif = depart pred = "" while actif != sortie: print(f"Position actuelle : {actif} en venant de {pred}") input("ENTREE pour effectuer le mouvement suivant") avant_dpct = actif actif = choisir_sommet(graphe, actif, pred) pred = avant_dpct print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j'] marche_avant(graphe, 'd', 'i')

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
def choisir_sommet(graphe:dict, actif:str, pred:str) -> str: """Renvoie l'étiquette de la première destination disponible""" successeurs = graphe[actif] for sommet in successeurs: # Pour chaque sommet dans la liste d'adjacence reçue if sommet != pred: # Si ce sommet n'est pas celui d'où on vient return sommet # Renvoyer ce sommet return pred # Sinon, c'est un cul de sac, on repart d'où on vient... def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche (un peu moins) naïve""" actif = depart pred = "" while actif != sortie: print(f"Position actuelle : {actif} en venant de {pred}") input("ENTREE pour effectuer le mouvement suivant") avant_dpct = actif actif = choisir_sommet(graphe, actif, pred) pred = avant_dpct print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j'] marche_avant(graphe, 'd', 'i')

Ca ne fonctionne toujours pas vraiment. On ne mémorise que le dernier endroit d'où on vient, ce n'est visiblement pas suffisant...

Pour gérer un humain décidant de prendre systématiquement à gauche, voici ce nouveau programme (qu'il ne faudra pas analyser mais juste utiliser).

09° Lancer ce nouveau programme pour vérifier qu'il correspond bien à un humain qui tourne toujours à gauche lorsqu'il le peut.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
def choisir_sommet(graphe:dict, actif:str, pred:str) -> str: """Renvoie l'étiquette de la première destination disponible""" successeurs = graphe[f"{actif} depuis {pred}"] for sommet in successeurs: # Pour chaque sommet dans la liste d'adjacence reçue if sommet != pred: # Si ce sommet n'est pas celui d'où on vient return sommet # Renvoyer ce sommet return pred # Sinon, c'est un cul de sac, on repart d'où on vient... def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche (un peu moins) naïve""" actif = depart pred = "" while actif != sortie: print(f"Position actuelle : {actif} en venant de {pred}") input("ENTREE pour effectuer le mouvement suivant") avant_dpct = actif actif = choisir_sommet(graphe, actif, pred) pred = avant_dpct print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a depuis '] = ['c', 'b', 'd'] graphe['a depuis d'] = ['c', 'b', 'd'] # liste d'adjacence de a si on provient de d graphe['a depuis b'] = ['d', 'c', 'b'] graphe['a depuis c'] = ['b', 'd', 'c'] graphe['b depuis '] = ['c', 'i', 'a'] graphe['b depuis a'] = ['c', 'i', 'a'] graphe['b depuis c'] = ['i', 'a', 'c'] graphe['b depuis i'] = ['a', 'c', 'i'] graphe['c depuis '] = ['b', 'a'] graphe['c depuis a'] = ['b', 'a'] graphe['c depuis b'] = ['a', 'b'] graphe['d depuis '] = ['f', 'e', 'a'] graphe['d depuis a'] = ['f', 'e', 'a'] graphe['d depuis f'] = ['e', 'a', 'd'] graphe['d depuis e'] = ['a', 'f', 'e'] graphe['e depuis '] = ['h', 'd'] graphe['e depuis d'] = ['h', 'd'] graphe['e depuis h'] = ['h', 'd'] graphe['f depuis '] = ['d', 'g'] graphe['f depuis g'] = ['d', 'g'] graphe['f depuis d'] = ['g', 'd'] graphe['g depuis '] = ['f', 'h'] graphe['g depuis h'] = ['f', 'h'] graphe['g depuis f'] = ['h', 'f'] graphe['h depuis '] = ['e', 'g', 'k', 'j'] graphe['h depuis j'] = ['e', 'g', 'k', 'j'] graphe['h depuis g'] = ['k', 'j', 'e', 'g'] graphe['h depuis e'] = ['g', 'k', 'j', 'e'] graphe['h depuis k'] = ['j', 'e', 'g', 'k'] graphe['i depuis '] = ['b'] graphe['i depuis b'] = ['b'] graphe['j depuis '] = ['h', 'k'] graphe['j depuis k'] = ['h', 'k'] graphe['j depuis h'] = ['k', 'h'] graphe['k depuis '] = ['h', 'j'] graphe['k depuis j'] = ['h', 'j'] graphe['k depuis h'] = ['j', 'h'] marche_avant(graphe, 'd', 'i')
Exploration d'un graphe

10° A première vue, cela fonctionne. Oui, mais c'est parce que le graphe choisi est bien choisi. Lancer ce nouveau programme. La seule différence : une liaison entre c et e.

Exploration d'un graphe avec c-e en plus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
def choisir_sommet(graphe:dict, actif:str, pred:str) -> str: """Renvoie l'étiquette de la première destination disponible""" successeurs = graphe[f"{actif} depuis {pred}"] for sommet in successeurs: # Pour chaque sommet dans la liste d'adjacence reçue if sommet != pred: # Si ce sommet n'est pas celui d'où on vient return sommet # Renvoyer ce sommet return pred # Sinon, c'est un cul de sac, on repart d'où on vient... def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche (un peu moins) naïve""" actif = depart pred = "" while actif != sortie: print(f"Position actuelle : {actif} en venant de {pred}") input("ENTREE pour effectuer le mouvement suivant") avant_dpct = actif actif = choisir_sommet(graphe, actif, pred) pred = avant_dpct print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a depuis '] = ['c', 'b', 'd'] graphe['a depuis d'] = ['c', 'b', 'd'] # liste d'adjacence de a si on provient de d graphe['a depuis b'] = ['d', 'c', 'b'] graphe['a depuis c'] = ['b', 'd', 'c'] graphe['b depuis '] = ['c', 'i', 'a'] graphe['b depuis a'] = ['c', 'i', 'a'] graphe['b depuis c'] = ['i', 'a', 'c'] graphe['b depuis i'] = ['a', 'c', 'i'] graphe['c depuis '] = ['b', 'a'] graphe['c depuis a'] = ['e', 'b', 'a'] graphe['c depuis b'] = ['a', 'e', 'b'] graphe['c depuis e'] = ['b', 'a', 'e'] graphe['d depuis '] = ['f', 'e', 'a'] graphe['d depuis a'] = ['f', 'e', 'a'] graphe['d depuis f'] = ['e', 'a', 'd'] graphe['d depuis e'] = ['a', 'f', 'e'] graphe['e depuis '] = ['h', 'c', 'd'] graphe['e depuis d'] = ['h', 'c', 'd'] graphe['e depuis h'] = ['c', 'h', 'd'] graphe['e depuis c'] = ['d', 'h', 'c'] graphe['f depuis '] = ['d', 'g'] graphe['f depuis g'] = ['d', 'g'] graphe['f depuis d'] = ['g', 'd'] graphe['g depuis '] = ['f', 'h'] graphe['g depuis h'] = ['f', 'h'] graphe['g depuis f'] = ['h', 'f'] graphe['h depuis '] = ['e', 'g', 'k', 'j'] graphe['h depuis j'] = ['e', 'g', 'k', 'j'] graphe['h depuis g'] = ['k', 'j', 'e', 'g'] graphe['h depuis e'] = ['g', 'k', 'j', 'e'] graphe['h depuis k'] = ['j', 'e', 'g', 'k'] graphe['i depuis '] = ['b'] graphe['i depuis b'] = ['b'] graphe['j depuis '] = ['h', 'k'] graphe['j depuis k'] = ['h', 'k'] graphe['j depuis h'] = ['k', 'h'] graphe['k depuis '] = ['h', 'j'] graphe['k depuis j'] = ['h', 'j'] graphe['k depuis h'] = ['j', 'h'] marche_avant(graphe, 'd', 'i')

11° Tentons une dernière différence : démarrons plutôt en a par exemple.

Modifier uniquement la dernière ligne :

69
marche_avant(graphe, 'a', 'i')

Alors, est-ce que la technique de la main gauche (ou droite si vous préférez) fonctionne à chaque fois ?

Mémoriser uniquement le dernier sommet dont on provient ne permet pas d'éviter les cycles ou les circuits.

Nous allons voir dans la partie algorithmique qu'il faut mémoriser tous les sommets rencontrés et manipuler une Pile (en profondeur) ou une File en (en largeur).

3 - Exploration naïve : marche aléatoire

Puisqu'avancer en suivant un mécanisme naïf "tourne à gauche dès que tu le peux" ne fonctionne pas à chaque fois, passons à autre chose : l'exploration aléatoire. Voyons si paniquer et courir permet d'avoir une chance de sortir d'un labyrinthe.

A chaque fois qu'on doit choisir où aller, on décide de prendre une des arêtes au hasard. Si on peut, on ne prend pas celle d'où on vient, sauf si c'est la seule possibilité.

12° Compléter ce programme pour choisir la prochaine destination au hasard, sans revenir directement sur nos pas. Sauf si on a le choix car c'est la seule destination.

  • Si on ne peut plus avancer, on décide de lever une exception.
  • Si on n'a qu'un seul choix, on le prend.
  • Sinon, on tire l'un des sommets au hasard tant qu'on obtient le sommet pred d'où on provient.

Le programme implémente ce graphe :

Exploration d'un graphe

La seule différence provient de la fonction de choix : cette fois, on veut que cela soit aléatoire.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
from random import randint def choisir_sommet(graphe:dict, actif:str, pred:str) -> str: """Renvoie l'étiquette d'un sommet tiré aléatoirement dans la liste d'adjacence""" successeurs = ... assert len(successeurs) > 0, "Bloqué : on ne peut plus avancer !" if len(successeurs) == 1: ... else: i = randint(0, len(successeurs)-1) while ... == pred: ... return successeurs[i] def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche (un peu moins) naïve""" actif = depart pred = "" while actif != sortie: print(f"Position actuelle : {actif} en venant de {pred}") input("ENTREE pour effectuer le mouvement suivant") avant_dpct = actif actif = choisir_sommet(graphe, actif, pred) pred = avant_dpct print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j'] marche_avant(graphe, 'd', 'i')

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
from random import randint def choisir_sommet(graphe:dict, actif:str, pred:str) -> str: """Renvoie l'étiquette d'un sommet tiré aléatoirement dans la liste d'adjacence""" successeurs = graphe[actif] assert len(successeurs) > 0, "Bloqué : on ne peut plus avancer !" if len(successeurs) == 1: return successeurs[0] else: i = randint(0, len(successeurs)-1) while successeurs[i] == pred: i = randint(0, len(successeurs)-1) return successeurs[i] def marche_avant(graphe:dict, depart:str, sortie:str) -> None: """Simule une marche (un peu moins) naïve""" actif = depart pred = "" while actif != sortie: print(f"Position actuelle : {actif} en venant de {pred}") input("ENTREE pour effectuer le mouvement suivant") avant_dpct = actif actif = choisir_sommet(graphe, actif, pred) pred = avant_dpct print(f"Bravo, vous êtes en {actif}") # Programme principal graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j'] marche_avant(graphe, 'd', 'i')

13° Est-ce viable si les très gros graphes ?

...CORRECTION...

Nous sommes en marche aléatoire. Avec beaucoup de temps, on pourra bien entendu tester toutes les possibilités, sauf que certaines auront certainement été testées plusieurs fois.

Il s'agit bien entendu d'une très mauvaise idée d'agir ainsi dès que le nombre de sommets devient un peu important.

Le but de ces deux programmes mimant des comportements courants était de vous montrer que les algorithmes sur les graphes sont nécessaires à partir du moment où on désire explorer sérieusement ces structures.

4 - Visualisation avec Pyxel

Tentons de voir cela avec Pyxel plutôt qu'à travers la console.

Quelques explications.

Dans Pyxel, on lance automatiquement un appel à deux fonctions toutes les trentièmes de seconde :

  • controler() va nous permettre de gérer les événements (le contrôleur) et modifier les données (le modèle).
  • afficher() va nous permettre de mettre l'affichage à jour (la vue).
Le problème de ces deux fonctions est qu'on ne peut pas leur fournir d'arguments (pas avec les connaissances officielles de NSI du moins). Nous allons donc utiliser trois variables globales.

Voici un descriptif des 3 variables globales.

  • graph est un dict qui encode les sommets et les arêtes de notre graphe non orienté.
  • info est un dict qui contient les informations nécessaires pour réaliser le parcours : d'où part-on, quel est le sommet de sortie, quel est le dernier sommet visité, combien de déplacements a-t-on déjà effectué...
  • vue est un dict qui contient les informations liées à l'affichage des sommets : la clé est l'étiquette du sommet et la valeur un tableau à deux cases : [x, y].

14° Utiliser ce programme utilisant le module Pyxel pour voir le parcours aléatoire dans votre graphe.

Graphe et Pyxel

Ca ne sert à rien mais cela vous permettra d'avoir un petit exemple d'affichage graphique basé sur les données d'un graphe.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
'''Les données sont contenus dans 3 dictionnaires globaux graphe : le dict contenant les données du graphe info : le dict contenant les informations sur le parcours en cours vue : le dict contenant les données d'affichage ''' # Importations ----------------------------------------------------- from random import randint import pyxel # Constantes et variables globales -------------------------------- ACTIF = 7 INACTIF = 1 SORTIE = 2 # Sommets et arêtes du graphe graphe = {} graphe['a'] = ['b', 'c', 'd'] graphe['b'] = ['a', 'c', 'i'] graphe['c'] = ['a', 'b'] graphe['d'] = ['a', 'f', 'e'] graphe['e'] = ['d', 'h'] graphe['f'] = ['d', 'g'] graphe['g'] = ['f', 'h'] graphe['h'] = ['e', 'g', 'j', 'k'] graphe['i'] = ['b'] graphe['j'] = ['h', 'k'] graphe['k'] = ['h', 'j'] # Infos sur le parcours info = {'depart':'d', 'sortie':'i', # sommet de départ et d'arrivée 'actif':'', # sommet actuellement actif 'pred':'', # sommet prédécesseur de l'actif 'n':0, # nombre de déplacements 'etat':1 # 1 en cours, 0 } info['actif'] = info['depart'] # Données d'affichage vue = {} vue['a'] = [3, 2] # [x, y] vue['b'] = [2, 1] vue['c'] = [2, 3] vue['d'] = [4, 2] vue['e'] = [5, 3] vue['f'] = [5, 1] vue['g'] = [6, 1] vue['h'] = [7, 1] vue['i'] = [1, 2] vue['j'] = [6, 3] vue['k'] = [8, 3] # Fonctions du MODELE ---------------------------------------------- def choisir_sommet() -> str: """Renvoie l'étiquette d'un sommet tiré aléatoirement dans la liste d'adjacence""" successeurs = graphe[ info["actif"] ] assert len(successeurs) > 0, "Bloqué : on ne peut plus avancer !" if len(successeurs) == 1: return successeurs[0] else: i = randint(0, len(successeurs)-1) while successeurs[i] == info["pred"]: i = randint(0, len(successeurs)-1) return successeurs[i] def etape_suivante() -> None: """Simule une étape de la marche""" if info['actif'] != info['sortie']: avant_dpct = info['actif'] info['actif'] = choisir_sommet() info['pred'] = avant_dpct info['n'] = info['n'] + 1 else: info['etat'] = 0 # Fonction de la partie CONTROLEUR ---------------------------------- def controler(): """Récupère l'événement et gère les données (30 fois par seconde)""" # on lance un déplacement chaque 0.5s puisqu'on compte 15 images if pyxel.frame_count >= 60: # On attend 2s avant de démarrer if info['etat'] != 0 and pyxel.frame_count % 15 == 0: etape_suivante() # Fonctions de la VUE ---------------------------------------------- def coords_en_px(x:int,y:int) -> tuple: """Convertit les valeurs de x et y stockées dans vue en pixels""" return (x*15-5, y*30+20) def afficher_aretes(): """Gestion de l'affichage des arêtes""" for sommet, adjacents in graphe.items(): x1, y1 = coords_en_px(vue[sommet][0], vue[sommet][1]) for adjacent in adjacents: x2, y2 = coords_en_px(vue[adjacent][0], vue[adjacent][1]) pyxel.line(x1, y1, x2, y2, 7) def afficher_sommets(): """Gestion de l'affichage des sommets""" actif = info['actif'] etat = info['etat'] sortie = info['sortie'] for cle, xyc in vue.items(): x,y = coords_en_px(xyc[0], xyc[1]) c = INACTIF if cle == actif: c = ACTIF if cle == sortie and etat == 0: c = SORTIE pyxel.circ(x, y, 5, c) pyxel.text(x-1, y-2, cle, 9) def afficher_message(): """Affiche un message en haut de l'écran""" if info['etat'] > 0: pyxel.text(10,10, f"En {info['actif']} et {info['n']} mouvements. ", 1) else: pyxel.text(10,10, f"En {info['actif']} en {info['n']} mouvements. ", 8) def afficher(): """création des objets (30 fois par seconde)""" pyxel.cls(0) # vide la fenetre afficher_aretes() afficher_sommets() afficher_message() # Programme principal ----------------------------------------------- pyxel.init(128, 128, title="Mon premier jeu") pyxel.run(controler, afficher)

Remarque liée à vue : la clé est l'étiquette du sommet concerné et la valeur associée un tableau de 2 cases [x, y]. x y sont des valeurs qui correspondent aux positions relatives des sommets, pas directement en pixels. C'est la fonction coords_en_px() qui se charge de convertir ces valeurs en pixels.

15° Pourquoi la fonction choisir_sommet() n'a-t-elle plus de besoin de recevoir des informations ?

...CORRECTION...

Comme on ne peut pas envoyer d'arguments à controler() et afficher(), on a eu besoin de créer des variables globales.

La fonction choisir_formule peut donc aller directement les lire également.

16° Quelqu'un trouve que le code est plus compréhensible en plaçant les contenus dans des variables plutôt que de taper le dictionnaire et la clé nécessaire. Il décide de modifier également la fonction etape_suivante().

Et c'est le drame : cela ne fonctionne plus !

Version initiale

def etape_suivante() -> None: """Simule une étape de la marche""" if info['actif'] != info['sortie']: avant_dpct = info['actif'] info['actif'] = choisir_sommet() info['pred'] = avant_dpct info['n'] = info['n'] + 1 else: info['etat'] = 0

Version modifiée qui ne fonctionne pas

def etape_suivante() -> None: """Simule une étape de la marche""" actif = info["actif"] pred = info["pref"] sortie = info["sortie"] if actif != sortie: avant_dpct = actif actif = choisir_sommet() pred = avant_dpct info['n'] = info['n'] + 1 else: info['etat'] = 0

Question

D'où vient le problème ?

...CORRECTION...

C'est une erreur typique : on place le contenu d'une valeur de dictionnaire dans une variable. Modifier la valeur ne va pas aller modifier le contenu du dictionnaire.

Le problème vient donc surtout de ces deux lignes :

actif = choisir_sommet() pred = avant_dpct

Elles ne font que modifier les variables locales, pas le contenu du dictionnaire lui-même !

5 - FAQ

Rien pour le moment

Activité publiée le 08 04 2021
Dernière modification : 13 02 2025
Auteur : ows. h.