Exercice 1 : Composantes fortement connexes (Kosaraju/Tarjan)
🌐 Énoncé du problème / Cadre d’étude
Section titled “🌐 Énoncé du problème / Cadre d’étude”Soit un graphe orienté, où représente l’ensemble des sommets de cardinal et l’ensemble des arcs de cardinal .
Deux sommets et d’un graphe orienté sont dits fortement connexes s’il existe un chemin de vers et un chemin de vers . La relation d’équivalence associée partitionne l’ensemble des sommets en Composantes Fortement Connexes (CFC).
L’objectif de ce problème est d’étudier et de concevoir des algorithmes efficaces en pour identifier ces composantes, ainsi que d’exploiter le graphe réduit (ou graphe des composantes) pour résoudre un problème d’ordonnancement avec contraintes.
On rappelle qu’en Python, un graphe est généralement représenté par des listes d’adjacence : adj est un dictionnaire ou une liste de listes telle que adj[u] contient les voisins sortants de u.
❓ Questions progressives
Section titled “❓ Questions progressives”Partie I : Algorithme de Kosaraju-Sharir
Section titled “Partie I : Algorithme de Kosaraju-Sharir”- Définition du graphe transposé : Écrire une fonction Python
transpose(adj)qui prend en entrée la représentation d’un graphe par listes d’adjacence et renvoie le graphe transposé où , en temps . - Propriété des temps de fin de parcours : Soit un parcours en profondeur (DFS) sur . On note la date de découverte et la date de fin de traitement du sommet . Soit et deux CFC distinctes telles qu’il existe un arc de vers dans . Soit le premier sommet visité par le DFS dans . Montrer que si , alors .
- Synthèse de l’algorithme : En déduire le principe de l’algorithme de Kosaraju-Sharir. Écrire la fonction Python
kosaraju(adj)qui renvoie la liste des CFC (chacune représentée par la liste de ses sommets).
Partie II : Graphe réduit et applications
Section titled “Partie II : Graphe réduit et applications”- Construction du graphe réduit : À partir des CFC obtenues, on définit le graphe réduit où est l’ensemble des CFC et contient un arc de vers () s’il existe et tels que . Écrire une fonction
graphe_reduit(adj, cfc)qui construit ce graphe réduit en temps . - Acyclicité : Démontrer mathématiquement que le graphe réduit est toujours un DAG (Directed Acyclic Graph, ou graphe orienté sans cycle).
- Application (Ordonnancement) : On souhaite exécuter tâches interdépendantes modélisées par . Des tâches appartenant à la même CFC doivent impérativement être exécutées en même temps (ou regroupées). Proposer un algorithme optimal utilisant pour trouver un ordre d’exécution global valide des tâches.
💡 Pistes & Indications méthodologiques
Section titled “💡 Pistes & Indications méthodologiques”- Partie I, Q1 : Parcourez tous les sommets et tous leurs voisins sortants pour reconstruire les listes d’adjacence inversées. Veillez à bien initialiser les listes pour tous les sommets (y compris les éventuels sommets isolés).
- Partie I, Q2 : Raisonnez selon l’appartenance de à ou . Utilisez le théorème des parenthèses propre aux parcours en profondeur.
- Partie I, Q3 : L’algorithme de Kosaraju se décompose en deux phases de DFS : un premier sur pour obtenir l’ordre décroissant des temps de fin, puis un second sur en suivant cet ordre.
- Partie II, Q4 : Pour éviter les arcs multiples dans le graphe réduit, l’utilisation d’une structure de type
setpour les arêtes est recommandée, ou un masquage par les identifiants de composantes. - Partie II, Q5 & Q6 : Si contenait un cycle, que dirait-on de la maximalité des composantes fortement connexes ? Pour l’ordonnancement, pensez à un tri topologique sur le DAG réduit.
📋 Corrigé détaillé pas à pas
Section titled “📋 Corrigé détaillé pas à pas”Partie I : Algorithme de Kosaraju-Sharir
Section titled “Partie I : Algorithme de Kosaraju-Sharir”-
Graphe transposé :
def transpose(adj):n = len(adj)adj_T = [[] for _ in range(n)]for u in range(n):for v in adj[u]:adj_T[v].append(u)return adj_TComplexité : On parcourt chaque sommet et chaque arc une fois, donc la complexité temporelle et spatiale est en .
-
Propriété des temps de fin de parcours : Supposons que et qu’il existe un arc avec et . Au moment où l’on découvre , la composante n’est pas encore totalement explorée. Comme il existe un chemin de vers n’importe quel sommet de et vers (via ), le DFS lancé depuis va atteindre tous les sommets de et de avant de terminer . Par conséquent, tous les sommets de et auront des temps de fin inférieurs à , ou alors le DFS s’étendra d’abord à et s’achèvera dans avant de revenir dans . Plus rigoureusement, le premier sommet atteint dans (noté ) sera découvert depuis , donc . Le parcours dans se terminera avant que l’on ne puisse dépiler , ce qui implique ? Attention, inversons : puisque et sont disjointes et que l’on quitte pour revenir dans (impossible car pas d’arc de vers dans le même sens sans briser la forte connexité), le dernier sommet de à être terminé le sera après tous les sommets de . Ainsi, .
-
Algorithme de Kosaraju-Sharir :
- Phase 1 : On effectue un DFS sur pour enregistrer l’ordre de fin des sommets.
- Phase 2 : On calcule .
- Phase 3 : On effectue un DFS sur en considérant les sommets par ordre décroissant de leur temps de fin calculé à la phase 1. Chaque arbre du DFS sur forme exactement une CFC.
def kosaraju(adj):n = len(adj)visited = [False] * norder = []def dfs1(u):visited[u] = Truefor v in adj[u]:if not visited[v]:dfs1(v)order.append(u)for u in range(n):if not visited[u]:dfs1(u)adj_T = transpose(adj)visited = [False] * ncfc = []def dfs2(u, comp):visited[u] = Truecomp.append(u)for v in adj_T[u]:if not visited[v]:dfs2(v, comp)for u in reversed(order):if not visited[u]:comp = []dfs2(u, comp)cfc.append(comp)return cfc
Partie II : Graphe réduit et applications
Section titled “Partie II : Graphe réduit et applications”-
Construction du graphe réduit : On commence par attribuer à chaque sommet l’identifiant de sa CFC (un entier de à où est le nombre de CFC).
def graphe_reduit(adj, cfc):n = len(adj)k = len(cfc)id_cfc = [0] * nfor i, comp in enumerate(cfc):for u in comp:id_cfc[u] = iadj_red = [set() for _ in range(k)]for u in range(n):u_comp = id_cfc[u]for v in adj[u]:v_comp = id_cfc[v]if u_comp != v_comp:adj_red[u_comp].add(v_comp)# Conversion des sets en listesreturn [list(voisins) for voisins in adj_red]Complexité : L’attribution des IDs prend , et le parcours de tous les arcs prend . La construction est donc en .
-
Démonstration de l’acyclicité : Supposons par l’absurde que contienne un cycle de composantes . Par définition de , cela signifie qu’il existe des arêtes dans allant de vers , de vers , …, et de vers . Par conséquent, pour toute paire de sommets pris respectivement dans et du cycle, il existe un chemin de vers et un chemin de vers (en suivant les chemins internes aux composantes et les arcs inter-composantes du cycle). Tous les sommets de appartiendraient donc à une seule et même composante fortement connexe, ce qui contredit le fait que les CFC formoient une partition stricte de sommets en composantes maximales. Le graphe réduit est donc nécessairement un DAG.
-
Résolution du problème d’ordonnancement : Puisque est un DAG, on peut lui appliquer un tri topologique. L’algorithme global est le suivant :
- Calculer les CFC de via l’algorithme de Kosaraju (ou Tarjan) en .
- Construire le graphe réduit en .
- Effectuer un tri topologique de (par exemple via un DFS ou l’algorithme de Kahn) en .
- L’ordre linéaire obtenu sur les CFC fournit l’ordonnancement séquentiel valide des blocs de tâches à exécuter.