Théorie des graphes et algorithmes gloutons

L'algorithme de Prim expliqué

L'algorithme de Prim fait croître un arbre couvrant de poids minimal vers l'extérieur depuis un seul sommet, en prenant à chaque fois l'arête la moins chère de la frontière. Découvrez la propriété de coupe qui rend le choix glouton prouvablement sûr, suivez un déroulé chiffré à six sommets, et voyez pourquoi une seule ligne le sépare de l'algorithme de Dijkstra.

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 Prim

L'algorithme de Prim construit un arbre couvrant de poids minimal : pour un graphe connexe, non orienté et pondéré, il sélectionne un sous-ensemble d'arêtes qui touche tous les sommets, ne contient aucun cycle et possède le plus petit poids total possible. Dans un graphe à V sommets, ce sous-ensemble contient toujours exactement V - 1 arêtes.

La stratégie consiste à faire croître un unique arbre vers l'extérieur. Partez d'un sommet quelconque, puis franchissez à répétition la frontière de ce que vous avez déjà bâti pour ramener l'arête la moins chère qui touche un sommet que vous ne possédez pas encore. Répétez V - 1 fois et l'arbre est terminé. Il n'y a pas de retour en arrière et rien n'est jamais retiré.

Cette description semble presque trop gloutonne pour être correcte. Choisir l'arête la moins chère disponible sur l'instant, sans anticipation, est exactement la stratégie qui échoue pour les plus courts chemins dès qu'une arête négative apparaît. Pour les arbres couvrants de poids minimal elle n'échoue pas, et la raison tient en un seul théorème qu'il vaut mieux comprendre avant de toucher au code.

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

Une coupe partage les sommets en deux groupes non vides. Une arête traverse la coupe si ses deux extrémités tombent dans des groupes différents. Le théorème qui fonde l'algorithme de Prim est le suivant :

La propriété de coupe. Pour toute coupe du graphe, l'arête de poids minimal qui la traverse appartient à un arbre couvrant de poids minimal. Si tous les poids d'arêtes sont distincts, cette arête appartient à l'arbre couvrant de poids minimal, qui est alors unique.

La preuve est courte et mérite d'être vue, car elle explique tout l'algorithme. Soit e l'arête la moins chère traversant une coupe, et supposons qu'un arbre couvrant de poids minimal T ne la contienne pas. Ajouter e à T crée exactement un cycle, et ce cycle doit traverser la coupe une seconde fois, par une autre arête f. Retirez f. Vous avez toujours un arbre couvrant et, comme e était l'arête traversante la moins chère, son poids n'excède pas celui de f, si bien que le nouvel arbre n'est pas plus lourd. Il existe donc un arbre couvrant de poids minimal contenant e.

Regardez maintenant ce que fait l'algorithme de Prim à chaque étape. Les sommets déjà dans l'arbre forment un côté d'une coupe, et tout le reste forme l'autre. L'algorithme sélectionne l'arête la moins chère traversant précisément cette coupe. Par la propriété de coupe, chaque arête qu'il retient est sûre : elle appartient à un arbre couvrant de poids minimal. Le choix glouton n'est jamais un pari qui se trouve payant, c'est un théorème appliqué V - 1 fois.

Le graphe d'exemple avec A et C dans l'arbre. Une ligne pointillée marque la coupe. Les quatre arêtes qui la traversent sont A-B de poids 4, B-C de poids 2, C-D de poids 6 et C-E de poids 7. La moins chère, B-C de poids 2, est mise en évidence comme le choix sûr.
Un aperçu du graphe d'exemple à six sommets présenté à la section 4. A et C étant déjà dans l'arbre, quatre arêtes traversent la coupe ; Prim prend la moins chère, B-C à 2, et la propriété de coupe garantit qu'elle est sûre.

3. Comment l'arbre grandit

Concrètement, l'algorithme maintient trois choses : l'ensemble des sommets déjà dans l'arbre, une valeur key pour chaque sommet à l'extérieur, et un pointeur parent retenant quel sommet de l'arbre a offert cette clé.

Chaque tour prend le sommet hors de l'arbre dont la key est la plus petite, l'ajoute avec l'arête vers son parent, puis relâche : pour chaque voisin w encore hors de l'arbre, si l'arête vers w est moins chère que key[w], abaissez key[w] et redirigez parent[w].

