Colorez un graphe
Le service de scolarité doit placer les examens de fin de semestre. Deux matières qui ont des étudiants en commun ne peuvent pas avoir leur examen sur le même créneau. Combien de créneaux faut-il, au minimum ? Et comment répartir les examens ?
Ce problème, et beaucoup d’autres (fréquences radio, registres d’un processeur, Sudoku), est un problème de coloration de graphe. Il est simple à énoncer, redoutablement difficile à résoudre exactement, mais de bonnes heuristiques existent. C’est l’objet de ce chapitre.
Le fil rouge : un planning d’examens
Section intitulée « Le fil rouge : un planning d’examens »Sept matières sont concernées : Graphes (G), Probabilités (P), Statistiques (S), Prolog (L), Apprentissage (A), Anglais (N) et Réseaux (R). On construit le graphe des incompatibilités : un sommet par matière, une arête entre deux matières qui ont au moins un étudiant en commun.
Les incompatibilités sont : G-P, G-S, G-L, P-S, P-A, S-A, L-A, L-R, A-N, N-R et S-N. Les couleurs du schéma montrent une solution à trois créneaux, que nous allons retrouver.
Stables, cliques et absorbants
Section intitulée « Stables, cliques et absorbants »Avant de colorier, trois notions structurent le raisonnement.
- Un stable (ou ensemble indépendant) est un ensemble de sommets deux à deux non adjacents. Dans notre graphe, est un stable : trois examens qui peuvent partager un créneau. Le nombre de stabilité est la taille du plus grand stable ; ici, .
- Une clique est un ensemble de sommets deux à deux adjacents. est une clique : ces trois examens doivent être sur des créneaux tous différents. Le nombre de clique est la taille de la plus grande clique ; ici, (il y a trois triangles, , et , mais aucun groupe de quatre).
- Dans un graphe orienté, un ensemble est absorbant si tout sommet hors de a au moins un successeur dans . Le nombre d’absorption est la taille du plus petit absorbant. Exemple typique : placer le moins de casernes de pompiers possible pour que chaque quartier soit voisin d’une caserne.
Stable et clique sont deux faces d’une même notion : un stable de est une clique du graphe complémentaire (celui qui relie exactement les paires non reliées dans ).
Colorier un graphe
Section intitulée « Colorier un graphe »Coloration et nombre chromatique
Section intitulée « Coloration et nombre chromatique »Une coloration des sommets attribue une couleur à chaque sommet de sorte que deux sommets adjacents aient des couleurs différentes. Chaque couleur définit donc un stable : colorier avec couleurs, c’est partitionner les sommets en stables. Le nombre chromatique (lisez « khi ») est le plus petit nombre de couleurs possible.
Pour le planning, une couleur est un créneau, et est le nombre minimal de créneaux.
Encadrer le nombre chromatique
Section intitulée « Encadrer le nombre chromatique »Calculer exactement est un problème NP-difficile : on ne connaît pas d’algorithme efficace pour les grands graphes. En revanche, on sait l’encadrer.
Deux résultats célèbres complètent le tableau. Le théorème des quatre couleurs (démontré en 1976, avec l’aide d’un ordinateur) affirme que tout graphe planaire (qu’on peut dessiner sans croisement d’arêtes, comme une carte de pays voisins) est coloriable avec 4 couleurs. Et pour la coloration des arêtes (deux arêtes qui partagent un sommet doivent avoir des couleurs différentes), le théorème de Vizing dit que l’indice chromatique vaut ou . Exemple d’usage : organiser un tournoi où chaque équipe joue au plus un match par journée ; les journées sont les couleurs des arêtes.
Des heuristiques gloutonnes
Section intitulée « Des heuristiques gloutonnes »Le glouton simple, et ses limites
Section intitulée « Le glouton simple, et ses limites »L’algorithme glouton parcourt les sommets dans un ordre donné et attribue à chacun la plus petite couleur non utilisée par ses voisins déjà coloriés. Il utilise toujours au plus couleurs, mais son résultat dépend énormément de l’ordre choisi : il existe des graphes à 2 couleurs où un mauvais ordre en fait utiliser (vous en verrez un en exercice). Toute l’astuce est donc de bien choisir l’ordre.
Welsh-Powell : les plus contraints d’abord
Section intitulée « Welsh-Powell : les plus contraints d’abord »L’heuristique de Welsh-Powell trie les sommets par degré décroissant, puis construit les couleurs une par une : on parcourt la liste et on donne la couleur 1 à chaque sommet non colorié qui n’a aucun voisin de couleur 1 ; puis on recommence avec la couleur 2, etc.
DSatur : la saturation d’abord
Section intitulée « DSatur : la saturation d’abord »L’heuristique DSatur (degré de saturation) choisit dynamiquement le prochain sommet : celui dont les voisins utilisent déjà le plus de couleurs différentes (le plus « coincé »), en départageant par le degré. Sur notre exemple, elle colorie A, puis S (voisin de A, saturation 1), puis N (voisin de A et S, saturation 2), et aboutit aussi à 3 couleurs. DSatur est exacte sur certaines familles de graphes (les graphes bipartis, par exemple) et donne en général de meilleurs résultats que Welsh-Powell.
def welsh_powell(voisins): """voisins : dictionnaire sommet -> ensemble de voisins. Renvoie sommet -> couleur.""" ordre = sorted(voisins, key=lambda x: (-len(voisins[x]), x)) # degré décroissant couleur, c = {}, 0 while len(couleur) < len(voisins): for x in ordre: if x not in couleur and all(couleur.get(y) != c for y in voisins[x]): couleur[x] = c c += 1 # couleur suivante return couleur
aretes = ["GP", "GS", "GL", "PS", "PA", "SA", "LA", "LR", "AN", "NR", "SN"]voisins = {x: set() for x in "GPSLANR"}for u, v in aretes: voisins[u].add(v) voisins[v].add(u)print(welsh_powell(voisins)) # A, G, R -> 0 ; S, L -> 1 ; N, P -> 2networkx propose les mêmes heuristiques (sa fonction ne renvoie qu’une coloration, pas une preuve d’optimalité) :
import networkx as nx
G = nx.Graph([tuple(a) for a in aretes])print(nx.greedy_color(G, strategy="largest_first")) # esprit Welsh-Powellprint(nx.greedy_color(G, strategy="saturation_largest_first")) # DSaturOù rencontre-t-on la coloration ?
Section intitulée « Où rencontre-t-on la coloration ? »- Attribution de fréquences : des émetteurs radio trop proches (par exemple à moins de 100 km) interfèrent s’ils utilisent la même fréquence. On relie les émetteurs trop proches, et le nombre minimal de fréquences est le nombre chromatique. C’était le thème d’un exercice de devoir surveillé.
- Allocation de registres : un compilateur doit ranger les variables d’un programme dans un petit nombre de registres du processeur. Deux variables « vivantes » en même temps ne peuvent pas partager un registre : on colorie le graphe d’interférence.
- Emplois du temps, Sudoku (un Sudoku est une coloration à 9 couleurs d’un graphe de 81 sommets, partiellement pré-coloriée), affectation de quais dans une gare.
En résumé
Section intitulée « En résumé »- Un stable est un ensemble de sommets non adjacents ( = taille max), une clique un ensemble de sommets deux à deux adjacents ( = taille max).
- Une coloration donne des couleurs différentes aux sommets adjacents ; chaque couleur est un stable. Le nombre chromatique est le minimum de couleurs.
- On encadre : . Le calcul exact est NP-difficile.
- Les heuristiques gloutonnes dépendent de l’ordre : Welsh-Powell (degré décroissant) et DSatur (saturation décroissante) donnent de bons résultats.
- Un graphe planaire se colorie avec 4 couleurs ; l’indice chromatique (coloration des arêtes) vaut ou .
Exercices
Section intitulée « Exercices »Exercice · Des fréquences pour cinq émetteurs
facileCinq émetteurs radio V, W, X, Y, Z interfèrent deux à deux dans les cas suivants : V-W, V-X, W-X, X-Y, Y-Z. Combien de fréquences faut-il au minimum ? Proposez une attribution.
Voir la solution
V, W et X sont deux à deux en conflit : c’est une clique de taille 3, donc il faut au moins 3 fréquences. Trois suffisent : V → f1, W → f2, X → f3, Y → f1 (Y n’est en conflit qu’avec X et Z), Z → f2 (en conflit seulement avec Y). On vérifie chaque conflit : V-W (f1, f2), V-X (f1, f3), W-X (f2, f3), X-Y (f3, f1), Y-Z (f1, f2). Donc .
En code
from itertools import product
emetteurs = ["V", "W", "X", "Y", "Z"]conflits = [("V", "W"), ("V", "X"), ("W", "X"), ("X", "Y"), ("Y", "Z")]
def premiere_coloration(k): """Essaie toutes les attributions de k fréquences et renvoie la première valide.""" for couleurs in product(range(1, k + 1), repeat=len(emetteurs)): f = dict(zip(emetteurs, couleurs)) if all(f[u] != f[v] for u, v in conflits): return f return None
k = 1while premiere_coloration(k) is None: # plus petit k qui marche : le nombre chromatique k += 1print(k, premiere_coloration(k)) # 3 {'V': 1, 'W': 2, 'X': 3, 'Y': 1, 'Z': 2}emetteurs <- c("V", "W", "X", "Y", "Z")conflits <- rbind(c("V", "W"), c("V", "X"), c("W", "X"), c("X", "Y"), c("Y", "Z"))
colorations_valides <- function(k) { # toutes les attributions de k fréquences, une par ligne f <- as.matrix(expand.grid(rep(list(1:k), length(emetteurs)))) colnames(f) <- emetteurs ok <- apply(f, 1, function(ligne) all(ligne[conflits[, 1]] != ligne[conflits[, 2]])) f[ok, , drop = FALSE]}
k <- 1while (nrow(colorations_valides(k)) == 0) k <- k + 1 # nombre chromatiquek # 3colorations_valides(3)[1, ] # V 3, W 2, X 1, Y 2, Z 1 (une solution parmi d'autres)Exercice · Le glouton piégé par l'ordre
moyenOn considère le graphe à six sommets où est relié à si et seulement si .
- Quel est son nombre chromatique ?
- Appliquez le glouton simple dans l’ordre . Combien de couleurs obtenez-vous ?
- Que donne Welsh-Powell ici ? Que faut-il en conclure ?
Voir la solution
- Aucune arête ne relie deux ni deux : le graphe est biparti, et 2 couleurs suffisent (tous les en couleur 1, tous les en couleur 2). Comme il a des arêtes, .
- reçoit 1. n’est pas voisin de : il reçoit 1. est voisin de (couleur 1) : il reçoit 2. est voisin de (1) mais pas de : il reçoit 2. est voisin de (1) et (2) : il reçoit 3. est voisin de (1) et (2) : il reçoit 3. Résultat : 3 couleurs au lieu de 2. Avec paires construites de la même façon, ce glouton utiliserait couleurs pour un graphe 2-coloriable.
- Tous les sommets ont le même degré (2), donc le tri par degré ne départage rien : selon l’ordre des égalités, Welsh-Powell peut tomber dans le même piège. Conclusion : une heuristique gloutonne n’offre aucune garantie d’optimalité. DSatur, lui, colorie correctement tout graphe biparti.
En code
voisins = {f"a{i}": {f"b{j}" for j in (1, 2, 3) if j != i} for i in (1, 2, 3)}for i in (1, 2, 3): voisins[f"b{i}"] = {f"a{j}" for j in (1, 2, 3) if j != i}
def glouton(voisins, ordre): """Chaque sommet reçoit la plus petite couleur absente chez ses voisins déjà coloriés.""" couleur = {} for x in ordre: prises = {couleur[y] for y in voisins[x] if y in couleur} couleur[x] = min(c for c in range(1, len(ordre) + 1) if c not in prises) return couleur
mauvais = glouton(voisins, ["a1", "b1", "a2", "b2", "a3", "b3"])bon = glouton(voisins, ["a1", "a2", "a3", "b1", "b2", "b3"])print(mauvais, max(mauvais.values())) # a1, b1 -> 1 ; a2, b2 -> 2 ; a3, b3 -> 3 : 3 couleursprint(max(bon.values())) # 2 : le même glouton, dans un autre ordre, est optimalsommets <- c("a1", "a2", "a3", "b1", "b2", "b3")adj <- matrix(FALSE, 6, 6, dimnames = list(sommets, sommets))for (i in 1:3) for (j in 1:3) if (i != j) adj[paste0("a", i), paste0("b", j)] <- adj[paste0("b", j), paste0("a", i)] <- TRUE
glouton <- function(adj, ordre) { # chaque sommet reçoit la plus petite couleur absente chez ses voisins déjà coloriés couleur <- setNames(rep(NA, length(ordre)), ordre) for (x in ordre) { prises <- couleur[adj[x, ordre]] couleur[x] <- min(setdiff(seq_along(ordre), prises)) } couleur}
glouton(adj, c("a1", "b1", "a2", "b2", "a3", "b3")) # 1 1 2 2 3 3 : 3 couleursmax(glouton(adj, c("a1", "a2", "a3", "b1", "b2", "b3"))) # 2 : optimal dans cet ordreExercice · Prouver le majorant
difficileDémontrez que tout graphe de degré maximal vérifie . Donnez un graphe pour lequel cette borne est atteinte, et un autre pour lequel elle est très mauvaise.
Voir la solution
On colorie les sommets un par un, dans n’importe quel ordre, en donnant à chacun la plus petite couleur parmi que n’utilise aucun de ses voisins déjà coloriés. Un sommet a au plus voisins, donc au plus couleurs sont interdites : il en reste toujours au moins une de libre. La coloration obtenue est valide et utilise au plus couleurs.
La borne est atteinte pour le graphe complet (, ) et pour un cycle de longueur impaire (, ). Elle est très mauvaise pour une étoile à branches : un centre relié à feuilles, soit , alors que (le centre d’une couleur, toutes les feuilles de l’autre). Le théorème de Brooks affine d’ailleurs la borne : pour un graphe connexe qui n’est ni complet ni un cycle impair, .
En code
from itertools import product
def chi(n, aretes): """Nombre chromatique par force brute (petits graphes seulement).""" k = 1 while not any(all(c[u] != c[v] for u, v in aretes) for c in product(range(k), repeat=n)): k += 1 return k
def delta(n, aretes): return max(sum(x in a for a in aretes) for x in range(n)) # degré maximal
exemples = { "K5": (5, [(u, v) for u in range(5) for v in range(u + 1, 5)]), "cycle C5": (5, [(i, (i + 1) % 5) for i in range(5)]), "étoile à 6 branches": (7, [(0, i) for i in range(1, 7)]),}for nom, (n, aretes) in exemples.items(): print(nom, "Delta + 1 =", delta(n, aretes) + 1, " chi =", chi(n, aretes))# K5 Delta + 1 = 5 chi = 5# cycle C5 Delta + 1 = 3 chi = 3# étoile à 6 branches Delta + 1 = 7 chi = 2chi <- function(n, aretes) { # nombre chromatique par force brute (petits graphes seulement) k <- 1 repeat { colorations <- as.matrix(expand.grid(rep(list(1:k), n))) if (any(apply(colorations, 1, function(c) all(c[aretes[, 1]] != c[aretes[, 2]])))) return(k) k <- k + 1 }}delta <- function(aretes) max(table(aretes)) # degré maximal
K5 <- t(combn(5, 2))C5 <- cbind(1:5, c(2:5, 1))etoile <- cbind(1, 2:7) # centre 1, six feuillesfor (g in list(K5, C5, etoile)) cat("Delta + 1 =", delta(g) + 1, " chi =", chi(max(g), g), "\n")# K5 : 5 et 5 ; C5 : 3 et 3 ; étoile à 6 branches : 7 et 2