Repérez les composantes fortement connexes
Sur un site web, certaines pages se renvoient les unes aux autres, et d’autres sont des culs-de-sac : une fois arrivé, impossible de revenir. Dans un graphe orienté, « être relié » ne suffit plus, il faut savoir si l’on peut aller et revenir. C’est la notion de forte connexité.
Dans ce chapitre, vous allez apprendre à calculer tout ce qui est accessible depuis chaque sommet (la fermeture transitive), puis à découper un graphe orienté en blocs où tout le monde peut atteindre tout le monde. Notre fil rouge : un petit site web de sept pages.
Le fil rouge : un site de sept pages
Section intitulée « Le fil rouge : un site de sept pages »Les pages du site sont numérotées : 1 accueil, 2 blog, 3 article, 4 catalogue, 5 fiche produit, 6 avis clients, 7 mentions légales. Un arc signifie « la page contient un lien vers la page » :
1 → 2, 2 → 3, 3 → 1, 2 → 4, 4 → 5, 5 → 6, 6 → 4, 6 → 7, 3 → 7.
Un internaute qui arrive sur l’accueil peut-il atteindre les avis clients ? Et revenir ensuite à l’accueil en suivant des liens ? Les cadres en pointillés donnent déjà la réponse, mais voyons comment un algorithme la trouve.
Descendants, ascendants et forte connexité
Section intitulée « Descendants, ascendants et forte connexité »Qui peut atteindre qui ?
Section intitulée « Qui peut atteindre qui ? »Pour un sommet :
- ses descendants sont les sommets qu’on peut atteindre depuis par un chemin ;
- ses ascendants sont les sommets depuis lesquels on peut atteindre .
Les descendants s’obtiennent par un simple parcours (BFS ou DFS) depuis , les ascendants par un parcours sur le graphe inversé (chaque arc retourné). Pour la page 4, et .
Forte connexité
Section intitulée « Forte connexité »Un graphe orienté est fortement connexe si, pour toute paire de sommets et , il existe un chemin de à et un chemin de à . Le réseau de navettes du chapitre 1 l’est ; notre site ne l’est pas (depuis la page 7, on ne va nulle part).
Comme pour la connexité simple, « et s’atteignent mutuellement » est une relation d’équivalence. Ses classes sont les composantes fortement connexes (CFC, strongly connected components). Et elles se calculent très simplement à partir des deux ensembles précédents :
en convenant que appartient à ses propres descendants et ascendants. En mots : la composante de contient les sommets qu’il atteint et qui l’atteignent.
Un graphe orienté possède aussi des notions plus faibles. Un sommet est une racine s’il atteint tous les autres (ici, 1, 2 et 3 en sont). Un graphe est quasi fortement connexe si, pour toute paire , un même sommet atteint à la fois et ; on montre qu’un graphe fini admet une racine si et seulement s’il est quasi fortement connexe.
La fermeture transitive et l’algorithme de Roy-Warshall
Section intitulée « La fermeture transitive et l’algorithme de Roy-Warshall »Tout calculer d’un coup
Section intitulée « Tout calculer d’un coup »Plutôt que de lancer un parcours par sommet, on peut calculer en une fois toutes les relations d’accessibilité. La fermeture transitive de , notée , est le graphe qui a un arc dès qu’il existe un chemin (d’au moins un arc) de à dans .
L’algorithme de Roy-Warshall la calcule sur la matrice d’adjacence , avec une idée simple : on autorise progressivement les chemins à passer par les sommets 1, puis 1 et 2, puis 1, 2 et 3, etc.
pour k de 1 à n : pour tout x tel que M[x, k] = 1 : (x atteint k) pour tout y tel que M[k, y] = 1 : (k atteint y) M[x, y] ← 1 (donc x atteint y, en passant par k)Après l’étape , si et seulement s’il existe un chemin de à dont tous les sommets intermédiaires sont parmi . À la fin (), plus aucune restriction : on a la fermeture transitive. Trois boucles imbriquées sur : la complexité est en .
def roy_warshall(M): """Fermeture transitive d'une matrice d'adjacence (liste de listes de 0/1).""" n = len(M) F = [ligne[:] for ligne in M] # copie : on ne modifie pas l'original for k in range(n): for x in range(n): if F[x][k]: # x atteint k for y in range(n): if F[k][y]: # k atteint y F[x][y] = 1 return FUne même boucle, beaucoup d’usages
Section intitulée « Une même boucle, beaucoup d’usages »La structure de Roy-Warshall est bien plus générale qu’il n’y paraît. Si la matrice contient des valeurs (des longueurs) au lieu de 0 et 1, et que l’on remplace « ou » par « min » et « et » par « + », la mise à jour devient :
où est la meilleure longueur connue d’un chemin de à . On obtient alors les plus courts chemins entre tous les couples : c’est l’algorithme de Floyd-Warshall du chapitre 7. Avec (max, +), on obtient les plus longs chemins (utiles en ordonnancement, sur un graphe sans circuit) ; avec (max, min), le chemin de plus grande capacité. Les sujets d’examen demandent volontiers d’adapter Roy-Warshall à un nouveau problème : il suffit de dire ce que représente et comment combiner deux demi-chemins.
Calculer les composantes fortement connexes
Section intitulée « Calculer les composantes fortement connexes »La méthode de Foulkes, par la fermeture transitive
Section intitulée « La méthode de Foulkes, par la fermeture transitive »Une fois calculée, les composantes se lisent directement. Deux sommets distincts et sont dans la même CFC si et seulement si . Il suffit donc de comparer la matrice à sa transposée : les lignes 1, 2 et 3 s’atteignent mutuellement, de même que 4, 5 et 6, et 7 reste seul. C’est la méthode vue en cours (algorithme de Foulkes). Elle est simple à dérouler à la main, mais coûte à cause de la fermeture transitive.
Les algorithmes linéaires : Tarjan et Kosaraju
Section intitulée « Les algorithmes linéaires : Tarjan et Kosaraju »Pour de grands graphes, on utilise des algorithmes en , fondés sur le DFS du chapitre 2 (ce sont des compléments standard, hors du cours magistral) :
- Kosaraju fait deux DFS : un premier sur pour ordonner les sommets par date de fin décroissante, puis un second sur le graphe inversé, dans cet ordre. Chaque arbre du second parcours est une CFC.
- Tarjan n’en fait qu’un, en maintenant pour chaque sommet le plus petit numéro de visite qu’il peut atteindre ; un sommet dont ce numéro est le sien est la « tête » d’une composante.
Le graphe réduit
Section intitulée « Le graphe réduit »Si l’on contracte chaque CFC en un seul sommet, en gardant un arc entre deux composantes dès qu’un arc les relie dans , on obtient le graphe réduit (ou condensation). Pour notre site : , , , avec les arcs (grâce à 2 → 4), (6 → 7) et (3 → 7).
Cette dernière propriété est précieuse : tout graphe orienté se décompose en blocs « circulaires » reliés par une structure sans circuit, que l’on sait traiter efficacement (chapitre 4).
Enfin, on appelle graphe τ-minimal (tau-minimal) un graphe partiel de qui a la même fermeture transitive avec le moins d’arcs possible. Ici, l’arc 3 → 7 est superflu (on atteint déjà 7 par 3 → 1 → 2 → 4 → 5 → 6 → 7) : les 8 autres arcs forment un graphe τ-minimal. C’est l’idée d’une « réduction transitive » : ne garder que les liens indispensables.
import networkx as nx
site = nx.DiGraph([(1, 2), (2, 3), (3, 1), (2, 4), (4, 5), (5, 6), (6, 4), (6, 7), (3, 7)])print(list(nx.strongly_connected_components(site))) # [{7}, {4, 5, 6}, {1, 2, 3}]reduit = nx.condensation(site) # graphe réduit, sans circuitprint(nx.is_directed_acyclic_graph(reduit)) # Truefermeture = nx.transitive_closure(site, reflexive=False)print(sorted(fermeture.successors(4))) # [4, 5, 6, 7]En résumé
Section intitulée « En résumé »- Dans un graphe orienté, et sont les sommets atteints depuis et ceux qui atteignent ; un parcours suffit à les calculer.
- Un graphe est fortement connexe si tout sommet atteint tout autre ; ses classes sont les composantes fortement connexes, .
- La fermeture transitive se calcule par Roy-Warshall en , en autorisant progressivement les sommets intermédiaires (boucle sur à l’extérieur).
- Roy-Warshall se généralise en changeant les opérations : (min, +) donne les plus courts chemins entre tous les couples.
- Méthode de Foulkes : et sont dans la même CFC si ; Tarjan et Kosaraju font la même chose en .
- Le graphe réduit est toujours sans circuit.
Exercices
Section intitulée « Exercices »Exercice · Lire des composantes dans une fermeture
facileOn ajoute au site un lien de la page 7 (mentions légales) vers la page 1 (accueil). Quelles sont les nouvelles composantes fortement connexes ? Le graphe devient-il fortement connexe ?
Voir la solution
Depuis 7, on atteint maintenant 1, donc tout le bloc , puis par 2 → 4. Et tous les sommets atteignaient déjà 7 (1, 2, 3 par 3 → 7 ; 4, 5, 6 par 6 → 7). Donc tout sommet atteint tout autre en passant par 7 : il n’y a plus qu’une seule composante, , et le graphe est fortement connexe. Un seul arc bien placé a suffi, parce que le graphe réduit était une chaîne : relier la dernière composante à la première referme tout.
En code
def descendants(graphe, x): """Sommets atteignables depuis x (x compris), par un parcours en profondeur.""" vu, pile = {x}, [x] while pile: for y in graphe[pile.pop()]: if y not in vu: vu.add(y) pile.append(y) return vu
def cfc(graphe): inverse = {x: [] for x in graphe} # graphe inversé, pour les ascendants for x in graphe: for y in graphe[x]: inverse[y].append(x) blocs, classes = [], set() for x in graphe: if x not in classes: bloc = descendants(graphe, x) & descendants(inverse, x) # Desc(x) ∩ Asc(x) blocs.append(sorted(bloc)) classes |= bloc return blocs
site = {1: [2], 2: [3, 4], 3: [1, 7], 4: [5], 5: [6], 6: [4, 7], 7: []}print(cfc(site)) # [[1, 2, 3], [4, 5, 6], [7]]site[7] = [1] # nouveau lien 7 -> 1print(cfc(site)) # [[1, 2, 3, 4, 5, 6, 7]] : fortement connexeroy_warshall <- function(M) { for (k in 1:nrow(M)) for (x in 1:nrow(M)) if (M[x, k] == 1) M[x, M[k, ] == 1] <- 1 # x atteint k, et k atteint ces y M}cfc <- function(M) { # méthode de Foulkes F <- roy_warshall(M) mutuel <- (F == 1 & t(F) == 1) | diag(nrow(M)) == 1 unique(lapply(1:nrow(M), function(x) which(mutuel[x, ])))}
arcs <- rbind(c(1, 2), c(2, 3), c(3, 1), c(2, 4), c(4, 5), c(5, 6), c(6, 4), c(6, 7), c(3, 7))M <- matrix(0, 7, 7); M[arcs] <- 1str(cfc(M)) # 1 2 3 ; 4 5 6 ; 7M[7, 1] <- 1 # nouveau lien 7 -> 1str(cfc(M)) # 1 2 3 4 5 6 7 : fortement connexeExercice · Adapter Roy-Warshall
moyenOn veut savoir, pour chaque couple , s’il existe un chemin de à passant par un sommet donné (qui peut être l’une des extrémités). Proposez une méthode qui utilise la fermeture transitive , puis appliquez-la au site avec : quels couples conviennent ?
Voir la solution
Un chemin de à passe par si et seulement si on peut aller de à , puis de à . En notant complétée par des 1 sur la diagonale (chaque sommet s’atteint lui-même par un chemin vide, ce qui autorise ou ), on pose :
Pour : les sommets qui atteignent 5 (ou sont 5) sont , et ceux atteints depuis 5 (ou 5 lui-même) sont . Tous les couples avec et conviennent, soit couples. Par exemple, (1, 7) convient (1 → 2 → 4 → 5 → 6 → 7), mais (1, 3) non : aucun chemin de 1 à 3 ne peut passer par 5, car on ne revient jamais du bloc vers .
En code
def roy_warshall(M): n = len(M) F = [ligne[:] for ligne in M] for k in range(n): # sommet intermédiaire à l'extérieur for x in range(n): if F[x][k]: for y in range(n): if F[k][y]: F[x][y] = 1 return F
arcs = [(1, 2), (2, 3), (3, 1), (2, 4), (4, 5), (5, 6), (6, 4), (6, 7), (3, 7)]M = [[0] * 7 for _ in range(7)]for x, y in arcs: M[x - 1][y - 1] = 1F = roy_warshall(M)etoile = [[1 if x == y else F[x][y] for y in range(7)] for x in range(7)] # M* : diagonale à 1
z = 5 - 1couples = [(x + 1, y + 1) for x in range(7) for y in range(7) if etoile[x][z] and etoile[z][y]]print(len(couples)) # 24print((1, 7) in couples, (1, 3) in couples) # True Falseroy_warshall <- function(M) { for (k in 1:nrow(M)) for (x in 1:nrow(M)) if (M[x, k] == 1) M[x, M[k, ] == 1] <- 1 # x atteint k, et k atteint ces y M}
arcs <- rbind(c(1, 2), c(2, 3), c(3, 1), c(2, 4), c(4, 5), c(5, 6), c(6, 4), c(6, 7), c(3, 7))M <- matrix(0, 7, 7); M[arcs] <- 1etoile <- roy_warshall(M); diag(etoile) <- 1 # M* : chaque sommet s'atteint lui-même
z <- 5A <- outer(etoile[, z], etoile[z, ]) # A[x, y] = M*[x, z] * M*[z, y]sum(A) # 24c(A[1, 7], A[1, 3]) # 1 0Exercice · Un graphe réduit sans circuit
difficileDémontrez que le graphe réduit d’un graphe orienté quelconque est sans circuit. Déduisez-en que, si un graphe n’est pas fortement connexe, son graphe réduit possède au moins une composante sans arc entrant et une composante sans arc sortant.
Voir la solution
Supposons que le graphe réduit contienne un circuit avec composantes distinctes. Prenons et . Il existe un chemin de à dans : on suit l’arc de vers , en circulant librement à l’intérieur de chaque composante. Il existe aussi un chemin de à : on suit le circuit de la même façon. Donc et s’atteignent mutuellement : ils devraient être dans la même composante, ce qui contredit . Le graphe réduit est donc sans circuit.
Si n’est pas fortement connexe, le graphe réduit a au moins deux sommets. Partons de n’importe quelle composante et suivons des arcs sortants tant que c’est possible : comme il n’y a pas de circuit, on ne repasse jamais par la même composante, et comme il y en a un nombre fini, on finit bloqué sur une composante sans arc sortant (un « puits »). En remontant les arcs entrants de la même façon, on trouve une composante sans arc entrant (une « source »). C’est exactement la page 7 et le bloc de notre site.
En code
def descendants(graphe, x): vu, pile = {x}, [x] while pile: for y in graphe[pile.pop()]: if y not in vu: vu.add(y) pile.append(y) return vu
site = {1: [2], 2: [3, 4], 3: [1, 7], 4: [5], 5: [6], 6: [4, 7], 7: []}inverse = {x: [y for y in site if x in site[y]] for x in site}compo = {x: min(descendants(site, x) & descendants(inverse, x)) for x in site} # étiquette de la CFC
reduit = {c: set() for c in compo.values()} # graphe réduit : un arc entre CFC distinctesfor x in site: for y in site[x]: if compo[x] != compo[y]: reduit[compo[x]].add(compo[y])print(reduit) # {1: {4, 7}, 4: {7}, 7: set()}# un circuit existerait si une CFC c était atteignable depuis l'un de ses successeurs dprint(any(c in descendants(reduit, d) for c in reduit for d in reduit[c])) # Falsesources = [c for c in reduit if all(c not in s for s in reduit.values())]puits = [c for c in reduit if not reduit[c]]print(sources, puits) # [1] [7] : le bloc {1, 2, 3} et la page 7roy_warshall <- function(M) { for (k in 1:nrow(M)) for (x in 1:nrow(M)) if (M[x, k] == 1) M[x, M[k, ] == 1] <- 1 M}
arcs <- rbind(c(1, 2), c(2, 3), c(3, 1), c(2, 4), c(4, 5), c(5, 6), c(6, 4), c(6, 7), c(3, 7))M <- matrix(0, 7, 7); M[arcs] <- 1F <- roy_warshall(M); diag(F) <- 1compo <- sapply(1:7, function(x) min(which(F[x, ] == 1 & F[, x] == 1))) # étiquette de la CFC
etiquettes <- unique(compo)R <- matrix(0, length(etiquettes), length(etiquettes), dimnames = list(etiquettes, etiquettes))for (k in 1:nrow(arcs)) { # graphe réduit a <- compo[arcs[k, 1]]; b <- compo[arcs[k, 2]] if (a != b) R[as.character(a), as.character(b)] <- 1}R # arcs 1 -> 4, 1 -> 7, 4 -> 7any(diag(roy_warshall(R)) == 1) # FALSE : aucun circuitetiquettes[colSums(R) == 0]; etiquettes[rowSums(R) == 0] # source 1, puits 7