Notez bien ce que représente la clé. C'est le poids d'une seule arête, pas le coût d'un chemin. Ce détail unique est ce qui sépare cet algorithme de celui de Dijkstra, un point sur lequel il faudra revenir une fois le code posé.

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.
Le graphe d'exemple et son arbre couvrant de poids minimal : 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

Démarrez en A. À chaque tour, la « frontière » est l'ensemble des arêtes ayant exactement une extrémité dans l'arbre, et l'algorithme prend la moins chère d'entre elles.

  1. Arbre = {A}. Frontière : A-C (1), A-B (4). La moins chère est A-C (1). Ajoutez C.
  2. Arbre = {A, C}. Frontière : B-C (2), A-B (4), C-D (6), C-E (7). Remarquez que B est désormais atteignable de deux façons, à 4 via A et à 2 via C, sa clé tombe donc à 2. La moins chère est B-C (2). Ajoutez B.
  3. Arbre = {A, C, B}. Frontière : B-D (5), C-D (6), C-E (7). D est atteignable à 5 ou à 6, sa clé vaut donc 5. La moins chère est B-D (5). Ajoutez D.
  4. Arbre = {A, C, B, D}. Frontière : D-E (2), C-E (7), D-F (8). La clé de E passe de 7 à 2. La moins chère est D-E (2). Ajoutez E.
  5. Arbre = {A, C, B, D, E}. Frontière : E-F (3), D-F (8). La clé de F passe de 8 à 3. La moins chère est E-F (3). Ajoutez F.

Cinq arêtes ajoutées pour six sommets, et l'algorithme s'arrête. L'arbre est A-C (1), B-C (2), B-D (5), D-E (2), E-F (3), pour un poids total de 13. Les arêtes A-B (4), C-D (6), C-E (7) et D-F (8) ne servent jamais.

Deux moments de ce déroulé méritent qu'on s'y arrête. Au tour 3, l'algorithme a accepté une arête de poids 5 alors qu'une arête de poids 2 (D-E) restait intacte ailleurs dans le graphe. Prim ne peut pas encore prendre D-E car aucune de ses extrémités n'est dans l'arbre, et la prendre laisserait deux fragments disjoints au lieu d'un seul arbre en croissance. Au tour 4, la clé de E est tombée de 7 à 2 dès que D a rejoint l'arbre. Les clés ne peuvent que diminuer, et c'est ce qui rend efficace la mise en oeuvre par file de priorité.

L'algorithme de Prim sur le graphe d'exemple. Partant du sommet A, l'arbre grandit vers l'extérieur un sommet à la fois, des pastilles numérotées indiquant l'ordre d'entrée A, C, B, D, E, F, chacun rattaché par l'arête la moins chère atteignant un nouveau sommet.
Un seul arbre connexe, grandi vers l'extérieur depuis A. Les pastilles indiquent l'ordre d'entrée que produit le déroulé ci-dessus.

5. Implémentation : Prim paresseux et Prim actif

Deux implémentations sont courantes, et la différence tient à ce que contient la file de priorité.

Prim paresseux

La version la plus simple pousse toutes les arêtes rencontrées dans un tas-min et écarte à l'extraction les entrées devenues obsolètes.

function LazyPrim(Graph, start):
    inTree = set()
    pq = tas-min vide, ordonné par poids d'arête
    mst = []

    visit(start)                    // marquer et empiler ses arêtes

    while pq non vide and size(mst) < V - 1:
        (w, u, v) = pq.pop()        // arête la moins chère vue
        if v in inTree: continue    // obsolète : deux bouts déjà dans l'arbre
        mst.append((u, v, w))
        visit(v)

    return mst

function visit(x):
    inTree.add(x)
    for chaque arête (x, y) de poids w:
        if y not in inTree: pq.push((w, x, y))

La ligne if v in inTree: continue fait le vrai travail. C'est elle qui empêche un cycle, et c'est pourquoi le tas peut contenir sans danger des entrées périmées : elles sont simplement ignorées lorsqu'elles remontent.

Prim actif

