Théorie des graphes et algorithmes gloutons

L'algorithme de Kruskal expliqué

L'algorithme de Kruskal trie toutes les arêtes par poids et conserve chacune sauf si elle fermerait un cycle, tenant une forêt de fragments qui ne devient un arbre qu'à la toute fin. Découvrez la propriété de cycle qui rend le rejet d'une arête prouvablement sûr, suivez un déroulé chiffré à six sommets, et voyez pourquoi union-find est ce qui le rend rapide.

12 min de lecture Mis à jour : août 2026 Niveau avancé
Mohammed Islam Hadjoudj
Mohammed Islam Hadjoudj
Ingénieur expert en recherche opérationnelle

1. Introduction à l'algorithme de Kruskal

L'algorithme de Kruskal construit un arbre couvrant de poids minimal : à partir d'un graphe connexe, non orienté et pondéré, il sélectionne l'ensemble d'arêtes le moins coûteux qui touche tous les sommets sans former de cycle. Pour V sommets, cet ensemble contient toujours exactement V - 1 arêtes.

Là où l'algorithme de Prim fait croître un unique arbre connexe vers l'extérieur depuis un sommet de départ, celui de Kruskal ignore presque entièrement la connexité jusqu'à la fin. Il trie toutes les arêtes du graphe par poids, parcourt cette liste de la moins chère à la plus chère, et conserve chaque arête sauf si elle fermerait un cycle. Rien d'autre n'est consulté. Il n'y a ni sommet de départ ni notion de frontière.

La conséquence est que Kruskal passe l'essentiel de son exécution à tenir une forêt plutôt qu'un arbre : de nombreux petits fragments dispersés dans le graphe, qui croissent et fusionnent indépendamment, et ne se fondent en un seul arbre couvrant qu'avec la toute dernière arête acceptée. C'est cette différence de forme qui le fait se comporter autrement sur les graphes creux, sur une entrée non connexe et sous un profileur.

2. Pourquoi cela fonctionne : la propriété de cycle

La correction de Prim repose sur un théorème indiquant quelles arêtes il est sûr d'accepter. Kruskal passe tout autant de temps à rejeter des arêtes, il vaut donc la peine d'énoncer le théorème complémentaire qui justifie d'en écarter une.

La propriété de cycle. Pour tout cycle du graphe, l'arête de poids maximal sur ce cycle n'appartient pas à l'arbre couvrant de poids minimal, à condition qu'elle soit strictement plus lourde que toutes les autres arêtes du cycle. Si plusieurs arêtes sont ex aequo au sommet, au moins l'une d'elles peut être laissée de côté.

La preuve reflète celle de la propriété de coupe. Supposons que l'arête la plus lourde e d'un cycle appartienne bel et bien à un arbre couvrant de poids minimal T. Retirer e de T le scinde en deux composantes. Le reste de ce cycle relie toujours ces deux composantes, donc une autre arête du cycle f les reconnecte. Remettez f à la place. Vous avez de nouveau un arbre couvrant et, puisque e était strictement la plus lourde, le nouvel arbre est strictement plus léger, ce qui contredit la minimalité de T.

Observez maintenant ce qui se passe quand Kruskal rejette une arête. Il atteint l'arête e et constate que ses deux extrémités sont déjà dans le même fragment. Cela signifie qu'un chemin existe déjà entre elles, bâti uniquement avec des arêtes acceptées plus tôt, donc avec des arêtes pas plus lourdes que e. Ajouter e fermerait un cycle sur lequel e est le membre le plus lourd. Par la propriété de cycle, l'écarter ne coûte rien.

Les arêtes acceptées sont sûres pour la même raison que celles de Prim. Lorsqu'une arête joint deux fragments distincts, elle est l'arête restante la moins chère traversant la coupe qui sépare un fragment de tout le reste, si bien que la propriété de coupe s'applique sans changement. Kruskal est glouton dans les deux sens à la fois, et les deux sens sont des théorèmes.

