Composantes fortement connexes et couplages
Cours complet · informatique (MP2I/MPI), chapitre 24 · MP2I et MPI
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
24.1 Deux problèmes, deux surprises
Ce chapitre traite deux questions de graphes que le programme place en deuxième année, et chacune réserve une surprise.
La première : décomposer un graphe orienté en composantes fortement connexes. On s'attend à un algorithme compliqué ; il tient en deux parcours en profondeur. Et le programme demande d'en tirer une conséquence inattendue : « on fait le lien entre composantes fortement connexes et le problème 2-sat » — un problème de logique résolu par un algorithme de graphes, en temps linéaire.
La seconde : trouver un couplage maximal dans un graphe biparti. La surprise est qu'un glouton n'y suffit pas, et que le remède consiste à défaire des choix déjà faits — la première fois du livre.
24.2 Composantes fortement connexes
Dans un graphe orienté, la relation « et sont mutuellement accessibles » est une relation d'équivalence. Ses classes sont les composantes fortement connexes.
En contractant chaque composante en un seul sommet, on obtient un graphe orienté sans cycle — le graphe quotient.
Démonstration
Un cycle entre plusieurs composantes rendrait tous leurs sommets mutuellement accessibles : elles n'en formeraient qu'une, ce qui contredit leur maximalité.
Le graphe quotient étant acyclique, il se trie topologiquement (chapitre chap:parcours). On obtient ainsi un ordre de traitement des composantes — ce qui permet de résoudre par programmation dynamique des problèmes qu'un cycle rendait circulaires. La décomposition en composantes fortement connexes est donc la manière standard de rendre acyclique un graphe qui ne l'est pas.
24.2.1 L'algorithme de Kosaraju
Méthode : Deux parcours, et un graphe retourné
- Faire un parcours en profondeur de et empiler chaque sommet quand on a fini de le traiter — c'est exactement le tri topologique du chapitre chap:parcours.
- Construire , le graphe transposé : tous les arcs retournés.
- Parcourir en repartant des sommets dans l'ordre de la pile. Chaque parcours atteint exactement une composante.
(* Renvoie un tableau comp tel que comp.(u) est le numéro de la composante
fortement connexe de u. Complexité : Theta(n + m). *)
let kosaraju g =
let n = Array.length g in
(* 1. ordre de fin de traitement *)
let vu = Array.make n false and ordre = ref [] in
let rec descendre u =
vu.(u) <- true;
List.iter (fun v -> if not vu.(v) then descendre v) g.(u);
ordre := u :: !ordre
in
for s = 0 to n - 1 do if not vu.(s) then descendre s done;
(* 2. le graphe transposé *)
let gt = Array.make n [] in
Array.iteri (fun u l -> List.iter (fun v -> gt.(v) <- u :: gt.(v)) l) g;
(* 3. parcours de gt dans cet ordre *)
let comp = Array.make n (-1) and k = ref 0 in
let rec marquer u =
comp.(u) <- !k;
List.iter (fun v -> if comp.(v) = -1 then marquer v) gt.(u)
in
List.iter (fun u -> if comp.(u) = -1 then begin marquer u; incr k end) !ordre;
comp
C'est la clé de l'algorithme, et elle est simple : et ont exactement les mêmes composantes fortement connexes — la relation « mutuellement accessibles » est symétrique, donc insensible au retournement.
En revanche, les arcs entre composantes sont retournés. Le quotient de est donc le quotient de à l'envers. En partant du sommet dont le traitement s'est terminé en dernier — qui appartient à une composante source de —, on se trouve dans une composante puits de : le parcours ne peut plus en sortir, et ramasse donc exactement cette composante.
: deux parcours et une transposition, chacun linéaire.
24.2.2 Le lien avec 2-sat
Le chapitre chap:logique annonçait que 2-sat est linéaire alors que 3-sat est np-complet. Voici pourquoi, et c'est le lien que le programme demande.
Une clause à deux littéraux s'écrit de deux façons équivalentes comme une implication :
On construit alors le graphe des implications : un sommet par littéral — sommets — et, pour chaque clause, les deux arcs ci-dessus.
Une formule 2-sat est satisfiable si et seulement si aucune variable n'a et dans la même composante fortement connexe.
Démonstration (Idée)
Une composante fortement connexe est un ensemble de littéraux qui s'impliquent tous mutuellement : ils doivent donc recevoir la même valeur. Si et y figurent ensemble, on exige : impossible.
Réciproquement, si aucune variable n'est ainsi piégée, on construit un modèle en traitant les composantes dans l'ordre topologique inverse du quotient, et en affectant vrai à toute composante non encore décidée. L'acyclicité du quotient garantit la cohérence.
. Le graphe contient , , , , , .
Aucune variable n'a ses deux littéraux dans une même composante : la formule est satisfiable — et de fait convient, quel que soit .
Pourquoi le procédé s'arrête-t-il à deux littéraux ? Parce qu'une clause à deux littéraux dit : si l'un est faux, l'autre est vrai — une implication, donc un arc. Une clause à trois littéraux dirait : si l'un est faux, l'un des deux autres est vrai — une disjonction, qu'aucun arc ne code. Toute la structure de graphe s'effondre, et avec elle la résolution en temps linéaire.
24.3 Couplages dans un graphe biparti
Un couplage est un ensemble d'arêtes deux à deux sans extrémité commune. Un sommet appartenant à une arête du couplage est dit saturé. On cherche un couplage de cardinal maximum.
Le programme situe l'usage : « les graphes bipartis et couplages sont introduits comme outils naturels de modélisation » — affecter des étudiants à des stages, des tâches à des machines, des candidats à des postes.
Prendre les arêtes une à une tant que leurs extrémités sont libres donne un couplage maximal — qu'on ne peut plus agrandir en ajoutant — mais pas nécessairement maximum.
Il faut donc pouvoir défaire un choix. C'est la première fois du livre : les gloutons du chapitre chap:gloutons ne revenaient jamais en arrière, et c'est précisément pourquoi ils échouent ici.
24.3.1 Les chemins augmentants
Un chemin augmentant pour un couplage est un chemin qui :
- part d'un sommet non saturé et arrive à un sommet non saturé ;
- alterne les arêtes hors de et les arêtes de .
Un tel chemin a un nombre impair d'arêtes, dont une de plus hors de que dedans.
Méthode : Augmenter
On échange le long du chemin : ce qui était dans en sort, ce qui était dehors y entre. Le couplage reste valide — chaque sommet intermédiaire perd une arête et en gagne une — et les deux extrémités, libres, deviennent saturées. Le cardinal augmente d'exactement un.
Un couplage est de cardinal maximum si et seulement s'il n'admet aucun chemin augmentant.
Démonstration (Sens réciproque, par la différence symétrique)
Si admet un chemin augmentant, il n'est évidemment pas maximum. Réciproquement, supposons pour un couplage maximum , et considérons , l'ensemble des arêtes de l'un ou de l'autre mais pas des deux.
Dans ce sous-graphe, chaque sommet est de degré au plus — au plus une arête de chaque couplage. Les composantes connexes sont donc des chemins et des cycles alternants. Les cycles ont autant d'arêtes de que de ; comme , au moins une composante est un chemin comportant plus d'arêtes de que de . Ce chemin commence et finit par une arête de , donc ses deux extrémités ne sont pas saturées par : c'est un chemin augmentant pour .
Méthode : L'algorithme, élémentaire
Le programme précise : « on se limite à une approche élémentaire ; l'algorithme de Hopcroft-Karp n'est pas au programme ».
(* Couplage maximum d'un graphe biparti. g.(u) est la liste des voisins de u
dans la partie droite. conj.(v) = Some u si v est couplé à u.
Complexité : O(n·m). *)
let couplage_maximum g nd =
let ng = Array.length g in
let conj = Array.make nd None in
(* cherche un chemin augmentant depuis u ; vu évite de tourner en rond *)
let rec augmenter vu u =
List.exists (fun v ->
if vu.(v) then false
else begin
vu.(v) <- true;
match conj.(v) with
| None -> conj.(v) <- Some u; true (* v est libre : on prend *)
| Some w ->
(* v est pris par w : w peut-il se recaser AILLEURS ? *)
if augmenter vu w then begin conj.(v) <- Some u; true end
else false
end) g.(u)
in
let taille = ref 0 in
for u = 0 to ng - 1 do
if augmenter (Array.make nd false) u then incr taille
done;
!taille
La ligne qui porte tout est le Some w : au lieu d'abandonner parce que est déjà pris, on demande à son partenaire actuel de se recaser ailleurs. C'est la remise en cause qu'un glouton s'interdit — et la récursion la propage aussi loin qu'il faut.
Chaque recherche de chemin augmentant coûte , et il y en a au plus — une par arête ajoutée. Total : . Hopcroft-Karp descend à , hors programme.
Le programme note que couplages et graphes bipartis « peuvent également constituer une introduction aux problèmes de flots ». Le lien est direct : un couplage maximum est un flot maximum dans le réseau obtenu en ajoutant une source reliée à toute la partie gauche, un puits relié à toute la partie droite, et des capacités de partout. Le chemin augmentant devient alors le chemin améliorant de l'algorithme de Ford-Fulkerson. Les flots ne sont pas au programme ; le vocabulaire, lui, est déjà acquis.
24.4 Ce qu'il faut retenir
- Kosaraju : deux parcours en profondeur, dont le second sur le graphe transposé, en suivant l'ordre de fin du premier. . Le quotient est acyclique — c'est ce qui rend le graphe traitable.
- 2-sat se résout en temps linéaire par les composantes fortement connexes du graphe des implications. Satisfiable si et seulement si aucune variable n'a ses deux littéraux dans la même composante. Le procédé casse dès trois littéraux : une disjonction n'est pas un arc.
- Couplage : le glouton donne un couplage maximal, pas maximum. Il faut défaire des choix — c'est le chemin augmentant, et le théorème de Berge dit qu'il n'en existe plus exactement quand on est au maximum.