La version active conserve au plus une entrée par sommet, la key[v] courante, et l'abaisse sur place par une opération decrease-key. Elle exige une file de priorité indexée, donc plus de machinerie, mais le tas ne dépasse jamais V entrées au lieu de E.

function EagerPrim(Graph, start):
    for chaque sommet v:
        key[v] = Infinity
        parent[v] = Null
    key[start] = 0
    pq = tas-min indexé de tous les sommets, ordonné par key[]

    while pq non vide:
        u = pq.popMin()
        inTree.add(u)
        for chaque arête (u, v) de poids w:
            if v not in inTree and w < key[v]:
                key[v] = w                  // le POIDS de l'arête, pas une somme
                parent[v] = u
                pq.decreaseKey(v, w)

    return parent            // parent[] est l'arbre

Préférez Prim actif sur les graphes denses, où E dépasse largement V et où garder chaque arête dans le tas devient un gaspillage. Prim paresseux convient parfaitement aux graphes creux et se code juste bien plus facilement.

Comparaison côte à côte des files de priorité paresseuse et active au même instant. La paresseuse contient quatre entrées d'arêtes dont deux atteignent toutes deux le sommet B. L'active contient une clé par sommet : B clé 2 via C, D clé 6, E clé 7 et F infini.
Le même instant, deux files. La paresseuse stocke des arêtes et peut contenir plusieurs entrées pour un même sommet ; l'active stocke une clé par sommet.

6. Complexité en temps et en espace

En résumé pratique : tas binaire sur les graphes creux, et simple balayage O(V2) sur matrice pour les denses. Le tas de Fibonacci relève surtout de l'intérêt théorique.

7. Prim contre Dijkstra : une ligne d'écart

Placez le pseudo-code de Prim actif à côté de l'algorithme de Dijkstra : ce sont presque les mêmes programmes. Tous deux gardent une clé par sommet, tous deux extraient le minimum à répétition, tous deux relâchent les voisins de ce qu'ils viennent d'extraire. Toute la différence tient à ce que l'on met dans la clé :

Prim :      if w < key[v]:              key[v] = w
Dijkstra :  if key[u] + w < key[v]:     key[v] = key[u] + w

La clé de Prim est le poids d'une seule arête. Celle de Dijkstra est la longueur cumulée d'un chemin entier depuis la source. Voilà pourquoi ils répondent à des questions différentes : Prim demande « quelle est la façon la moins chère de rattacher ce sommet à mon arbre », et Dijkstra demande « quelle est la façon la moins chère d'atteindre ce sommet depuis la source ».

Cela explique aussi pourquoi les poids négatifs cassent l'un et pas l'autre. La correction de Dijkstra repose sur le fait que les coûts de chemin ne diminuent jamais lorsque les chemins s'allongent, ce qu'une arête négative détruit. Prim n'additionne jamais de poids, si bien que les poids négatifs lui sont totalement inoffensifs. Un arbre couvrant de poids minimal est bien défini sur un graphe à poids négatifs, et Prim le trouve sans modification.

Comparaison des valeurs de clé produites par Prim et Dijkstra depuis la source A sur le même graphe. Prim donne A 0, B 2, C 1, D 5, E 2, F 3. Dijkstra donne A 0, B 3, C 1, D 7, E 8, F 11.
Une ligne changée, et quatre des six clés diffèrent. Prim stocke un poids d'arête ; Dijkstra stocke un total de chemin.

8. Prim contre Kruskal

Les deux algorithmes sont gloutons, tous deux se justifient par la propriété de coupe, et sur un graphe à poids distincts tous deux renvoient le même arbre. Ils diffèrent par ce qu'ils gardent connexe en chemin.

La règle pratique découle de la densité. Le coût de Kruskal est dominé par le tri, en O(E log E), excellent quand E est petit. Prim avec matrice d'adjacence tourne en O(V2) quel que soit le nombre d'arêtes, et l'emporte sur les graphes denses. Sur une entrée non connexe s'ajoute une différence structurelle : Kruskal produit naturellement une forêt couvrante de poids minimal, tandis que Prim, parti d'un unique sommet, ne couvre que la composante de ce sommet, ce qui oblige à le relancer une fois par composante.

9. Notes pratiques et pièges courants