Le graphe d'exemple avec le cycle A-C-B-A et les poids 1, 2 et 4. L'arête la plus lourde du cycle, A-B de poids 4, est marquée comme exclue de l'arbre couvrant de poids minimal, tandis que les deux arêtes plus légères du cycle sont conservées.
Un aperçu du graphe d'exemple à six sommets présenté à la section 4. Sur le cycle A-C-B-A, l'arête la plus lourde est A-B à 4, et la propriété de cycle dit qu'elle peut être laissée de côté.

3. Comment la forêt fusionne

Concrètement, l'algorithme place au départ chaque sommet dans son propre fragment : V fragments, zéro arête. En parcourant la liste triée, il pose une seule question à chaque arête (u, v) :

Chaque arête acceptée réduit le nombre de fragments d'exactement un. Partir de V fragments et finir à un seul signifie donc précisément V - 1 acceptations, ce qui constitue la condition d'arrêt. Une fois ce nombre atteint, toute arête restante de la liste sera à coup sûr rejetée, la boucle peut donc sortir plus tôt.

L'algorithme entier se réduit ainsi à une unique question de structure de données : comment tester « même fragment ? » et « fusionne ces deux fragments » rapidement, des millions de fois ? C'est exactement ce que fournit union-find.

Un graphe non orienté pondéré à six sommets de A à F. L'arbre couvrant de poids minimal est mis en évidence avec les arêtes A-C de poids 1, B-C de poids 2, D-E de poids 2, E-F de poids 3 et B-D de poids 5, pour un poids total de 13. Les arêtes plus lourdes A-B, C-D, C-E et D-F restent inutilisées.
Un aperçu du graphe d'exemple de la section 4, avec l'arbre que Kruskal va construire : cinq arêtes, poids total 13.

4. Exécution pas à pas

Prenons le graphe à six sommets utilisé tout au long de notre guide des arbres couvrants de poids minimal. Ses neuf arêtes sont :

A-B 4    A-C 1    B-C 2
B-D 5    C-D 6    C-E 7
D-E 2    D-F 8    E-F 3

Les trier par poids donne l'ordre que l'algorithme parcourra réellement :

A-C 1   B-C 2   D-E 2   E-F 3   A-B 4   B-D 5   C-D 6   C-E 7   D-F 8

Chaque sommet commence seul, la forêt démarre donc comme {A} {B} {C} {D} {E} {F}.

  1. A-C (1). Accepter. Fragments différents. La forêt devient {AC} {B} {D} {E} {F}. Total courant 1.
  2. B-C (2). Accepter. B est seul, C est avec A. La forêt devient {ABC} {D} {E} {F}. Total 3.
  3. D-E (2). Accepter. Remarquez que cela bâtit un fragment très loin du premier. La forêt devient {ABC} {DE} {F}. Total 5.
  4. E-F (3). Accepter. La forêt devient {ABC} {DEF}. Total 8.
  5. A-B (4). Rejeter. A et B sont déjà tous deux dans {ABC}, cela fermerait donc le cycle A-C-B-A. Écartée.
  6. B-D (5). Accepter. Cette arête fait le pont entre les deux fragments restants. La forêt devient {ABCDEF}. Total 13.

Cinq arêtes acceptées pour six sommets : l'algorithme s'arrête sans jamais examiner C-D (6), C-E (7) ni D-F (8). L'arbre obtenu est A-C (1), B-C (2), D-E (2), E-F (3), B-D (5), pour un poids total de 13, exactement l'arbre que Prim trouve sur le même graphe.

L'étape 3 est le moment qui distingue cet algorithme. Prim n'aurait pas pu prendre D-E à ce stade, car aucune de ses extrémités ne touchait son arbre en croissance. Kruskal s'en moque : il démarre volontiers un second fragment, sans lien avec le premier, à l'autre bout du graphe, et se souciera plus tard de les relier. Et l'étape 5 montre la propriété de cycle à l'oeuvre, en rejetant précisément l'arête la plus lourde du cycle qu'elle aurait créé.

