Chapitre 18 : Les graphes non orientés.

Introduction :

Les graphes sont des structures de données qui modélisent des relations entre des objets.

Un graphe est composé de nœuds (ou sommets) et de liens (ou arêtes) qui connectent ces nœuds. Ils sont utilisés pour représenter des réseaux, comme des chemins dans un GPS, des connexions sur les réseaux sociaux ou des dépendances dans un système.

1. Définition:

Un graphe est une structure de données qui comporte des objets appelés "sommets".

Les graphes non-orientés :

Les sommets sont reliés entre eux par des arêtes n'ayant pas de sens.

Les arêtes forment des relations entre les sommets.

Un graphe est une structure de données relationnelles.

Les sommets peuvent aussi être appelés des noeuds.

2. Définitions:

Dans un graphe non-orienté :

3. Exercice:

A faire dans le cahier.

On considère le graphe ci-dessous :

  1. Citer deux sommets adjacents.
  2. Citer deux sommets non adjacents.
  3. Citer deux arêtes adjacentes.
  4. Quel est le degré de Emma?
  5. Citer un cycle initalisé en Laura.
  6. Ce graphe est-il complet?
  7. Citer 2 chaînes distinctes reliant Bob et Julia.

4. Stockage du type G = [V, E]:

Il existe plusieurs façons de stocker un graphe non-orienté dans Python, l'une d'elle est la création d'une liste de deux éléments :

5. Exercice : Opérations sur un graphe non orienté

On considère le graphe ci-dessous :

On a implémenté ce graphe à l'aide du code suivant :

graphe = [
    ["Alice", "Bob", "Charlie", "David", "Emma", "Fiona", "George", "Hannah", "Ian", "Julia", "Kevin", "Laura", "Mike", "Nancy", "Olivia", "Paul"],
    [
        ("Alice", "Bob"), 
        ("Bob", "Charlie"), 
        ("Charlie", "David"), 
        ("David", "Emma"), 
        ("Emma", "Fiona"), 
        ("Fiona", "George"), 
        ("George", "Hannah"), 
        ("Hannah", "Ian"), 
        ("Ian", "Julia"), 
        ("Julia", "Kevin"), 
        ("Kevin", "Laura"), 
        ("Laura", "Mike"), 
        ("Mike", "Nancy"), 
        ("Nancy", "Olivia"), 
        ("Olivia", "Paul"),
        ("Alice", "Fiona"), 
        ("Emma", "Ian"), 
        ("David", "Kevin"), 
        ("Hannah", "Nancy")
    ]
]

Complétez les fonctions ci-dessous conformément à leurs documentations.

graphe_exemple = [ ["Alice", "Bob", "Charlie", "David", "Emma", "Fiona", "George", "Hannah", "Ian", "Julia", "Kevin", "Laura", "Mike", "Nancy", "Olivia", "Paul"], [ ("Alice", "Bob"), ("Bob", "Charlie"), ("Charlie", "David"), ("David", "Emma"), ("Emma", "Fiona"), ("Fiona", "George"), ("George", "Hannah"), ("Hannah", "Ian"), ("Ian", "Julia"), ("Julia", "Kevin"), ("Kevin", "Laura"), ("Laura", "Mike"), ("Mike", "Nancy"), ("Nancy", "Olivia"), ("Olivia", "Paul"), ("Alice", "Fiona"), ("Emma", "Ian"), ("David", "Kevin"), ("Hannah", "Nancy") ] ] def nombre_de_sommets(graphe: list) -> int: """ Retourne le nombre de sommets du graphe. """ ... def nombre_d_aretes(graphe: list) -> int: """ Retourne le nombre d'arêtes du graphe. """ ... def degre_sommet(graphe: list, sommet: str) -> int: """ Retourne le degré (nombre d'arêtes incidentes) du sommet donné. """ ... def adjacent_sommets(graphe: list, sommet1: str, sommet2: str) -> bool: """ Retourne True si sommet1 et sommet2 sont reliés par une arête, False sinon. """ ... def ajout_arete(graphe: list, arete: tuple) -> None: """ Ajoute l'arête donnée au graphe. Le graphe est modifié sur place. """ ... def suppression_arete(graphe: list, arete: tuple) -> None: """ Supprime l'arête donnée du graphe (si elle est présente). Le graphe est modifié sur place. Les arêtes peuvent être lues dans les deux sens. """ ... def ajout_sommet(graphe: list, sommet: str) -> None: """ Ajoute un sommet au graphe (s'il n'y est pas déjà). Le graphe est modifié sur place. """ ... def suppression_sommet(graphe: list, sommet: str) -> None: """ Supprime le sommet du graphe ainsi que toutes les arêtes qui l'utilisent. Le graphe est modifié sur place. """ ... # Exemple d'utilisation possible : # print(nombre_de_sommets(graphe_exemple))

