Les graphes : modèle et représentations
Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 12 · prépas scientifiques, tronc commun
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>12.1 Introduction et motivation
Un plan de métro, une carte d'amitiés, le réseau des pages web, les interactions entre protéines d'une cellule : derrière ces objets sans rapport apparent vit une même structure mathématique — des points reliés par des liens. Cette structure s'appelle un graphe, et c'est l'objet conceptuel le plus polyvalent de l'informatique : dès qu'un problème parle de relations, de réseaux, de dépendances ou de déplacements, le réflexe professionnel est de le modéliser par un graphe — première compétence du programme — puis d'appliquer l'arsenal d'algorithmes des deux chapitres à venir.
Les tailles en jeu donnent la mesure de l'enjeu : le graphe du web compte de l'ordre de pages et bien davantage de liens ; un réseau social, comptes et relations ; le réseau routier européen, carrefours ; un réseau d'interactions protéiques, nœuds. À ces échelles, le choix de la représentation en mémoire — listes d'adjacence ou matrice d'adjacence, les deux héros de ce chapitre — n'est pas un détail d'implémentation : c'est lui qui décide si le graphe tient en mémoire, et à quel coût on peut l'interroger.
12.2 Le vocabulaire des graphes
12.2.1 Graphes non orientés, graphes orientés
Un graphe non orienté est la donnée d'un ensemble fini de sommets (on dit aussi nœuds) et d'un ensemble d'arêtes, chaque arête reliant une paire de sommets . L'arête est un lien symétrique : si est relié à , alors est relié à — le modèle des amitiés, des routes à double sens, des liaisons chimiques.
Un graphe orienté a des arcs : des couples ordonnés , représentés par des flèches de vers . Le lien n'implique pas — le modèle des liens hypertextes (la page pointe vers ), des abonnements, des rues à sens unique, des dépendances entre tâches. Une boucle est un arc d'un sommet vers lui-même. (Conformément au programme, on exclut les multi-arcs et multi-arêtes : entre deux sommets, au plus un lien dans chaque sens.)
À gauche, le sommet est isolé : aucun lien ne l'atteint — un graphe n'est pas forcément d'un seul tenant. À droite, le triangle se parcourt dans un seul sens.
12.2.2 Degrés, chemins, cycles, connexité
Dans un graphe non orienté, le degré d'un sommet est le nombre d'arêtes qui le touchent. Dans un graphe orienté, on distingue le degré sortant (nombre d'arcs partant de ) et le degré entrant (nombre d'arcs arrivant en ). Sur les exemples ci-dessus : , ; , . (Une boucle compte dans chacun des deux degrés orientés.)
Dans un graphe non orienté, : chaque arête est comptée par ses deux extrémités. Dans un graphe orienté, . Conséquence amusante et utile : dans toute assemblée, le nombre de personnes ayant serré un nombre impair de mains est pair.
Un chemin d'un sommet à un sommet est une suite de sommets telle que chaque étape soit un arc (ou une arête) du graphe ; est sa longueur (en nombre d'arcs). Un cycle est un chemin de longueur qui revient à son point de départ sans réutiliser d'arête (non orienté) — dans l'exemple : ; en orienté : .
Un graphe non orienté est connexe si tout couple de sommets est relié par un chemin — s'il est « d'un seul tenant ». Sinon, il se découpe en composantes connexes : les îlots maximaux de sommets mutuellement accessibles. Le graphe non orienté de l'exemple n'est pas connexe : deux composantes, et . (La notion analogue pour les graphes orientés est plus subtile et n'est pas au programme.)
12.3 Représenter un graphe en machine
Toute l'algorithmique des graphes repose sur deux questions élémentaires : quels sont les voisins de ? et l'arc existe-t-il ? Les deux représentations classiques y répondent à des coûts opposés.
12.3.1 Les listes d'adjacence
La représentation par listes d'adjacence associe à chaque sommet la liste de ses successeurs (ses voisins, en non orienté). En Python, un dictionnaire sommet liste est la forme la plus souple :
# le graphe non orienté de l'exemple : chaque arête apparaît DEUX fois
G = {"a": ["b", "c"],
"b": ["a", "c", "d"],
"c": ["a", "b", "d"],
"d": ["b", "c"],
"e": []}
# le graphe orienté : chaque arc apparaît UNE fois, chez son origine
H = {"u": ["v"], "v": ["w", "x"], "w": ["u"], "x": ["x"]}
(Si les sommets sont numérotés , une simple liste de listes G[s] fait le même office — le dictionnaire gagne quand les sommets ont des noms.)
def degre_sortant(G: dict, s) -> int:
return len(G[s])
def existe_arc(G: dict, u, v) -> bool:
return v in G[u] # parcours de la liste : O(d+(u))
def nb_arcs(G: dict) -> int:
"""Nombre d'arcs d'un graphe ORIENTÉ en listes d'adjacence."""
return sum(len(G[s]) for s in G)
assert nb_arcs(H) == 5
assert nb_arcs(G) == 10 # graphe non orienté : 2 x 5 arêtes !
La dernière assertion illustre la convention à ne jamais oublier : en non orienté, chaque arête est stockée deux fois (une par extrémité) — le nombre d'arêtes est nb_arcs(G) // 2, et toute fonction de construction doit ajouter les deux sens.
12.3.2 La matrice d'adjacence
Pour des sommets numérotés , la matrice d'adjacence est la matrice (chapitre 8 !) définie par
Un graphe non orienté donne une matrice symétrique () ; une boucle met un sur la diagonale.
# le graphe orienté H avec u, v, w, x numérotés 0, 1, 2, 3
M = [[0, 1, 0, 0],
[0, 0, 1, 1],
[1, 0, 0, 0],
[0, 0, 0, 1]]
12.3.3 Le match des deux représentations
Complexité : Listes contre matrice
Pour un graphe de sommets et arcs :
| Listes d'adjacence | Matrice d'adjacence | |
|---|---|---|
| Mémoire | ||
| « existe-t-il ? » | ||
| Énumérer les voisins de | ||
| Énumérer tous les arcs | ||
| Ajouter un arc |
Le critère décisif est la densité. Les grands graphes réels sont creux : sur le web, une page pointe vers quelques dizaines d'autres, pas vers — , très loin du maximum . Pour : les listes occupent quelques dizaines de milliards de cases (gros, mais stockable) ; la matrice en exigerait — un million de téraoctets, inenvisageable, presque entièrement remplis de zéros. Règle : listes d'adjacence par défaut (et pour tous les parcours du chapitre suivant) ; matrice quand le graphe est petit ou dense, ou que le test d'arc en domine l'usage.
def matrice_vers_listes(M: list) -> dict:
n = len(M)
return {i: [j for j in range(n) if M[i][j] == 1] for i in range(n)}
def listes_vers_matrice(G: dict, n: int) -> list:
"""Précondition : sommets numérotés 0 à n-1."""
M = [[0] * n for _ in range(n)] # le piège du chapitre 8, évité !
for u in G:
for v in G[u]:
M[u][v] = 1
return M
Coûts : dans le sens matrice listes (il faut lire toute la matrice), — plus le coût de la matrice vide, — dans l'autre. Les deux conversions sont l'exercice de contrôle parfait : convertir aller-retour doit redonner le graphe de départ.
12.4 Graphes pondérés et étiquetés
Un graphe est pondéré lorsque chaque arc ou arête porte un nombre — son poids : la distance en kilomètres d'un tronçon de route, la durée d'un vol, le débit d'un câble, le coût d'une transition. Plus généralement, arcs et sommets peuvent porter des étiquettes arbitraires (un nom de ligne de métro, un type de relation). En listes d'adjacence, le poids accompagne le voisin — couple (voisin, poids) ou, plus commode, dictionnaire de dictionnaires :
# distances en kilomètres (extrait routier, non orienté symétrisé)
R = {"Paris": {"Lyon": 465, "Lille": 225, "Nantes": 385},
"Lyon": {"Paris": 465, "Marseille": 315},
"Lille": {"Paris": 225},
"Nantes": {"Paris": 385},
"Marseille": {"Lyon": 315}}
R["Paris"]["Lyon"] # 465 : le poids, en O(1)
En matrice, contient le poids — avec une valeur conventionnelle (None, ou l'infini float("inf")) pour « pas d'arc », car serait ambigu si des poids nuls existent.
Le poids transforme la question reine. Sans poids, le « plus court chemin » compte les étapes (nombre d'arcs — chapitre 13, parcours en largeur) ; avec poids, il additionne les kilomètres (chapitre 14, Dijkstra) — et les deux réponses diffèrent : Paris--Marseille par Lyon fait étapes et km, un hypothétique vol direct ferait étape mais peu importe sa distance. Modéliser, c'est d'abord choisir ce que mesure le poids.
<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>12.5 Exercices résolus
Niveau (Application directe du cours)
Pour le graphe orienté du cours (u, v, w, x) : donner tous les degrés entrants et sortants, vérifier le lemme des poignées de main orienté, lister les chemins de à de longueur , et dire si le graphe contient un cycle.
Démonstration (Solution)
Degrés sortants : , , , (la boucle) — somme . Degrés entrants : (depuis ), , , ( et la boucle) — somme . ✓ Chemins de à : (longueur ) ; (longueur , via la boucle). Cycles : (longueur ) et la boucle (longueur ). (Compter séparément les deux familles de degrés, puis vérifier l'égalité des sommes : le contrôle systématique qui attrape les arcs oubliés — l'équivalent graphe du « mêmes éléments » des tris.)
Écrire degres_entrants(G) qui renvoie le dictionnaire pour un graphe orienté en listes d'adjacence, en un seul parcours. Pourquoi est-il moins commode que dans cette représentation ?
Démonstration (Solution)
def degres_entrants(G: dict) -> dict:
d = {s: 0 for s in G} # tous présents, même degré entrant nul !
for u in G:
for v in G[u]:
d[v] += 1
return d
assert degres_entrants({"u": ["v"], "v": ["w", "x"],
"w": ["u"], "x": ["x"]}) \
== {"u": 1, "v": 1, "w": 1, "x": 2}
Coût : , un parcours complet du graphe. L'asymétrie est structurelle : les listes d'adjacence stockent les arcs chez leur origine — est une simple longueur de liste (), tandis que exige de fouiller toutes les listes. L'initialisation à zéro pour tous les sommets est le piège de l'exercice : sans elle, un sommet sans arc entrant (comme une page web que personne ne cite) manquerait du dictionnaire. (Quand une application interroge sans cesse les prédécesseurs — « qui pointe vers cette page ? » —, on stocke aussi le graphe inverse : de l'espace contre du temps, l'arbitrage du chapitre 10.)
Un fichier fournit un graphe non orienté comme liste d'arêtes : aretes = [(0, 1), (0, 2), (1, 2), (1, 3)] avec sommets ( à ). Construire les listes d'adjacence puis la matrice, et vérifier la cohérence (symétrie, degrés, nombre d'arêtes).
Démonstration (Solution)
def depuis_aretes(aretes: list, n: int) -> dict:
G = {s: [] for s in range(n)}
for (u, v) in aretes:
G[u].append(v)
G[v].append(u) # non orienté : les DEUX sens
return G
G = depuis_aretes([(0, 1), (0, 2), (1, 2), (1, 3)], 5)
# G == {0: [1, 2], 1: [0, 2, 3], 2: [0, 1], 3: [1], 4: []}
M = listes_vers_matrice(G, 5)
assert all(M[i][j] == M[j][i] for i in range(5) for j in range(5)) # symétrie
assert sum(len(G[s]) for s in G) == 2 * 4 # 2 |A|
assert sum(sum(ligne) for ligne in M) == 2 * 4
Les trois assertions sont les invariants de cohérence d'un graphe non orienté : matrice symétrique, somme des degrés dans les deux représentations. Le sommet , isolé, doit exister avec sa liste vide — c'est l'initialisation for s in range(n) qui le garantit, pas les arêtes. (La liste d'arêtes est le format d'échange — celui des fichiers — et les deux représentations du cours sont les formats de travail : la conversion d'entrée est toujours la première fonction d'un projet graphes, et ses invariants le premier jeu de tests.)
Niveau (Application avec raisonnement intermédiaire)
Pour chacune des situations, choisir listes ou matrice, en justifiant par les coûts du cours : (a) le graphe d'amitiés d'un réseau social (, amis en moyenne) ; (b) un tournoi où chacun des joueurs affronte tous les autres et l'on enregistre qui a battu qui ; (c) le réseau routier français ( carrefours, ) interrogé par « cette route existe-t-elle ? » des millions de fois.
Démonstration (Solution)
(a) Listes, sans hésitation : — les listes occupent cellules (déjà énorme, mais c'est l'information elle-même) ; la matrice exigerait cases remplies à : exclu.
(b) Matrice : le graphe est complet par construction — : la matrice ( cases) ne gaspille rien, donne « qui a battu qui » en , et la symétrie n'est pas requise (graphe orienté des victoires). Les listes n'apporteraient rien : quand , leurs avantages s'évanouissent.
(c) Le cas piège : le test d'arc en plaide pour la matrice, mais cases pour routes : inenvisageable. Réponse : listes d'adjacence — le test v in G[u] y coûte , c'est-à-dire constant en pratique puisque le degré est borné. (La morale de (c) : les coûts en se lisent avec les ordres de grandeur du problème — un avec vaut un , et la densité tranche toujours en premier.)
Écrire est_symetrique(G) qui vérifie qu'un graphe en listes d'adjacence représente bien un graphe non orienté (chaque arc a son retour), puis symetrise(G) qui complète les retours manquants — sans créer de doublon.
Démonstration (Solution)
def est_symetrique(G: dict) -> bool:
for u in G:
for v in G[u]:
if u not in G[v]:
return False
return True
def symetrise(G: dict) -> dict:
H = {s: list(G[s]) for s in G} # copie : G n'est pas modifié
for u in G:
for v in G[u]:
if u not in H[v]:
H[v].append(u)
return H
G = {"a": ["b"], "b": [], "c": ["c"]}
assert not est_symetrique(G)
S = symetrise(G)
assert est_symetrique(S) and S["b"] == ["a"] and S["c"] == ["c"]
Coût de est_symetrique : pour chaque arc, un test u in G[v] en — total , au pire mais en général, parfaitement praticable sur graphes creux. La boucle c teste le cas limite : son retour, c'est elle-même — déjà présente, aucun doublon à créer. (La copie préalable de symetrise applique la discipline du chapitre 10 : fonction pure, source intacte ; et est_symetrique rejoint la collection d'invariants de cohérence de l'exercice 3 — un graphe « non orienté » non symétrique est une donnée corrompue à détecter tôt.)
Les cases d'une grille forment un graphe : chaque case est reliée à ses voisines du dessus, du dessous, de gauche et de droite. Écrire graphe_grille(p, q) (sommets couples ), donner et en fonction de et , et vérifier sur .
Démonstration (Solution)
def graphe_grille(p: int, q: int) -> dict:
G = {}
for i in range(p):
for j in range(q):
voisins = []
for (di, dj) in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
ni, nj = i + di, j + dj
if 0 <= ni < p and 0 <= nj < q: # rester dans la grille
voisins.append((ni, nj))
G[(i, j)] = voisins
return G
G = graphe_grille(2, 3)
assert len(G) == 6
assert sum(len(G[s]) for s in G) == 2 * 7 # 7 arêtes
assert sorted(G[(0, 0)]) == [(0, 1), (1, 0)] # un coin : 2 voisins
Comptes : ; les arêtes horizontales sont , les verticales , soit — pour : . ✓ (La grille est le graphe des labyrinthes, des images — chapitre 8 : un pixel et ses voisins ! — et des plateaux de jeu ; le test des bornes 0 <= ni < p est exactement celui de somme_voisins du chapitre 8 : les graphes unifient des problèmes qu'on croyait distincts, c'est leur raison d'être. Noter les sommets-tuples : clés de dictionnaire licites, chapitre 2.)
Un triangle d'un graphe non orienté est un triplet de sommets deux à deux reliés (trois amis mutuels — l'indicateur fétiche de l'analyse des réseaux sociaux). Écrire nb_triangles(G) pour un graphe en listes d'adjacence à sommets numérotés, en évitant de compter chaque triangle six fois ; donner le coût.
Démonstration (Solution)
On impose l'ordre pour ne compter chaque triangle qu'une fois :
def nb_triangles(G: dict) -> int:
c = 0
for u in G:
for v in G[u]:
if v > u: # chaque arête une fois
for w in G[v]:
if w > v and w in G[u]: # u < v < w, et l'arête de fermeture
c += 1
return c
triangle_plus_queue = {0: [1, 2], 1: [0, 2, 3], 2: [0, 1], 3: [1]}
assert nb_triangles(triangle_plus_queue) == 1
Sans le filtre croissant, le triangle serait vu depuis chacune de ses permutations. Coût : pour chaque arête , on parcourt les voisins de avec un test d'appartenance w in G[u] en — au total dans cette version naïve ; remplacer les listes de voisins par des ensembles (test en , même idée que le dictionnaire du chapitre 2) ramène à . (Sur le graphe d'un réseau social réel, ce comptage mesure la « cohésion » ; les versions industrielles reposent exactement sur cette boucle, ordonnée et optimisée — et le passage liste ensemble des voisinages est le premier réglage qu'on y fait.)
Niveau (Raisonnement subtil ou plusieurs étapes)
Soit la matrice d'adjacence d'un graphe orienté. Montrer que — le coefficient de la puissance -ième de (produit matriciel du chapitre 5) — est le nombre de chemins de longueur exactement de à . Vérifier sur le triangle orienté .
Démonstration (Solution)
Par récurrence sur . Pour : vaut s'il y a un arc — un chemin de longueur — et sinon. ✓ Supposons la propriété pour . Par définition du produit :
Le terme vaut (nombre de chemins de longueur de à ) ( si l'arc existe) : c'est le nombre de chemins de longueur de à dont l'avant-dernier sommet est . La somme sur les compte tous, chacun une fois (un chemin a un seul avant-dernier sommet). ✓
Vérification : pour le triangle, (le calcul donne la matrice identité) — de chaque sommet, exactement un chemin de longueur revient à lui-même (le tour complet), et aucun ne mène ailleurs. ✓ (Ce théorème est la passerelle entre l'algèbre et les graphes : compter des chemins devient multiplier des matrices — et l'exponentiation rapide du chapitre 5 compte les chemins de longueur en produits. C'est aussi lui qui fait des matrices d'adjacence l'outil des marches aléatoires et de l'algorithme de classement des pages web : la matrice du graphe, normalisée, itérée — le cours de probabilités y reviendra.)
Pour un graphe pondéré creux dont on veut quand même le test d'arc en , une troisième représentation existe : le dictionnaire d'arcs P[(u, v)] = poids. Écrire les conversions depuis/vers les listes d'adjacence pondérées (dictionnaire de dictionnaires), comparer les coûts des opérations de base aux deux représentations du cours, et identifier l'opération qu'elle fait mal.
Démonstration (Solution)
def listes_vers_arcs(R: dict) -> dict:
return {(u, v): R[u][v] for u in R for v in R[u]}
def arcs_vers_listes(P: dict, sommets: list) -> dict:
R = {s: {} for s in sommets}
for (u, v) in P:
R[u][v] = P[(u, v)]
return R
P = listes_vers_arcs({"a": {"b": 3}, "b": {"a": 3, "c": 1}, "c": {}})
assert P == {("a", "b"): 3, ("b", "a"): 3, ("b", "c"): 1}
assert ("a", "c") not in P # test d'arc : O(1)
Bilan : mémoire (mieux que la matrice, comparable aux listes) ; test d'arc et lecture de poids en (comme la matrice) ; ajout d'arc . L'opération sacrifiée : énumérer les voisins d'un sommet — il faudrait balayer tous les arcs (), là où les listes répondent en . Or les parcours du chapitre 13 ne font que ça : demander les voisins, des millions de fois. (D'où la conclusion mûre du chapitre : il n'y a pas de représentation universelle — listes pour parcourir, matrice ou dictionnaire d'arcs pour interroger, liste d'arêtes pour échanger ; un projet réel en maintient parfois deux, synchronisées, et le « choix éclairé des collections » du programme se joue exactement ici. En pratique, le dictionnaire de dictionnaires du cours cumule d'ailleurs le meilleur des deux : voisins en et test-poids en — c'est pourquoi il sera notre format au chapitre 14.)
Pour chaque situation, définir précisément le graphe (sommets, liens, orienté ?, pondéré ?, taille estimée) et la représentation choisie : (a) trouver le chemin le plus rapide en métro, correspondances comprises ; (b) détecter les dépendances circulaires entre les modules d'un logiciel ( modules) ; (c) proposer « les amis de mes amis » dans un réseau social.
Démonstration (Solution)
(a) Métro : sommets stations par ligne (Châtelet-ligne-1 et Châtelet-ligne-4 sont deux sommets !) ; arêtes pondérées par le temps : tronçons entre stations consécutives ( min) et arcs de correspondance entre les doublons d'une même station ( min) — c'est ce dédoublement qui fait payer les changements, l'erreur de modélisation classique étant de l'omettre. Non orienté pour l'essentiel ; , : listes d'adjacence pondérées, et l'algorithme sera Dijkstra (chapitre 14).
(b) Modules : sommets modules, arc orienté si importe ; non pondéré. Une dépendance circulaire un cycle dans ce graphe orienté — la question est exactement « le graphe a-t-il un cycle ? » (chapitre 13). , : listes d'adjacence ; la matrice ( cases) serait défendable mais sans bénéfice, aucun test d'arc isolé n'étant requis.
(c) Amis d'amis : sommets comptes, arêtes non orientées, non pondérées ; , : listes d'adjacence obligatoires (exercice 4). « Les amis de mes amis » les sommets à distance exactement 2 — l'union des voisins des voisins, privée des voisins directs et de soi : deux niveaux de la boucle de voisinage, soit opérations par requête, instantané même à l'échelle du réseau entier. (Trois énoncés en français, trois graphes différents par chacun de leurs attributs — orienté ou non, pondéré ou non, dédoublé ou non : la modélisation est la première décision algorithmique, et la plus lourde de conséquences ; les chapitres 13 et 14 fournissent les algorithmes, mais c'est ici que se gagne ou se perd leur pertinence.)
- Vocabulaire : ; non orienté (arêtes , symétriques) / orienté (arcs , flèches ; boucles) ; pas de multi-arcs ; degrés , , ; lemme des poignées de main ( ; ) ; chemin (suite d'arcs consécutifs, longueur nombre d'arcs), cycle, connexité et composantes (non orienté).
- Tailles réelles : web sommets, réseaux sociaux , routier — et tous creux () : la densité gouverne tout.
- Listes d'adjacence (dictionnaire sommet voisins) : mémoire , voisins en , test d'arc en ; en non orienté, chaque arête stockée deux fois (compter ) ; le format des parcours — le choix par défaut.
- Matrice d'adjacence ( si arc) : mémoire , test d'arc , voisins ; symétrique en non orienté ; pour graphes petits ou denses ; bonus algébrique : nombre de chemins de longueur .
- Pondération : poids sur les arcs (distances, durées, coûts) — dictionnaire de dictionnaires
R[u][v] = poids(voisins et poids ) ; en matrice,inf/Nonepour « pas d'arc » (jamais ) ; le poids change la notion de plus court chemin (étapes vs kilomètres). - Invariants de cohérence (le jeu de tests des graphes) : symétrie des listes en non orienté, , sommets isolés présents avec liste vide, aller-retour des conversions.
- Modéliser : choisir sommets, liens, orientation, poids — dédoubler s'il le faut (stations par ligne) ; la liste d'arêtes pour échanger, les listes d'adjacence pour travailler ; il n'existe pas de représentation universelle, seulement des usages.
12.6 Exercices d'entraînement
Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.
A. Vocabulaire et petits graphes
- () Dessiner le graphe non orienté , arêtes si est multiple de : degrés, composantes connexes, cycles ?
- () Combien d'arêtes possède le graphe complet à sommets ? Le cycle à sommets ? La grille (exercice résolu 6) ? Vérifier par le lemme des poignées de main.
- ( ) Montrer qu'un graphe non orienté à sommets possède toujours deux sommets de même degré (principe des tiroirs sur les degrés possibles , dont deux sont incompatibles).
- () Peut-on tracer un graphe à sommets tous de degré ? Justifier par la parité de .
- () Dans un groupe de six personnes, montrer qu'il en existe trois qui se connaissent mutuellement ou trois qui s'ignorent mutuellement (colorier les arêtes du graphe complet en deux couleurs : un triangle monochrome existe toujours — le petit théorème de Ramsey).
B. Représentations
- () Écrire
vers_aretes(G)(la liste d'arêtes d'un graphe non orienté en listes d'adjacence, chaque arête une fois) et vérifier l'aller-retour avecdepuis_aretes. - () Écrire
sommets_isoles(G),degre_max(G)etsont_voisins(G, u, v)pour les deux représentations ; comparer les coûts. - ( ) Écrire
lire_graphe(nom_fichier): un fichier texte « une arête par ligne, deux noms séparés par un espace » (chapitre 4 !) vers les listes d'adjacence — robuste aux lignes vides et aux doublons d'arêtes. - () Le graphe inverse d'un graphe orienté (tous les arcs retournés) : l'écrire pour les deux représentations ; laquelle rend l'opération triviale ?
- () Mesurer réellement la mémoire : construire le graphe aléatoire , chaque arc présent avec probabilité puis , dans les deux représentations (
sys.getsizeofen première approximation) — vérifier le basculement creux/dense. - () L'ensemble (
set) comme liste d'adjacence : remplacer les listes de voisins par des ensembles, réécrireexiste_arcetnb_triangles, mesurer le gain — et identifier ce qu'on perd (l'ordre des voisins).
C. Graphes pondérés
- () Compléter le graphe routier du cours avec trois villes et leurs distances réelles, puis écrire
poids_total(R)(la somme des poids, chaque arête comptée une fois). - () Écrire
poids_chemin(R, chemin): le poids total d'un chemin donné comme liste de sommets, ouNonesi une étape n'existe pas — avec son jeu de tests par partitionnement (chapitre 10 : chemin vide, une étape, étape manquante). - ( ) La matrice pondérée avec
float("inf")pour « pas d'arc » : écrire la conversion depuis le dictionnaire de dictionnaires, et justifier pourquoiinf(et non ou ) est la bonne sentinelle pour les algorithmes de plus courts chemins à venir. - () Étiquettes multiples : modéliser un réseau aérien où chaque arc porte (durée, prix, compagnie) ; écrire
vol_le_moins_cher(R, u, v)parmi les arcs directs, et discuter ce que « plus court chemin » signifie quand il y a deux poids (l'optimisation multi-critères n'a pas une réponse unique).
D. Études
- () Construire le graphe des mots : sommets mots de trois lettres d'une liste donnée, arête si les mots diffèrent d'exactement une lettre — la brique du jeu des échelles de mots (chapitre 13 le parcourra).
- ( ) Vérifier expérimentalement le théorème de l'exercice résolu 8 : pour un graphe aléatoire à sommets, comparer au comptage récursif des chemins de longueur (énumération, chapitre 6).
- () Le graphe du « petit monde » : générer un cycle de sommets où chacun est aussi relié à ses voisins à distance , puis recâbler des arêtes au hasard ; calculer le degré moyen avant/après — préparation à la mesure des distances au chapitre 13.
- ( ) Dossier de modélisation : pour le problème « un robot doit traverser un entrepôt à étagères en évitant les zones interdites », définir le graphe (grille du cours moins les obstacles), écrire le constructeur depuis un plan en chaînes de caractères (
#mur,.libre), et livrer les invariants de cohérence et leur jeu de tests — l'entrée du labyrinthe que le chapitre 13 résoudra.