La forêt que maintient l'algorithme de Kruskal au fil de ses six décisions. Elle débute avec six fragments d'un seul sommet et fusionne pas à pas : AC, puis ABC, puis un DE séparé, puis DEF, A-B étant rejetée, et enfin B-D reliant les deux moitiés en un arbre de poids 13.
Kruskal tient une forêt, pas un arbre. Les fragments croissent indépendamment et ne fusionnent en un arbre couvrant qu'avec la dernière arête acceptée.

5. Union-find : la structure qui le rend rapide

La façon naïve de tester si deux sommets sont déjà reliés consiste à lancer un parcours depuis l'un pour voir si l'on atteint l'autre. Cela coûte O(V) par arête et tire tout l'algorithme vers O(V * E), soit pire que le tri qu'il était censé compléter.

Union-find, aussi appelée structure d'ensembles disjoints, répond aux deux questions en temps quasi constant. Elle maintient chaque fragment sous forme d'arbre de pointeurs vers le parent, avec un représentant à la racine, et expose deux opérations :

Deux optimisations la rendent assez rapide pour disparaître de l'analyse de complexité. L'union par rang rattache toujours l'arbre le plus court sous le plus haut, empêchant la structure de dégénérer en chaîne. La compression de chemin repointe directement vers la racine chaque noeud visité pendant un find, si bien que les recherches répétées deviennent moins chères à mesure que l'algorithme avance.

Avec les deux, une suite de m opérations sur n éléments coûte O(m α(n)), où α est la fonction inverse d'Ackermann. Elle croît si lentement qu'elle reste inférieure à 5 pour toute entrée tenant dans l'univers observable : le travail d'union-find dans Kruskal est donc en pratique linéaire en nombre d'arêtes.

6. Implémentation et pseudo-code

Comme union-find porte la difficulté, l'algorithme lui-même est court.

function Kruskal(V, edges):
    trier edges par poids croissant
    makeSet(v) pour chaque sommet v    // V fragments singletons
    mst = []

    for chaque arête (u, v, w) dans l'ordre trié:
        if find(u) != find(v):         // fragments différents
            union(u, v)
            mst.append((u, v, w))
            if size(mst) == V - 1:     // sortie anticipée : arbre complet
                break

    return mst

Deux détails méritent d'être défendus. Le break n'est pas requis pour la correction, puisque toute arête ultérieure serait de toute façon rejetée, mais sur un graphe dense il saute la majeure partie de la liste. Et le supprimer entièrement n'est pas un bogue : c'est ce qui en fait un algorithme de forêt couvrante de poids minimal, comme expliqué plus bas.

La comparaison find(u) != find(v) est le seul endroit où les cycles sont jamais considérés. Aucune détection explicite de cycle n'apparaît nulle part, et c'est là l'élégance de l'approche : la comptabilité de la connexité s'en charge implicitement.

L'algorithme de Kruskal sur le graphe d'exemple. Les arêtes sont triées par poids et considérées de la moins chère à la plus chère : A-C, B-C, D-E et E-F sont ajoutées, A-B de poids 4 est écartée car elle formerait un cycle, et B-D achève l'arbre. Le poids total est 13.
Ce que produit ce pseudo-code, vu sur le graphe : les arêtes triées parcourues de la moins chère en avant, avec seulement A-B rejetée.

7. Complexité en temps et en espace

Le coût étant concentré en un seul point, toutes les optimisations utiles s'attaquent au tri. Si les arêtes arrivent déjà triées, ou si les poids sont de petits entiers permettant un tri par base ou par comptage, l'algorithme entier tombe à O(E α(V)), très proche du linéaire. Un tri partiel ou un tas paresseux aide également : on a rarement besoin de l'ordre complet, car l'algorithme s'arrête généralement bien avant d'atteindre les arêtes les plus lourdes.

