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.
Un graphe est une structure de données qui comporte des objets appelés "sommets".
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.
Dans un graphe non-orienté :
A et B deux sommets, on note A-B ou
(A,B) une arête qui le relie.A faire dans le cahier.
On considère le graphe ci-dessous :
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 :
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.
Tests :
Affichage :
Console:
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\).
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.
Tests :
Affichage :
Console:
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 :
aretes est un dictionnaire contenant les listes d’adjacence,sommet1 et sommet2 est ajoutée dans les deux sens,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".
Tests :
Affichage :
Console:
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:
Tests :
Affichage :
Console:
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 :
La liste retournée ne doit pas contenir de doublons (l ordre n a pas d importance).
Tests :
Affichage :
Console:
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.
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.
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.
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 :
depart n existe pas, retourner une liste vide.sorted.Exemple:
Sur ce graphe :
Sortie attendue (sur le graphe d exemple) :
parcours_largeur("Alice")
["Alice", "Bob", "Fiona", "Charlie", "Emma", "George", "David", "Ian", ...]
Tests :
Affichage :
Console:
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.
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.
voisins afin qu'il corresponde au graphe.É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)
marche_alea est une fonction récursive.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)
.....
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.
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]
]
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.
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.
Il y a deux implémentations possibles : une récursive et l'autre itérative.
Voici comment cela pourrait être implémenté en pseudo-code :
DFS(graphe, noeud_actuel, liste_noeud_visite):
marquer noeud_actuel comme visité en l'ajoutant à la liste
Traiter noeud_actuel
Pour chaque voisin du noeud_actuel dans le graphe:
Si noeud_actuel n est pas visite:
DFS(graphe, voisin, liste_noeud_visite)
Voici comment cela pourrait être implémenté en pseudo-code :
DFS(graphe, noeud_de_départ):
pile = Pile()
pile.empiler(noeud_de_départ)
On marque le noeud de départ comme étant visité
Tant que la pile n est pas vide:
noeud_actuel = pile.depiler()
Traiter noeud_actuel
Pour chaque voisin du noeud_actuel dans le graphe:
Si voisin est non visité:
pile.empiler(voisin)
On marque voisin comme étant visité
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 :
depart n existe pas, retourner une liste vide.sorted.Exemple :
parcours_profondeur("Entree")
["Entree", "Galerie", "Salle_1", "Salle_2", ...]
Tests :
Affichage :
Console: