Aller au contenu

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.

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.

AGRSLNP

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.

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, {A,G,R}\lbrace A, G, R \rbrace est un stable : trois examens qui peuvent partager un créneau. Le nombre de stabilité α(G)\alpha(G) est la taille du plus grand stable ; ici, α=3\alpha = 3.
  • Une clique est un ensemble de sommets deux à deux adjacents. {G,P,S}\lbrace G, P, S \rbrace est une clique : ces trois examens doivent être sur des créneaux tous différents. Le nombre de clique ω(G)\omega(G) est la taille de la plus grande clique ; ici, ω=3\omega = 3 (il y a trois triangles, {G,P,S}\lbrace G, P, S \rbrace, {A,P,S}\lbrace A, P, S \rbrace et {A,N,S}\lbrace A, N, S \rbrace, mais aucun groupe de quatre).
  • Dans un graphe orienté, un ensemble BB est absorbant si tout sommet hors de BB a au moins un successeur dans BB. Le nombre d’absorption β(G)\beta(G) 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 {A, G, R} : aucune arête entre eux→ un même créneau possibleAGRSLNPclique {G, P, S} : tous reliés→ trois créneaux différentsAGRSLNP
Un stable peut partager un créneau ; une clique demande autant de créneaux que de sommets.

Stable et clique sont deux faces d’une même notion : un stable de GG est une clique du graphe complémentaire (celui qui relie exactement les paires non reliées dans GG).

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 kk couleurs, c’est partitionner les sommets en kk stables. Le nombre chromatique χ(G)\chi(G) (lisez « khi ») est le plus petit nombre de couleurs possible.

Pour le planning, une couleur est un créneau, et χ(G)\chi(G) est le nombre minimal de créneaux.

Calculer χ(G)\chi(G) 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.

123456n / α = 7 / 3 ≈ 2,33ω = 3 (clique)Δ + 1 = 5χ = 3, atteint par Welsh-Powell3 ≤ χ ≤ 5
Les deux minorants et le majorant enferment χ entre 3 et 5 ; une coloration à 3 couleurs suffit alors à conclure.

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 q(G)q(G) vaut Δ\Delta ou Δ+1\Delta + 1. 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.

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 Δ+1\Delta + 1 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 n/2n / 2 (vous en verrez un en exercice). Toute l’astuce est donc de bien choisir l’ordre.

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.

1372456couleur 1 : {A, G, R}AGRSLNP1372456en ambre : rang dans l’ordre des degrés décroissants (A, S, G, L, N, P, R)couleur 2 : {S, L}AGRSLNP1372456couleur 3 : {N, P}AGRSLNP
Welsh-Powell parcourt la liste A, S, G, L, N, P, R et remplit une couleur à la fois : trois passes, donc trois créneaux.

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.

1234567ordre de DSatur :1. A (saturation 0)2. S (saturation 1)3. N (saturation 2)4. P (saturation 2)5. G (saturation 2)6. L (saturation 1)7. R (saturation 2)AGRSLNP
DSatur choisit à chaque fois le sommet le plus contraint : A, puis S, puis N ; il aboutit lui aussi à trois couleurs.
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 -> 2

networkx 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-Powell
print(nx.greedy_color(G, strategy="saturation_largest_first")) # DSatur
  • 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.
  • Un stable est un ensemble de sommets non adjacents (α\alpha = taille max), une clique un ensemble de sommets deux à deux adjacents (ω\omega = taille max).
  • Une coloration donne des couleurs différentes aux sommets adjacents ; chaque couleur est un stable. Le nombre chromatique χ\chi est le minimum de couleurs.
  • On encadre : max⁡(ω,⌈n/α⌉)≤χ≤Δ+1\max(\omega, \lceil n / \alpha \rceil) \leq \chi \leq \Delta + 1. 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 Δ\Delta ou Δ+1\Delta + 1.

Exercice · Des fréquences pour cinq émetteurs

facile

Cinq é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 χ=3\chi = 3.

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 = 1
while premiere_coloration(k) is None: # plus petit k qui marche : le nombre chromatique
k += 1
print(k, premiere_coloration(k)) # 3 {'V': 1, 'W': 2, 'X': 3, 'Y': 1, 'Z': 2}

Exercice · Le glouton piégé par l'ordre

moyen

On considère le graphe à six sommets a1,a2,a3,b1,b2,b3a_1, a_2, a_3, b_1, b_2, b_3 où aia_i est relié à bjb_j si et seulement si i≠ji \neq j.

  1. Quel est son nombre chromatique ?
  2. Appliquez le glouton simple dans l’ordre a1,b1,a2,b2,a3,b3a_1, b_1, a_2, b_2, a_3, b_3. Combien de couleurs obtenez-vous ?
  3. Que donne Welsh-Powell ici ? Que faut-il en conclure ?
Voir la solution
  1. Aucune arête ne relie deux aa ni deux bb : le graphe est biparti, et 2 couleurs suffisent (tous les aa en couleur 1, tous les bb en couleur 2). Comme il a des arêtes, χ=2\chi = 2.
  2. a1a_1 reçoit 1. b1b_1 n’est pas voisin de a1a_1 : il reçoit 1. a2a_2 est voisin de b1b_1 (couleur 1) : il reçoit 2. b2b_2 est voisin de a1a_1 (1) mais pas de a2a_2 : il reçoit 2. a3a_3 est voisin de b1b_1 (1) et b2b_2 (2) : il reçoit 3. b3b_3 est voisin de a1a_1 (1) et a2a_2 (2) : il reçoit 3. Résultat : 3 couleurs au lieu de 2. Avec kk paires construites de la même façon, ce glouton utiliserait kk couleurs pour un graphe 2-coloriable.
  3. 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 couleurs
print(max(bon.values())) # 2 : le même glouton, dans un autre ordre, est optimal

Exercice · Prouver le majorant

difficile

Démontrez que tout graphe de degré maximal Δ\Delta vérifie χ(G)≤Δ+1\chi(G) \leq \Delta + 1. 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 {1,…,Δ+1}\lbrace 1, \dots, \Delta + 1 \rbrace que n’utilise aucun de ses voisins déjà coloriés. Un sommet a au plus Δ\Delta voisins, donc au plus Δ\Delta couleurs sont interdites : il en reste toujours au moins une de libre. La coloration obtenue est valide et utilise au plus Δ+1\Delta + 1 couleurs.

La borne est atteinte pour le graphe complet KnK_n (Δ=n−1\Delta = n - 1, χ=n\chi = n) et pour un cycle de longueur impaire (Δ=2\Delta = 2, χ=3\chi = 3). Elle est très mauvaise pour une étoile à nn branches : un centre relié à nn feuilles, soit Δ=n\Delta = n, alors que χ=2\chi = 2 (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, χ≤Δ\chi \leq \Delta.

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 = 2