Tests :

Affichage :

Console:


    
>>>

6. Matrice d'adjacence:

Il existe plusieurs façons de stocker un graphe non-orienté dans Python, l'une d'elle est la création d'une matrice d'adjacence.

S'il y a \(n\) sommets, on associe à chaque sommet un unique nombre entier de \([0;n-1]\).

On créait alors la matrice de taille \(n \times n\) contenant les valeurs \(a_{i,j}\) avec \(i\) et \(j\) deux nombres entiers appartenant à \([0;n-1]\).

\(a_{i,j}\) correspond au nombre d'arêtes reliant les sommets associés à \(i\) et \(j\).

7. Exercice : Opérations sur un graphe (matrice)

On considère le graphe ci-dessous :

On a implémenté ce graphe à l'aide du dictionnaire et de la matrice ci-dessous :

dico = {'Alice': 0, 'Bob': 1, 'Charlie': 2, 'David': 3, 'Emma': 4, 'Fiona': 5, 'George': 6, 'Hannah': 7, 'Ian': 8, 'Julia': 9, 'Kevin': 10, 'Laura': 11, 'Mike': 12, 'Nancy': 13, 'Olivia': 14, 'Paul': 15}
matrice = [
  [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
  [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
]

Complétez les fonctions ci-dessous conformément à leurs documentations.

dico_exemple = {'Alice': 0, 'Bob': 1, 'Charlie': 2, 'David': 3, 'Emma': 4, 'Fiona': 5, 'George': 6, 'Hannah': 7, 'Ian': 8, 'Julia': 9, 'Kevin': 10, 'Laura': 11, 'Mike': 12, 'Nancy': 13, 'Olivia': 14, 'Paul': 15} matrice_exemple = [ [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0] ] def nombre_de_sommets(dico: dict, matrice: list) -> int: """ Retourne le nombre de sommets du graphe. On pourra utiliser dico ou matrice. """ ... def nombre_d_aretes(dico: dict, matrice: list) -> int: """ Retourne le nombre d'arêtes du graphe. Attention : la matrice d'adjacence est symétrique. """ ... def degre_sommet(dico: dict, matrice: list, sommet: str) -> int: """ Retourne le degré du sommet (nombre d'arêtes incidentes). On utilisera le dictionnaire pour trouver l'indice du sommet. """ ... def adjacent_sommets(dico: dict, matrice: list, sommet1: str, sommet2: str) -> bool: """ Retourne True si sommet1 et sommet2 sont adjacents, False sinon. """ ... def ajout_arete(dico: dict, matrice: list, arete: tuple) -> None: """ Ajoute une arête dans la matrice (graphe non orienté). L'arête est un tuple (sommet1, sommet2). Modifier la matrice sur place. """ ... def suppression_arete(dico: dict, matrice: list, arete: tuple) -> None: """ Supprime une arête dans la matrice (graphe non orienté). L'arête est un tuple (sommet1, sommet2). Modifier la matrice sur place. """ ... # Exemple d'utilisation possible : # print(nombre_de_sommets(dico_exemple, matrice_exemple))

Tests :

Affichage :

Console:


    
>>>

8. Implémentation d’un graphe non orienté

Une manière simple de représenter un graphe non orienté en Python est d’utiliser un dictionnaire d’adjacence :

class GrapheNonOriente:
    def __init__(self):
        self.aretes = {}

    def ajouter_sommet(self, sommet):
        if sommet not in self.aretes:
            self.aretes[sommet] = []

    def ajouter_arete(self, sommet1, sommet2):
        self.ajouter_sommet(sommet1)
        self.ajouter_sommet(sommet2)
        self.aretes[sommet1].append(sommet2)
        self.aretes[sommet2].append(sommet1)

Dans cette implémentation :

9. Exercice : Construire un graphe de serveurs

On considère un graphe non orienté représentant un réseau de serveurs, avec les arêtes suivantes :

On utilise la classe GrapheNonOriente ci-dessous.

Écrire les lignes de code permettant de créer une instance nommée reseau qui implémente exactement ce graphe (mêmes sommets et mêmes arêtes).

Les sommets sont représentés par des chaînes de caractères : "Serveur1", "Serveur2", "Serveur3" et "Serveur4".

class GrapheNonOriente: def __init__(self): self.aretes = {} def ajouter_sommet(self, sommet): if sommet not in self.aretes: self.aretes[sommet] = [] def ajouter_arete(self, sommet1, sommet2): self.ajouter_sommet(sommet1) self.ajouter_sommet(sommet2) self.aretes[sommet1].append(sommet2) self.aretes[sommet2].append(sommet1) # Creer une instance nommee reseau et ajouter les aretes du reseau reseau = ... ... # Affichage de verification (optionnel) print(reseau.aretes) serveur = "Serveur2" print(f"Le {serveur} est relié aux serveurs suivants: {', '.join(reseau.aretes[serveur])}.")

Tests :

Affichage :

Console:


    
>>>

10. Exercice : Méthodes d un graphe non orienté

On utilise une classe GrapheNonOriente pour représenter un graphe non orienté avec une structure de listes d'adjacences.

Complétez les méthodes demandées en respectant strictement les documentations.

Un graphe d exemple sera construit à partir d une liste d arêtes, puis des print permettront de visualiser les résultats:

class GrapheNonOriente: def __init__(self) -> None: self.aretes = {} def ajouter_sommet(self, sommet: str) -> None: if sommet not in self.aretes: self.aretes[sommet] = [] def ajouter_arete(self, sommet1: str, sommet2: str) -> None: self.ajouter_sommet(sommet1) self.ajouter_sommet(sommet2) self.aretes[sommet1].append(sommet2) self.aretes[sommet2].append(sommet1) def liste_sommets(self) -> list: """ Retourne la liste des sommets du graphe. Retour : - list[str] : une liste contenant tous les sommets (ordre quelconque) Exemple : - si le graphe contient Alice, Bob, Charlie, la methode peut renvoyer ["Bob", "Alice", "Charlie"]. """ ... def sommets_adjacents(self, sommet1: str, sommet2: str) -> bool: """ Indique si deux sommets sont adjacents (relies par une arete). Parametres : - sommet1 (str) - sommet2 (str) Retour : - bool : True si sommet2 est dans la liste des voisins de sommet1, False sinon. Remarque : - si sommet1 n existe pas dans le graphe, on renvoie False. """ ... def nb_sommets(self) -> int: """ Retourne le nombre de sommets du graphe. Retour : - int : nombre de sommets """ ... def nb_aretes(self) -> int: """ Retourne le nombre d aretes du graphe (graphe non oriente). Important : - Dans self.aretes, chaque arete (u, v) apparait deux fois : v est dans la liste de u ET u est dans la liste de v. - Il faut donc compter correctement les aretes sans les doubler. Aide : - Completez le code ci dessous. - total_voisins correspond au total des longueurs des listes d adjacency. - Le resultat final est a deduire de total_voisins. """ total_voisins = 0 for sommet in self.aretes: total_voisins = total_voisins + ... return ... def degre_sommet(self, sommet: str) -> int: """ Retourne le degre d un sommet (nombre de voisins). Parametre : - sommet (str) Retour : - int : degre du sommet - si le sommet n existe pas, renvoyer 0. """ ... # Construction d un graphe d exemple aretes_exemple = [ ("Alice", "Bob"), ("Bob", "Charlie"), ("Charlie", "David"), ("David", "Emma"), ("Emma", "Fiona"), ("Fiona", "George"), ("George", "Hannah"), ("Hannah", "Ian"), ("Ian", "Julia"), ("Julia", "Kevin"), ("Kevin", "Laura"), ("Laura", "Mike"), ("Mike", "Nancy"), ("Nancy", "Olivia"), ("Olivia", "Paul"), ("Alice", "Fiona"), ("Emma", "Ian"), ("David", "Kevin"), ("Hannah", "Nancy") ] graphe_exemple = GrapheNonOriente() for a in aretes_exemple: graphe_exemple.ajouter_arete(a[0], a[1]) # Prints de visualisation print("Sommets :", sorted(graphe_exemple.liste_sommets())) print("Nombre de sommets :", graphe_exemple.nb_sommets()) print("Nombre d aretes :", graphe_exemple.nb_aretes()) print("Alice adjacent Bob :", graphe_exemple.sommets_adjacents("Alice", "Bob")) print("Bob adjacent Emma :", graphe_exemple.sommets_adjacents("Bob", "Emma")) print("Degre de David :", graphe_exemple.degre_sommet("David")) print("Degre de Paul :", graphe_exemple.degre_sommet("Paul"))

Tests :

Affichage :

Console:


    
>>>

11. Exercice : Voisin du voisin

On considère un graphe non orienté représenté par une classe GrapheNonOriente.

Écrire la fonction voisin_du_voisin(graphe, sommet_depart: str) -> list qui retourne la liste des sommets qui sont exactement à une distance de deux du sommet donné.

Autrement dit, ce sont les sommets atteignables en deux arêtes, mais qui ne sont :

  • ni le sommet de départ,
  • ni des voisins directs (distance 1).

La liste retournée ne doit pas contenir de doublons (l ordre n a pas d importance).

class GrapheNonOriente: def __init__(self) -> None: self.aretes = {} def ajouter_sommet(self, sommet: str) -> None: if sommet not in self.aretes: self.aretes[sommet] = [] def ajouter_arete(self, sommet1: str, sommet2: str) -> None: self.ajouter_sommet(sommet1) self.ajouter_sommet(sommet2) self.aretes[sommet1].append(sommet2) self.aretes[sommet2].append(sommet1) def voisin_du_voisin(graphe: GrapheNonOriente, sommet_depart: str) -> list: """ Retourne la liste des sommets qui sont exactement a distance 2 du sommet donne. Definitions : - Voisins directs (distance 1) : sommets presents dans graphe.aretes[sommet_depart] - Distance 2 : sommets atteignables en deux aretes (sommet_depart -> voisin -> x) Contraintes : - Ne pas inclure le sommet de depart. - Ne pas inclure les voisins directs. - Ne pas mettre de doublons. - Si le sommet n existe pas dans le graphe, renvoyer une liste vide. Retour : - list[str] : liste (ordre quelconque) des sommets a distance 2. """ ... # Graphe d exemple aretes_exemple = [ ("Alice", "Bob"), ("Bob", "Charlie"), ("Charlie", "David"), ("David", "Emma"), ("Emma", "Fiona"), ("Fiona", "George"), ("George", "Hannah"), ("Hannah", "Ian"), ("Ian", "Julia"), ("Julia", "Kevin"), ("Kevin", "Laura"), ("Laura", "Mike"), ("Mike", "Nancy"), ("Nancy", "Olivia"), ("Olivia", "Paul"), ("Alice", "Fiona"), ("Emma", "Ian"), ("David", "Kevin"), ("Hannah", "Nancy") ] graphe_exemple = GrapheNonOriente() for a in aretes_exemple: graphe_exemple.ajouter_arete(a[0], a[1]) # Prints de visualisation print("Voisins de Alice :", graphe_exemple.aretes["Alice"]) print("Voisin du voisin de Alice :", sorted(voisin_du_voisin(graphe_exemple, "Alice"))) print("Voisin du voisin de David :", sorted(voisin_du_voisin(graphe_exemple, "David"))) print("Voisin du voisin de Paul :", sorted(voisin_du_voisin(graphe_exemple, "Paul")))

Tests :

Affichage :

Console:


    
>>>

12. Parcours en largeur d'un graphe (BFS - Breadth-First Search)

Le parcours en largeur est un algorithme utilisé pour explorer de manière systématique un graphe. Il commence à partir d'un noeud source donné, explore tous les noeuds voisins de ce noeud, puis passe aux voisins de ces voisins, et ainsi de suite. Cette méthode garantit que les noeuds sont visités dans l'ordre de leur distance par rapport au noeud source. L'algorithme BFS est particulièrement utile pour résoudre des problèmes de connectivité, la recherche du chemin le plus court et d'autres problèmes liés aux graphes.

Exemple :

Prenons un exemple simple pour illustrer le fonctionnement de BFS. Imaginons le graphe suivant :

Si nous utilisons BFS à partir du noeud source A, l'ordre de visite des noeuds sera : A, B, C, D. L'algorithme commence par le noeud source A, visite ensuite les noeuds B et C (qui sont les voisins directs de A), puis visite le noeud D, voisin de B.

Applications :

Implémentation :

L'implémentation de l'algorithme BFS nécessite généralement l'utilisation d'une file (queue). Voici un exemple en pseudo-code :

BFS(graphe, noeud_de_départ):
    file = File()
    file.enfiler(noeud_de_départ)
    Marquer noeud_de_départ comme visité
    Tant que la file n est pas vide:
        noeud_actuel = file.defiler()
        Traiter noeud_actuel
        Pour chaque voisin du noeud_actuel dans le graphe:
            Si il est non visité:
                file.enfiler(voisin)
                Marquer voisin comme visité

Point important à retenir : il faut marquer quand on enfile et non quand on défile.

13. Exercice : Parcours en largeur

On considère un graphe non orienté représenté par une classe GrapheNonOriente.

Compléter la méthode parcours_largeur(self, depart: str) -> list qui effectue un parcours en largeur (BFS) à partir du sommet depart.

La méthode doit retourner la liste des sommets visités dans l ordre du parcours.

Règles à respecter :

  • Si depart n existe pas, retourner une liste vide.
  • On utilise une file (queue) : on retire en tête et on ajoute en fin.
  • Pour rendre le résultat déterministe, on parcourt les voisins dans l'ordre alphabétique grâce à la commande sorted.
  • On ne visite jamais deux fois le même sommet.

Exemple:

Sur ce graphe :

Sortie attendue (sur le graphe d exemple) :

parcours_largeur("Alice")
["Alice", "Bob", "Fiona", "Charlie", "Emma", "George", "David", "Ian", ...]
class File: def __init__(self): self.f = [] def enfiler(self, data): self.f.append(data) def defiler(self): assert len(self.f) > 0, "On ne peut pas defiler un élément d une file vide." return self.f.pop(0) def est_vide(self): return len(self.f) == 0 class GrapheNonOriente: def __init__(self) -> None: """ Initialise un graphe non orienté. Représentation : - self.aretes : dict[str, list[str]] * chaque clé est un sommet * chaque valeur est la liste des voisins """ self.aretes = {} def ajouter_sommet(self, sommet: str) -> None: """ Ajoute un sommet au graphe s il n existe pas déjà. """ if sommet not in self.aretes: self.aretes[sommet] = [] def ajouter_arete(self, sommet1: str, sommet2: str) -> None: """ Ajoute une arête non orientée entre sommet1 et sommet2. """ self.ajouter_sommet(sommet1) self.ajouter_sommet(sommet2) self.aretes[sommet1].append(sommet2) self.aretes[sommet2].append(sommet1) def parcours_largeur(self, depart: str) -> list: """ Effectue un parcours en largeur (BFS) a partir de depart. Retour : - list[str] : liste des sommets visites dans l ordre du parcours. Contraintes : - si depart n existe pas, renvoyer []. - utiliser une file (queue) : * defiler en tete * enfiler en fin - ne pas visiter deux fois le meme sommet. - pour un resultat deterministe : * a chaque etape, traiter les voisins dans l ordre alphabetique avec la commande sorted. """ ... # Graphe d exemple aretes_exemple = [ ("Alice", "Bob"), ("Bob", "Charlie"), ("Charlie", "David"), ("David", "Emma"), ("Emma", "Fiona"), ("Fiona", "George"), ("George", "Hannah"), ("Hannah", "Ian"), ("Ian", "Julia"), ("Julia", "Kevin"), ("Kevin", "Laura"), ("Laura", "Mike"), ("Mike", "Nancy"), ("Nancy", "Olivia"), ("Olivia", "Paul"), ("Alice", "Fiona"), ("Emma", "Ian"), ("David", "Kevin"), ("Hannah", "Nancy") ] g = GrapheNonOriente() for (u, v) in aretes_exemple: g.ajouter_arete(u, v) # Prints de visualisation print("Voisins de Alice :", sorted(g.aretes["Alice"])) print("Parcours largeur depuis Alice :", g.parcours_largeur("Alice")) print("Parcours largeur depuis Paul :", g.parcours_largeur("Paul")) print("Parcours largeur depuis Inconnu :", g.parcours_largeur("Inconnu"))

Tests :

Affichage :

Console:


    
>>>

14. Exercice type bac :

A faire dans le cahier.

On cherche à lutter contre un virus informatique qui essaie de contourner les protocoles de sécurité en migrant régulièrement vers un autre ordinateur, en choisissant à chaque fois au hasard sa nouvelle cible parmi les ordinateurs accessibles.

On cherche à savoir quels ordinateurs protéger afin de lutter de manière la plus efficace possible avec des ressources limitées.

On considère le réseau informatique suivant, composé de 5 ordinateurs numérotés : 0, 1, 2, 3 et 4.

  1. On représente ce réseau informatique par un graphe que l'on stocke sous forme de listes de voisins. Compléter la définition de la variable voisins.

    voisins = [ [1, 2, 3, 4],
                [0, 2, 3],
                [0, 1],
                [........],
                [.....]]

On ajoute au réseau actuel un sixième ordinateur, numéroté 5. Cet ordinateur n'est accessible que des ordinateurs numérotés 0 et 2.

  1. Dessiner le nouveau graphe sur votre cahier.
  2. Modifier la variable voisins afin qu'il corresponde au graphe.
  3. Écrire la fonction voisin_alea(voisins, s) qui prend en paramètre un graphe voisins sous forme de listes de voisins et un entier s représentant un sommet et qui renvoie un entier représentant un voisin de s choisi aléatoirement.

    On pourra utiliser la fonction random.randrange(n) qui renvoie un nombre aléatoire entre 0 inclus et n exclus.

On donne la fonction marche_alea suivante :

def marche_alea(voisins, i, n):
    if n == 0:
        return i 
    return marche_alea(voisins, voisin_alea(voisins, i), n-1)
  1. Justifier que la fonction marche_alea est une fonction récursive.
  2. Décrire ce que modélise cette fonction, en rapport avec le contexte de l'exercice.
  3. Compléter la fonction simule qui simule n_tests fois le déplacement d'un virus pendant n_pas étapes, démarrant au sommet i, et qui renvoie une liste contenant en position j le nombre de fois que le virus a terminé son parcours au sommet j, divisé par n_tests.
    def simule(voisins , i, n_tests, n_pas) :
        results = [0] * len(voisins)
        .....
            
  4. L'appel simule(voisins, 4, 1000, 1000) renvoie la valeur suivante :

    [0.328, 0.195, 0.18, 0.12, 0.059, 0.118].

    Déduire de ce résultat l'ordinateur du réseau qu'il est le plus rentable de protéger.

Au début, on suppose que le virus n'est présent que sur un ordinateur. À chaque étape, il contamine tous ses voisins non déjà contaminés. On cherche à savoir combien de temps prend ce virus pour se propager à tout le réseau.

  1. Un graphe voisins représente un réseau, et s représente un sommet de départ.

    Compléter le programme suivant pour déterminer le temps, en étape, que met un virus à se propager à l'intégralité du réseau.

    def nb_etapes_contamination_totale(voisins, s):
        nb_etapes = 0
        contamination = [False] * len(voisins) # aucun ordinateur n'est infecté
        contamination[s] = True   # l'ordinateur s devient contaminé
        while ............................... :
    
            #Plusieurs lignes
    
        return nb_etapes
            

    On pourra tester notre code sur ce graphe plus grand :

    voisins_nombreux = [
            [1, 2, 3, 4] ,
            [0, 2, 3, 8, 10] ,
            [0, 1, 5, 6] ,
            [0, 1, 9, 12] ,
            [0, 6, 7] ,
            [2, 13, 27] ,
            [2, 4, 7, 14] ,
            [4, 6, 15, 28] ,
            [1, 25] ,
            [3, 26] ,
            [1, 16, 29] ,
            [12, 17] ,
            [3, 11, 18, 30] ,
            [5, 19] ,
            [6, 20] ,
            [7, 21] ,
            [10, 22] ,
            [11, 23] ,
            [12, 24] ,
            [13] ,
            [14] ,
            [15] ,
            [16] ,
            [17, 24] ,
            [18, 23] ,
            [8] ,
            [9] ,
            [5] ,
            [7] ,
            [10] ,
            [12] 
    ]

15. Parcours en profondeur d'un graphe (DFS - Depth-First Search)

Le parcours en profondeur est un algorithme utilisé pour explorer de manière systématique un graphe en suivant une branche jusqu'à ce qu'il atteigne un noeud terminal, puis il revient en arrière pour explorer d'autres branches. L'algorithme DFS est largement utilisé pour la recherche de chemins, la détection de cycles et d'autres problèmes liés aux graphes.

Exemple :

Prenons un exemple simple pour illustrer le fonctionnement de DFS. Imaginons le graphe suivant :

Si nous utilisons DFS à partir du noeud source A, l'ordre de visite des noeuds pourrait être : A, B, D, C. L'algorithme commence par le noeud source A, explore le noeud B, puis explore le noeud D, avant de revenir au noeud C.

Applications :

Implémentation :

Il y a deux implémentations possibles : une récursive et l'autre itérative.

16. Exercice : Parcours en profondeur

On modélise un réseau de salles dans un musée. Chaque salle est un sommet, et une porte entre deux salles est une arête (graphe non orienté).

Le gardien veut suivre une ronde en visitant les salles avec un parcours en profondeur (DFS) à partir d une salle de départ.

Compléter la méthode parcours_profondeur(self, depart: str) -> list qui retourne la liste des salles visitées, dans l ordre du parcours.

Règles à respecter :

  • Si depart n existe pas, retourner une liste vide.
  • On utilise une pile (stack) : on empile et on dépile en fin.
  • Pour rendre le résultat déterministe, on parcourt les voisins dans l'ordre alphabétique avec sorted.
  • On ne visite jamais deux fois le même sommet.

Exemple :

parcours_profondeur("Entree")
["Entree", "Galerie", "Salle_1", "Salle_2", ...]
class Pile: def __init__(self): self.p = [] def empiler(self, data): self.p.append(data) def depiler(self): assert len(self.p) > 0, "On ne peut pas depiler un element d une pile vide." return self.p.pop() def est_vide(self): return len(self.p) == 0 class GrapheNonOriente: def __init__(self) -> None: """ Initialise un graphe non orienté. Représentation : - self.aretes : dict[str, list[str]] * chaque clé est un sommet * chaque valeur est la liste des voisins """ self.aretes = {} def ajouter_sommet(self, sommet: str) -> None: """ Ajoute un sommet au graphe s il n existe pas déjà. """ if sommet not in self.aretes: self.aretes[sommet] = [] def ajouter_arete(self, sommet1: str, sommet2: str) -> None: """ Ajoute une arête non orientée entre sommet1 et sommet2. """ self.ajouter_sommet(sommet1) self.ajouter_sommet(sommet2) self.aretes[sommet1].append(sommet2) self.aretes[sommet2].append(sommet1) def parcours_profondeur(self, depart: str) -> list: """ Effectue un parcours en profondeur (DFS) a partir de depart. Retour : - list[str] : liste des sommets visites dans l ordre du parcours. Contraintes : - si depart n existe pas, renvoyer []. - utiliser une pile : * empiler * depiler - ne pas visiter deux fois le meme sommet. - pour un resultat deterministe : * quand on traite un sommet, on regarde ses voisins dans l ordre alphabetique avec sorted. Indice : - avec une pile, pour respecter l ordre alphabetique dans le parcours, on peut empiler les voisins dans l ordre inverse (sorted(..., reverse=True)). """ ... # Plan du musee (graphe d exemple) portes = [ ("Entree", "Accueil"), ("Accueil", "Galerie"), ("Galerie", "Salle_1"), ("Galerie", "Salle_2"), ("Salle_1", "Salle_3"), ("Salle_2", "Salle_4"), ("Salle_3", "Sortie"), ("Salle_4", "Sortie"), ("Accueil", "Boutique"), ("Boutique", "Sortie") ] musee = GrapheNonOriente() for (a, b) in portes: musee.ajouter_arete(a, b) # Prints de visualisation print("Voisins de Galerie :", sorted(musee.aretes["Galerie"])) print("Parcours profondeur depuis Entree :", musee.parcours_profondeur("Entree")) print("Parcours profondeur depuis Boutique :", musee.parcours_profondeur("Boutique")) print("Parcours profondeur depuis Inconnu :", musee.parcours_profondeur("Inconnu"))

Tests :

Affichage :

Console:


    
>>>