11.6. Composante fortement connexe d'un graphe orienté

Pour les graphes non-orientés nous avions défini une composante connexe comme étant un ensemble de sommets tel qu'il existait toujours un chemin entre deux sommets de cet ensemble. Deux sommets reliés par un arc faisait forcément partie de la même composante connexe.

D'une manière similaire, pour un graphe orienté, on peut définir une composante fortement connexe comme étant un ensemble de sommets tel qu'il existe toujours un chemin entre deux sommets de cet ensemble. Cette définition implique que tous les sommets d'une composante connexe font partie d'un même cycle. Deux sommets reliés par un arc ne font donc pas forcément partie de la même composante.

En fait, les sommets se subdivisent en composantes fortement connexes, qui ont la propriété que tous les sommets d'une composante soient accessibles depuis les autres et réciproquement. Mais il est impossible d'aller d'un sommet d'une composante à un sommet d'une autre composante et retour. Deux composantes fortement connexes d'un graphe sont soit entièrement disjointes, soit reliées par un ou plusieurs arcs qui vont tous dans le même sens (les arcs partent tous de la même composante pour arriver dans l'autre)

Les composantes fortement connexes du graphe-exemple du début de ce chapitre (figure 11.1.) sont :

B
K
H I
D E F
A C G J L M

Dans cet exemple, le sommet A est dans une autre composante connexe que F car il y a bien un chemin menant de A à F mais il n'y a aucun moyen d'aller de F à A.

A l'aide de la matrice de fermeture transitive (appelons la M), il est possible de déterminer si deux sommets x et y font partie de la même composante fortement connexe : il suffit que M[x,y]=M[y,x]=1.

Comme on peut s'y attendre, la méthode pour déterminer les composantes fortement connexes d'un graphe orienté repose sur l'algorithme de DFS. La variante de l'algorithme de DFS proposée plus loin est due à R.E. Tarjan (1972) dont l'article entier mérite d'être étudié.

Référence:
R.E. Tarjan
"Depth-first search and linear graph algorithms"
SIAM Journal on Computing, vol. 1, No. 2 (1972).

Cette version modifiée de DFS se présente comme une fonction et, dans la réalisation considérée ici, elle travaille sur une représentation par chaîne de connectivité avec les déclarations suivantes :

const MaxNbSommets = ...;

type Lien = ^Sommet;
     Sommet = record
        v: integer;   {v=numéro du sommet}
        Next: Lien;   {  par hash-coding}
        end; { Sommet }

var PremierVoisin:array[1..MaxNbSommets] of Lien;
    CmptrVisite : 0..MaxNbSommets; {Nb. sommets déjà visités}
    NumDOrdre : array[1..MaxNbSommets] of integer;
    Nom : array[1..MaxNbSommets] of char; {noms des sommets}
    Pile : array[1..MaxNbSommets] of 1..MaxNbSommets;
    SommetDePile : integer;
    NbSommets : 0..MaxNbSommets; {nombre de sommets}

function Visite(IndexSommet:integer) : integer;
{retourne le numéro d'ordre du sommet le
 plus ancien rencontré dans le cadre du
 parcours récursif des successeurs du
 sommet dont l'index est passé en param.}

var Courant: Lien;
    Anciennete,LePlusAncien : integer;

begin { Visite }
  CmptrVisite := CmptrVisite+1;
  NumDOrdre[IndexSommet] := CmptrVisite;
  LePlusAncien := CmptrVisite;
  Pile [SommetDePile] := IndexSommet;
  SommetDePile := SommetDePile+1;
  Courant:= PremierVoisin[IndexSommet];

  while Courant <> nil do begin

    if NumDOrdre[Courant^.v]=0 then begin
      Anciennete := Visite (Courant^.v);
      if Anciennete <LePlusAncien then
        LePlusAncien := Anciennete;
      end { then }
    else if NumDOrdre[Courant^.v] < LePlusAncien then
      LePlusAncien:=NumDOrdre[Courant^.v];
    Courant := Courant^.Next;
    end; { while }
  if LePlusAncien = NumDOrdre[IndexSommet] then begin

    { on arrive ici s'il n'y a pas eu de changement dans
      la boucle while, ou si l'on est revenu du traitement
      récursif des suivants vers le plus ancien }

    repeat
      SommetDePile := SommetDePile-1;
      write (Nom[Pile[SommetDePile]]);
      NumDOrdre[Pile[SommetDePile]] :=NbSommets+1;
    until Pile[SommetDePile]=IndexSommet;

    writeln;
    end; { if }

  Visite := LePlusAncien;

end; { Visite }

Explication de la fonction Visite :

  1. Le nouveau sommet IndexSommet est mis sur la pile et on explore ensuite la chaîne de connectivité associée au sommet IndexSommet (Courant := PremierVoisin[IndexSommet])

  2. On poursuit la chaîne jusqu'à la fin.

  3. Pour chaque élément de la chaîne, on détermine s'il a déjà été visité (NumDOrdre[Courant^.v] <> 0), sinon on appelle récursivement la fonction Visite qui associe à chaque sommet une valeur entière Anciennete. Si cette valeur est plus petite que le minimum courant (LePlusAncien) courant, elle devient le nouveau minimum courant.

  4. A la fin du traitement d'une chaîne de connectivité on détermine si le plus petit numéro d'ordre rencontré est égal à la valeur du numéro d'ordre de visite du sommet IndexSommet. Si oui, cela signifie que tous les sommets qui ont été rencontrés (et visités) depuis le sommet IndexSommet font partie de la même composante fortement connexe que IndexSommet. On procède alors aux actions suivantes (E, F et G) :

  5. On désempile ces sommets que l'on imprime les uns après les autres, jusqu'à revenir au sommet IndexSommet et on met le numéro d'ordre de chacun de ces sommets à une valeur impossible (NbSommets+1) pour être sûr de ne pas les retraiter.

  6. On imprime une ligne blanche lorsqu'on a fini d'imprimer tous les sommets d'une composante fortement connexe.

  7. On affecte la dernière valeur de LePlusAncien à la fonction Visite et on insère dans le tableau NumDOrdre[IndexSommet] une valeur normalement impossible: NbSommets+1.

Pour mieux comprendre le fonctionnement de cet algorithme, il faut se rappeler que les sommets d'une composante fortement connexe font partie d'un même cycle voire de plusieurs (par exemple, dans la figure 11.1, GJL et AGC forment deux cycles faisant partie de la même composante fortement connexe). C'est donc en revenant sur un sommet déjà rencontré le long du chemin courant que l'on s'aperçoit de ces cycles et que l'on peut ainsi déterminer les composantes fortement connexes du graphe.

Comme il peut y avoir plusieurs cycles dans la même composante, la fonction Visite retourne le plus ancien des noeuds rencontrés le long d'un chemin. Cette valeur de retour n'a d'intérêt que lorsque la fonction Visite est appelée récursivement (dans la partie C).

Si la fonction Visite retourne un numéro plus ancien (plus petit) que le numéro de noeud courant, c'est que ce noeud courant fait partie d'un cycle sans en être le point de départ (c'est-à-dire que ce noeud n'est pas le plus ancien noeud de la composante à être traité).

Si la fonction Visite retourne un numéro égal au numéro de noeud courant, c'est que ce noeud courant est le point de départ d'une composante fortement connexe.

Si la fonction Visite retourne un numéro supérieur au numéro de noeud courant, c'est que ce noeud courant fait partie d'un chemin qui ne constitue pas un cycle, le noeud courant constitue donc une composante fortement connexe à lui tout seul.

Prenons pour exemple un graphe comportant deux cycles imbriqués :

En commençant le parcours par le sommet A, l'on est amené à traiter le sommet B puis le sommet C. Lors du traitement du sommet C, l'on se rend compte que C fait partie d'un cycle car le plus ancien de ses successeurs (A) a un numéro d'ordre plus petit (1) que le sien (3). On ne fera pourtant rien encore.

Après avoir traité C, l'on revient au traitement des successeurs de B. On arrive donc à D pour lequel le plus ancien successeur est C. Là encore, l'on se rend compte que D fait partie d'un cycle car le plus ancien de ses successeurs (C) a un numéro d'ordre plus petit (3) que le sien (4). On ne fera une fois de plus rien d'autre.

Le traitement des successeurs de B est terminé et le plus ancien sommet atteint est A. B fait donc aussi partie d'un cycle dont il n'est pas le point de départ. C'est en revenant au traitement des successeurs de A que l'on se rend compte que le plus ancien sommet que l'on peut atteindre depuis A est A lui-même. On a donc détecté une composante fortement connexe partant de A et comprenant B, C et D.

Bien évidemment, la fonction Visite peut faire beaucoup plus que simplement imprimer les noms des sommets d'une composante fortement connexe. Pour cela il suffit de remplacer, dans la partie E, l'instruction

    write(Nom[Pile[SommetDePile]])

par l'appel à une procédure de traitement P; on obtient alors :

    if LePlusAncien = NumDOrdre(IndexSommet) then
      repeat
        SommetDePile := SommetDePile-1;
        P(Nom[Pile[SommetDePile]])
      until Pile[SommetDePile] = IndexSommet;

12. Structures récursives non linéaires: les réseaux

Table des matières.

Site Hosting: Bronco