Quatre situations font trébucher au passage du cas d'école aux données réelles.

10. Applications concrètes

Les arbres couvrants de poids minimal répondent à une question récurrente : quelle est la façon la moins chère de tout relier, sans redondance ? Prim convient aux cas où le réseau croît réellement depuis une source.

Tracé de réseaux et de services

Poser du câble, de la fibre, une canalisation ou une route entre un ensemble fixe de sites, où chaque site doit être atteignable et où l'on minimise la longueur ou le coût total, est la motivation d'origine. L'article de Prim est né exactement de ce problème aux Bell Labs.

Analyse de regroupement

Construire l'arbre couvrant de poids minimal d'un nuage de points puis en supprimer les arêtes les plus lourdes, c'est le regroupement par lien simple : retirer les k - 1 arêtes les plus lourdes laisse exactement k groupes. L'arbre se calcule une fois et toute valeur de k en découle.

Approximation de problèmes plus durs

L'arbre couvrant de poids minimal fournit une borne inférieure pour la tournée du voyageur de commerce, et doubler ses arêtes donne une tournée au plus deux fois l'optimum sur les instances métriques. C'est le point de départ de la construction de Christofides, qui améliore cette garantie à 1,5.

Segmentation d'images et génération de labyrinthes

En traitant les pixels comme des sommets et la dissemblance comme un poids d'arête, la segmentation fondée sur l'arbre couvrant regroupe une image en régions. Lancez plutôt Prim sur une grille à poids aléatoires et vous obtenez un labyrinthe d'aspect uniforme, ce qui en fait un classique de la génération procédurale.

11. Ressources académiques et histoire

Comme plusieurs algorithmes de graphes classiques, celui-ci a été découvert plus d'une fois, et le nom qu'il porte n'est pas celui de la personne qui l'a trouvé en premier.

Pour le récit de référence sur qui a trouvé quoi et quand, voyez l'histoire du problème par Graham et Hell. Pour un traitement rigoureux assorti de preuves complètes de la propriété de coupe et des deux algorithmes, la référence standard est Cormen, Leiserson, Rivest et Stein, Introduction to Algorithms, au chapitre consacré aux arbres couvrants de poids minimal. Ceux que la limite de complexité intéresse devraient regarder le résultat des tas de Fibonacci de Fredman et Tarjan ainsi que l'algorithme quasi linéaire de Chazelle. Les références complètes figurent à la fin de cet article.

Foire aux questions

Pourquoi le choix glouton de Prim produit-il toujours un arbre couvrant de poids minimal ?

Grâce à la propriété de coupe : pour tout partage des sommets en deux groupes, l'arête la moins chère qui traverse ce partage appartient à un arbre couvrant de poids minimal. À chaque étape, Prim prend l'arête la moins chère traversant la coupe entre les sommets déjà dans son arbre et tout le reste, si bien que chaque arête ajoutée est prouvablement sûre. Le choix glouton n'est pas une heuristique chanceuse, c'est ce théorème appliqué V - 1 fois.

Quelle est la différence entre l'algorithme de Prim et celui de Dijkstra ?

Les deux sont presque le même programme, et toute la différence tient à la clé conservée par sommet. Prim utilise le poids d'une seule arête : il demande à quel prix rattacher ce sommet à l'arbre. Dijkstra utilise la longueur cumulée d'un chemin entier depuis la source : il demande à quel prix atteindre ce sommet. C'est aussi pourquoi les poids négatifs cassent Dijkstra mais pas Prim.

L'algorithme de Prim gère-t-il les poids d'arêtes négatifs ?

Oui, sans aucune modification. Prim n'additionne jamais de poids d'arêtes, il ne fait que comparer des arêtes individuelles, si bien que le raisonnement qui fait échouer Dijkstra sur des entrées négatives ne s'applique pas ici. Un arbre couvrant de poids minimal est parfaitement bien défini sur un graphe à poids négatifs, et Prim le trouve. La véritable exigence est que le graphe soit non orienté et connexe.

Regardez l'arbre de Prim grandir

La propriété de coupe devient évidente dès que la frontière choisit son arête la moins chère. Exécutez Prim, pas à pas.

Ouvrir le visualiseur Prim

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