8. Kruskal contre Prim

Les deux sont gloutons, tous deux se justifient par la même paire de théorèmes, et sur un graphe à poids distincts tous deux renvoient le même arbre. Les différences pratiques découlent de ce que chacun garde connexe.

                 Kruskal                      Prim
structure        foret de fragments           un arbre en croissance
piloté par       liste d'aretes triee         file de priorite
exige            union-find                   tas (ou balayage V x V)
cout             O(E log E)                   O(E log V), dense O(V^2)
meilleur sur     graphes creux                graphes denses
non connexe      donne une foret couvrante    ne couvre qu'une composante

La règle pratique est la densité. Sur un graphe creux, E est petit, le tri est bon marché, et Kruskal l'emporte. Sur un graphe denseE approche V2, trier environ V2 arêtes coûte O(V2 log V), tandis que Prim avec matrice d'adjacence tourne en O(V2) tout net et prend la tête.

Comparaison côte à côte de Prim et Kruskal sur le même graphe à six sommets. Prim fait croître un seul arbre connexe depuis A dans l'ordre A, C, B, D, E, F. Kruskal bâtit plusieurs fragments disjoints qui fusionnent à la fin. Tous deux produisent un arbre de poids total 13.
Deux chemins vers la même réponse. Prim garde un arbre connexe du début à la fin ; Kruskal laisse des fragments apparaître partout et les fusionne en dernier.

9. Notes pratiques et pièges courants

Forêts couvrantes et graphes non connexes

C'est ici que Kruskal possède un véritable avantage structurel. Lancez-le sur un graphe non connexe et il n'atteindra tout simplement jamais V - 1 arêtes acceptées. Il épuise la liste d'arêtes, et ce qui reste est une forêt couvrante de poids minimal : l'arbre couvrant minimal de chaque composante connexe, calculé en une passe et sans traitement particulier.

Prim ne peut pas faire cela depuis un unique départ. Lancé sur un sommet, il couvre la composante de ce sommet puis s'arrête, renvoyant en silence un arbre qui paraît valide mais ne couvre qu'une partie du graphe. Récupérer le reste suppose de détecter le manque et de relancer depuis un sommet non visité, une fois par composante. Le compte est d'ailleurs le diagnostic : si Kruskal termine avec V - 1 arêtes acceptées, le graphe était connexe ; s'il en a accepté V - k, le graphe comptait k composantes.

Arbre couvrant de poids maximal

Triez par ordre décroissant plutôt que croissant et chaque ligne de l'algorithme reste valable. La propriété de cycle bascule en un argument de propriété de coupe sur des poids opposés, et vous obtenez l'arbre couvrant le plus lourd. Prendre l'opposé des poids et lancer l'algorithme inchangé fonctionne tout aussi bien.

Reverse-delete

L'image miroir de Kruskal : triez les arêtes de la plus lourde à la plus légère et supprimez chacune sauf si sa suppression déconnecte le graphe. Il se justifie par la même propriété de cycle lue à l'envers, et produit le même arbre. Il est rarement employé car le test de connexité après chaque suppression est bien plus coûteux qu'une requête union-find.

Poids ex aequo

Lorsque plusieurs arêtes partagent un poids, le graphe peut posséder plus d'un arbre couvrant minimal, et celui que vous obtenez dépend de la façon dont votre tri départage les égalités. L'exemple ci-dessus en contient une : B-C et D-E pèsent toutes deux 2. Les arbres obtenus sont tous également optimaux, si bien que tout test comparant à une liste d'arêtes figée est fragile. Comparez plutôt le poids total.

Boucles et arêtes multiples

Une boucle échoue toujours au test find(u) != find(v) et se trouve écartée automatiquement, elle ne demande donc aucun traitement particulier. Parmi des arêtes multiples, la moins chère est atteinte en premier et acceptée, les autres sont ensuite rejetées comme cycles. Kruskal se montre exceptionnellement tolérant aux entrées désordonnées.

