Skip to content

Exercice 1 : Composantes fortement connexes (Kosaraju/Tarjan)

🌐 Énoncé du problème / Cadre d’étude

Section titled “🌐 Énoncé du problème / Cadre d’étude”

Soit G=(S,A)G = (S, A) un graphe orienté, où SS représente l’ensemble des sommets de cardinal n=∣S∣n = |S| et AA l’ensemble des arcs de cardinal m=∣A∣m = |A|.

Deux sommets uu et vv d’un graphe orienté sont dits fortement connexes s’il existe un chemin de uu vers vv et un chemin de vv vers uu. 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 O(n+m)\mathcal{O}(n + m) 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.


  1. 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é GT=(S,AT)G^T = (S, A^T) où AT={(v,u)∣(u,v)∈A}A^T = \{(v, u) \mid (u, v) \in A\}, en temps O(n+m)\mathcal{O}(n + m).
  2. Propriété des temps de fin de parcours : Soit un parcours en profondeur (DFS) sur GG. On note d(u)d(u) la date de découverte et f(u)f(u) la date de fin de traitement du sommet uu. Soit CC et C′C' deux CFC distinctes telles qu’il existe un arc de CC vers C′C' dans GG. Soit u0u_0 le premier sommet visité par le DFS dans C∪C′C \cup C'. Montrer que si u0∈Cu_0 \in C, alors max⁡u∈Cf(u)<max⁡v∈C′f(v)\max_{u \in C} f(u) < \max_{v \in C'} f(v).
  3. 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”
  1. Construction du graphe réduit : À partir des CFC obtenues, on définit le graphe réduit Gred=(Sred,Ared)G^{red} = (S^{red}, A^{red}) où SredS^{red} est l’ensemble des CFC et AredA^{red} contient un arc de C1C_1 vers C2C_2 (C1≠C2C_1 \neq C_2) s’il existe u∈C1u \in C_1 et v∈C2v \in C_2 tels que (u,v)∈A(u, v) \in A. Écrire une fonction graphe_reduit(adj, cfc) qui construit ce graphe réduit en temps O(n+m)\mathcal{O}(n + m).
  2. Acyclicité : Démontrer mathématiquement que le graphe réduit GredG^{red} est toujours un DAG (Directed Acyclic Graph, ou graphe orienté sans cycle).
  3. Application (Ordonnancement) : On souhaite exécuter nn tâches interdépendantes modélisées par GG. 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 GredG^{red} 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 u0u_0 à CC ou C′C'. 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 GG pour obtenir l’ordre décroissant des temps de fin, puis un second sur GTG^T en suivant cet ordre.
  • Partie II, Q4 : Pour éviter les arcs multiples dans le graphe réduit, l’utilisation d’une structure de type set pour les arêtes est recommandée, ou un masquage par les identifiants de composantes.
  • Partie II, Q5 & Q6 : Si GredG^{red} 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.

  1. 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_T

    Complexité : On parcourt chaque sommet et chaque arc une fois, donc la complexité temporelle et spatiale est en O(n+m)\mathcal{O}(n + m).

  2. Propriété des temps de fin de parcours : Supposons que u0∈Cu_0 \in C et qu’il existe un arc (x,y)(x, y) avec x∈Cx \in C et y∈C′y \in C'. Au moment où l’on découvre u0u_0, la composante C∪C′C \cup C' n’est pas encore totalement explorée. Comme il existe un chemin de u0u_0 vers n’importe quel sommet de CC et vers C′C' (via x→yx \to y), le DFS lancé depuis u0u_0 va atteindre tous les sommets de CC et de C′C' avant de terminer u0u_0. Par conséquent, tous les sommets de CC et C′C' auront des temps de fin ff inférieurs à f(u0)f(u_0), ou alors le DFS s’étendra d’abord à C′C' et s’achèvera dans C′C' avant de revenir dans CC. Plus rigoureusement, le premier sommet atteint dans C′C' (noté ww) sera découvert depuis CC, donc d(u0)<d(w)d(u_0) < d(w). Le parcours dans C′C' se terminera avant que l’on ne puisse dépiler u0u_0, ce qui implique max⁡v∈C′f(v)<max⁡u∈Cf(u)\max_{v \in C'} f(v) < \max_{u \in C} f(u) ? Attention, inversons : puisque CC et C′C' sont disjointes et que l’on quitte C′C' pour revenir dans CC (impossible car pas d’arc de C′C' vers CC dans le même sens sans briser la forte connexité), le dernier sommet de CC à être terminé le sera après tous les sommets de C′C'. Ainsi, max⁡v∈C′f(v)<max⁡u∈Cf(u)\max_{v \in C'} f(v) < \max_{u \in C} f(u).

  3. Algorithme de Kosaraju-Sharir :

    • Phase 1 : On effectue un DFS sur GG pour enregistrer l’ordre de fin des sommets.
    • Phase 2 : On calcule GTG^T.
    • Phase 3 : On effectue un DFS sur GTG^T en considérant les sommets par ordre décroissant de leur temps de fin calculé à la phase 1. Chaque arbre du DFS sur GTG^T forme exactement une CFC.
    def kosaraju(adj):
    n = len(adj)
    visited = [False] * n
    order = []
    def dfs1(u):
    visited[u] = True
    for 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] * n
    cfc = []
    def dfs2(u, comp):
    visited[u] = True
    comp.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”
  1. Construction du graphe réduit : On commence par attribuer à chaque sommet l’identifiant de sa CFC (un entier de 00 à k−1k-1 où kk est le nombre de CFC).

    def graphe_reduit(adj, cfc):
    n = len(adj)
    k = len(cfc)
    id_cfc = [0] * n
    for i, comp in enumerate(cfc):
    for u in comp:
    id_cfc[u] = i
    adj_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 listes
    return [list(voisins) for voisins in adj_red]

    Complexité : L’attribution des IDs prend O(n)\mathcal{O}(n), et le parcours de tous les arcs prend O(m)\mathcal{O}(m). La construction est donc en O(n+m)\mathcal{O}(n + m).

  2. Démonstration de l’acyclicité : Supposons par l’absurde que GredG^{red} contienne un cycle de composantes C0,C1,…,Cp=C0C_0, C_1, \dots, C_p = C_0. Par définition de GredG^{red}, cela signifie qu’il existe des arêtes dans GG allant de C0C_0 vers C1C_1, de C1C_1 vers C2C_2, …, et de CpC_p vers C0C_0. Par conséquent, pour toute paire de sommets (u,v)(u, v) pris respectivement dans CiC_i et CjC_j du cycle, il existe un chemin de uu vers vv et un chemin de vv vers uu (en suivant les chemins internes aux composantes et les arcs inter-composantes du cycle). Tous les sommets de C0,C1,…,Cp−1C_0, C_1, \dots, C_{p-1} 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 GredG^{red} est donc nécessairement un DAG.

  3. Résolution du problème d’ordonnancement : Puisque GredG^{red} est un DAG, on peut lui appliquer un tri topologique. L’algorithme global est le suivant :

    1. Calculer les CFC de GG via l’algorithme de Kosaraju (ou Tarjan) en O(n+m)\mathcal{O}(n + m).
    2. Construire le graphe réduit GredG^{red} en O(n+m)\mathcal{O}(n + m).
    3. Effectuer un tri topologique de GredG^{red} (par exemple via un DFS ou l’algorithme de Kahn) en O(k+mred)=O(n+m)\mathcal{O}(k + m_{red}) = \mathcal{O}(n + m).
    4. L’ordre linéaire obtenu sur les CFC fournit l’ordonnancement séquentiel valide des blocs de tâches à exécuter.