10. Applications concrètes

Conception de réseaux et d'infrastructures

La motivation d'origine : relier un ensemble fixe de sites avec le moins de câble, de conduite ou de voie possible. La vision centrée sur les arêtes de Kruskal convient naturellement lorsque l'entrée arrive déjà sous forme de liste de liaisons candidates avec leurs coûts, ce qui est le format habituel de ces données.

Classification hiérarchique

Lancez Kruskal et notez l'ordre dans lequel les fragments fusionnent : vous avez effectué une classification ascendante à lien simple. La séquence des fusions est précisément le dendrogramme, et s'arrêter plus tôt à V - k arêtes laisse exactement k groupes. Cette équivalence explique pourquoi les arbres couvrants minimaux apparaissent si souvent en apprentissage non supervisé.

Segmentation d'images

En traitant les pixels comme des sommets et les différences d'intensité comme des poids d'arêtes, l'algorithme de segmentation de Felzenszwalb et Huttenlocher est pour l'essentiel Kruskal, doté d'un prédicat de fusion comparant la variation interne d'un fragment au poids de l'arête.

Conception de circuits et routage

Minimiser la longueur totale de piste entre des plots fixes est un problème d'arbre couvrant minimal, et la liste d'arêtes des tracés candidats est justement ce qu'un routeur a déjà sous la main.

11. Ressources académiques et histoire

Contrairement à l'algorithme de Prim, découvert indépendamment au moins trois fois, celui-ci possède une attribution nette.

Pour l'histoire complète de l'attribution du problème, voyez Graham et Hell. Pour des preuves rigoureuses des propriétés de coupe et de cycle ainsi que des deux algorithmes, la référence standard est Cormen, Leiserson, Rivest et Stein, Introduction to Algorithms, au chapitre sur les arbres couvrants de poids minimal. Les références complètes figurent à la fin de cet article.

Foire aux questions

Pourquoi l'algorithme de Kruskal peut-il rejeter une arête sans risque ?

Grâce à la propriété de cycle : l'arête la plus lourde d'un cycle peut être laissée hors de l'arbre couvrant de poids minimal. Quand Kruskal rejette une arête, ses deux extrémités sont déjà dans le même fragment, ce qui signifie qu'un chemin existe déjà entre elles, bâti avec des arêtes acceptées plus tôt et donc pas plus lourdes que celle-ci. L'arête rejetée est la plus lourde du cycle qu'elle aurait fermé, l'écarter ne coûte donc rien.

Pourquoi l'algorithme de Kruskal a-t-il besoin d'union-find ?

L'algorithme doit demander, pour chaque arête, si ses deux extrémités sont déjà reliées. Y répondre par un parcours de graphe coûte O(V) par arête et dominerait tout le temps d'exécution. Union-find y répond en temps quasi constant grâce à l'union par rang et à la compression de chemin, donnant O(E alpha(V)) pour tout le travail de connexité, où alpha est la fonction inverse d'Ackermann et reste inférieure à 5 pour toute entrée réaliste.

Quand faut-il utiliser Kruskal plutôt que Prim ?

Préférez Kruskal sur les graphes creux, lorsque l'entrée arrive déjà sous forme de liste d'arêtes, ou lorsque le graphe peut être non connexe. Son coût est dominé par le tri en O(E log E), bon marché quand E est petit, et sur un graphe non connexe il produit naturellement une forêt couvrante de poids minimal en une seule passe. Préférez Prim sur les graphes denses, où une implémentation à matrice d'adjacence tourne en O(V^2) tout net et évite de trier environ V au carré arêtes.

Regardez Kruskal accepter et rejeter les arêtes

Triez les arêtes, puis voyez union-find laisser passer chacune ou la refuser. Exécutez Kruskal sur un graphe, pas à pas.

Ouvrir le visualiseur Kruskal

Références vérifiées et lectures complémentaires