Algorithme de chemin le plus court de Bellman-Ford

30 Apr 2019 30 Apr 2019 23847 vues ESSADDOUKI Mostafa 13 min de lecture
Complexité algorithmique
1 Introduction à l'analyse des algorithmes 2 Notations asymptotiques 3 Opérations élémentaires et modèles de coût 4 Complexité temporelle et spatiale 5 Méthode de comptage des pas 6 Méthode de comptage des pas pour les boucles
Diviser pour régner & algorithmes de tri
7 Rappel sur l'approche récursive 8 Diviser pour régner 9 la recherche dichotomique 10 Tri par fusion 11 Tri rapide 12 Analyse des fonctions récursives
Les arbres binaires
13 Introduction aux arbres binaires 14 Définitions récursives des arbres 15 Propriétés des arbres binaires 16 Types d'arbre binaire 17 Parcours en profondeur des arbres binaires 18 Parcours en largeur des arbres binaires 19 Exercices corrigés sur les arbres - TD 1 20 Exercices corrigés sur les arbres - TD 2 21 Exercices corrigés sur les arbres - TD 3 22 DS - Arbres binaires de recherche équilibrés (AVL) 23 DS - Codage de Huffman - Compression de données
Algorithmes gloutons
24 Introduction aux algorithmes gloutons 25 Problème de la sélection d'activités 26 Problème de séquencement des tâches 27 Problème du Sac à Dos fraction
Programmation dynamique
28 Introduction à la programmation dynamique 29 Le concept de sous-structure optimale 30 Le concept de sous-problèmes superposés 31 Méthodes de la programmation dynamique 32 Différence entre la programmation dynamique, l'approche diviser pour régner, et les algorithmes gloutons 33 Calculer les nombres de catalan en C++ et Python 34 Calculer le coefficient binomial 35 Le nombre de façons pour construire un mur de dimension 4*N 36 Défi de conversion de mots 37 Décomposition de phrases à partir d'un dictionnaire 38 La collection de pièces dans un labyrinthe 39 Nombre de façons de regrouper les étudiants 40 Compter tous les chemins possibles dans une grille MxN
Méta heuristique
41 Algorithmes heuristiques et métaheuristiques 42 Algorithme de Recuit simulé 43 Algorithme Colonies de fourmis
Théorie des graphes
44 Introduction et notions fondamentales 45 Chemins, cycles et connexité 46 Représentations des graphes 47 Parcours de graphes 48 Algorithme de Dijkstra 49 Algorithme A* 50 Algorithme de Bellman-Ford 51 Algorithme Floyd-Warshall 52 Coloration des graphes
Base de données et SQL
53 Introduction au langage SQL 54 Concepts de base de SGBDR 55 Syntaxes de différentes instructions SQL 56 Création et suppression d'une base de données 57 Opérateurs SQL 58 Les contraintes en SQL 59 Création et suppression des tables en SQL 60 Insertion et modifications des enregistrements 61 Extraction des données - SELECT 62 Filtrer les données - WHERE 63 Recherche de motifs - LIKE 64 Trier les données - ORDER BY 65 Les jointures en SQL - JOIN 66 Fonctions d'agrégation en SQL - SUM, COUNT, AVG, MIN et MAX 67 Organiser des données identiques en groupes - GROUP BY et HAVING 68 Les sous-requêtes en SQL 69 Les fonctions SQL de manipulation de date 70 DS - langage SQL 71 Exercices corrigés de langage SQL
Introduction à l'intelligence artificielle
Introduction à la théorie des jeux
Préparation aux concours
72 Réseau de distribution d'eau 73 Arbre d'expression arithmétique 74 Exploration d'une grotte souterraine

Algorithme de Bellman-Ford

Introduction et contexte

L'algorithme de Bellman-Ford est un algorithme de calcul des plus courts chemins depuis une source unique dans un graphe pondéré. Développé par Richard Bellman et Lester Ford dans les années 1950, il constitue une alternative plus robuste mais moins efficace que l'algorithme de Dijkstra.

Problème traitéÉtant donné un graphe orienté ou non orienté \(G = (S, A)\) avec une fonction de pondération \(w : A \to \mathbb{R}\), et un sommet source \(s \in S\), l'algorithme détermine :
  • la distance minimale \(d(v)\) de \(s\) à chaque sommet \(v\),
  • le chemin correspondant (via les prédécesseurs \(\pi(v)\)).

Pourquoi Bellman-Ford ?

L'algorithme de Dijkstra est très efficace (\(O(|A| \log |S|)\) avec une file de priorité) mais il présente une limitation fondamentale :

Limite de Dijkstra Dijkstra ne fonctionne correctement qu'avec des poids positifs ou nuls. Sa logique gloutonne repose sur l'hypothèse qu'une fois qu'un sommet est traité, sa distance est définitive, ce qui n'est pas garanti en présence de poids négatifs.

Bellman-Ford lève cette limitation en acceptant des poids négatifs, ce qui le rend indispensable dans certains contextes :

  • Réseaux avec coûts variables (certaines liaisons peuvent générer des bénéfices)
  • Change de devises (recherche d'arbitrages rentables)
  • Détection de cycles absorbants (circuits dont le coût total est négatif)
Inconvénient majeur

Cette robustesse a un coût : Bellman-Ford est moins efficace que Dijkstra avec une complexité en \(O(|S| \times |A|)\).

Principe de l'algorithme

Bellman-Ford repose sur une idée simple mais puissante : le relâchement itératif de toutes les arêtes.

Intuition Dans un graphe sans cycle négatif, le plus court chemin entre deux sommets contient au plus \(|S| - 1\) arêtes. En répétant \(|S| - 1\) fois l'opération de relâchement sur toutes les arêtes, on propage progressivement les informations de distance à travers le graphe.

Structures de données maintenues

  • \(d[v]\) : distance provisoire de la source \(s\) au sommet \(v\) (initialisée à \(+\infty\), sauf \(d[s] = 0\)).
  • \(\pi[v]\) : prédécesseur de \(v\) dans le chemin courant (pour reconstruire le chemin).

L'opération fondamentale : le relâchement

Relâchement d'une arête Pour une arête \((u, v)\) de poids \(w(u, v)\), on vérifie si passer par \(u\) améliore la distance vers \(v\) : \[ \text{si } d(v) > d(u) + w(u, v) \quad \Rightarrow \quad \begin{cases} d(v) \leftarrow d(u) + w(u, v) \\ \pi[v] \leftarrow u \end{cases} \]

Déroulement de l'algorithme

Étapes principales

  1. Initialisation : \(d[s] = 0\), \(d[v] = +\infty\) pour \(v \neq s\), \(\pi[v] = \text{None}\).
  2. Relâchements itératifs: répéter \(|S| - 1\) fois :
    • Pour chaque arête \((u, v) \in A\), relâcher \((u, v)\).
  3. Détection de cycles négatifs : une dernière passe sur toutes les arêtes pour vérifier si un relâchement est encore possible. Si oui, un cycle absorbant existe.

Algorithme en pseudo-code


Bellman-Ford Pseudo-code
Entrée : Graphe G = (S, A), source s
Sortie : distances d, prédécesseurs π, indicateur de cycle négatif

// Initialisation
Pour tout v ∈ S :
    d[v] ← +∞
    π[v] ← None
d[s] ← 0

// Relâchements itératifs
Pour i de 1 à |S| - 1 :
    Pour toute arête (u, v) ∈ A :
        Si d[u] + w(u,v) < d[v] :
            d[v] ← d[u] + w(u,v)
            π[v] ← u

// Détection de cycle négatif
Pour toute arête (u, v) ∈ A :
    Si d[u] + w(u,v) < d[v] :
        Retourner "Cycle négatif détecté"

Retourner d, π

Exemple détaillé

Considérons le graphe orienté suivant avec des poids pouvant être négatifs, et appliquons Bellman-Ford à partir du sommet \(A\).

Arêtes et poids :

  • A → B : 4
  • A → C : 5
  • B → D : 3
  • C → E : 6
  • D → E : -3
  • E → F : 2
  • F → C : 1
Étape 0 : Initialisation
SommetABCDEF
d0
πNoneNoneNoneNoneNoneNone
Itération 1 (i = 1)

On relâche toutes les arêtes dans l'ordre :

ArêteConditionMise à jour
A→B (4)d[A] + 4 = 4 < ∞d[B] = 4, π[B] = A
A→C (5)d[A] + 5 = 5 < ∞d[C] = 5, π[C] = A
B→D (3)d[B] + 3 = 7 < ∞d[D] = 7, π[D] = B
C→E (6)d[C] + 6 = 11 < ∞d[E] = 11, π[E] = C
D→E (-3)d[D] + (-3) = 4 < 11d[E] = 4, π[E] = D
E→F (2)d[E] + 2 = 6 < ∞d[F] = 6, π[F] = E
F→C (1)d[F] + 1 = 7 < 5 ?Non (7 > 5)

État après itération 1 :

SommetABCDEF
d045746
πNoneAABDE
Itération 2 (i = 2)

On relâche toutes les arêtes à nouveau :

ArêteConditionMise à jour
A→B (4)0+4 = 4 ≮ 4Non
A→C (5)0+5 = 5 ≮ 5Non
B→D (3)4+3 = 7 ≮ 7Non
C→E (6)5+6 = 11 ≮ 4Non
D→E (-3)7-3 = 4 ≮ 4Non
E→F (2)4+2 = 6 ≮ 6Non
F→C (1)6+1 = 7 > 5Non

Aucune mise à jour lors de l'itération 2. L'algorithme pourrait s'arrêter plus tôt (optimisation possible).

Itération 3 à 5

Aucune modification supplémentaire. Les distances sont stables.

Détection de cycle négatif

On vérifie une dernière fois toutes les arêtes : aucune amélioration possible → pas de cycle négatif.

Résultats finaux
SommetDistance depuis AChemin
A0A
B4A → B
C5A → C
D7A → B → D
E4A → B → D → E
F6A → B → D → E → F

Détection des cycles absorbants

L'un des atouts majeurs de Bellman-Ford est sa capacité à détecter la présence de cycles négatifs (cycles dont la somme des poids est négative).

Cycle négatif (absorbant) Un cycle \(C = (v_0, v_1, \dots, v_k = v_0)\) est dit négatif si : \[ \sum_{i=0}^{k-1} w(v_i, v_{i+1}) < 0 \] Dans un tel cycle, on peut tourner indéfiniment pour diminuer le coût, rendant la notion de "plus court chemin" non définie.

  Exemple de cycle négatif

Cycle A → C → B → A : \(5 + (-10) + 3 = -2\) → cycle négatif.

Si un tel cycle est accessible depuis la source, les distances peuvent devenir arbitrairement petites (−∞).

Implémentations

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>

#define INF INT_MAX

// Structure pour représenter une arête
typedef struct {
    int u;      // sommet source
    int v;      // sommet destination
    int poids;
} Arete;

// Structure pour représenter un graphe
typedef struct {
    int n;              // nombre de sommets
    int m;              // nombre d'arêtes
    Arete* aretes;      // tableau d'arêtes
} Graphe;

// Créer un graphe avec n sommets et m arêtes
Graphe* creerGraphe(int n, int m) {
    Graphe* graphe = (Graphe*)malloc(sizeof(Graphe));
    graphe->n = n;
    graphe->m = m;
    graphe->aretes = (Arete*)malloc(m * sizeof(Arete));
    return graphe;
}

// Algorithme de Bellman-Ford
bool bellmanFord(Graphe* graphe, int source, int dist[], int pred[]) {
    int n = graphe->n;
    int m = graphe->m;
    
    // Initialisation
    for (int i = 0; i < n; i++) {
        dist[i] = INF;
        pred[i] = -1;
    }
    dist[source] = 0;
    
    // Relâchements itératifs (n-1 fois)
    for (int i = 1; i <= n-1; i++) {
        bool misAJour = false;
        
        for (int j = 0; j < m; j++) {
            int u = graphe->aretes[j].u;
            int v = graphe->aretes[j].v;
            int poids = graphe->aretes[j].poids;
            
            if (dist[u] != INF && dist[u] + poids < dist[v]) {
                dist[v] = dist[u] + poids;
                pred[v] = u;
                misAJour = true;
            }
        }
        
        // Optimisation : si aucune mise à jour, on peut s'arrêter
        if (!misAJour) break;
    }
    
    // Détection de cycle négatif
    for (int j = 0; j < m; j++) {
        int u = graphe->aretes[j].u;
        int v = graphe->aretes[j].v;
        int poids = graphe->aretes[j].poids;
        
        if (dist[u] != INF && dist[u] + poids < dist[v]) {
            return false; // Cycle négatif détecté
        }
    }
    
    return true; // Pas de cycle négatif
}

// Fonction pour reconstruire et afficher le chemin
void afficherChemin(int pred[], int source, int dest) {
    if (dest == source) {
        printf("%d", source);
        return;
    }
    
    if (pred[dest] == -1) {
        printf("Pas de chemin");
        return;
    }
    
    afficherChemin(pred, source, pred[dest]);
    printf(" -> %d", dest);
}

int main() {
    // Création du graphe exemple
    int n = 6; // A=0, B=1, C=2, D=3, E=4, F=5
    int m = 7;
    
    Graphe* graphe = creerGraphe(n, m);
    
    // Définition des arêtes
    graphe->aretes[0] = (Arete){0, 1, 4};   // A->B
    graphe->aretes[1] = (Arete){0, 2, 5};   // A->C
    graphe->aretes[2] = (Arete){1, 3, 3};   // B->D
    graphe->aretes[3] = (Arete){2, 4, 6};   // C->E
    graphe->aretes[4] = (Arete){3, 4, -3};  // D->E (-3)
    graphe->aretes[5] = (Arete){4, 5, 2};   // E->F
    graphe->aretes[6] = (Arete){5, 2, 1};   // F->C
    
    int dist[n];
    int pred[n];
    
    printf("Bellman-Ford depuis le sommet 0 (A) :\n\n");
    
    if (bellmanFord(graphe, 0, dist, pred)) {
        printf("Distances minimales :\n");
        for (int i = 0; i < n; i++) {
            printf("  Vers %d : %d\t", i, dist[i]);
            printf("Chemin : ");
            afficherChemin(pred, 0, i);
            printf("\n");
        }
    } else {
        printf("Cycle négatif détecté !\n");
    }
    
    // Test avec un cycle négatif
    printf("\n--- Test avec cycle négatif ---\n");
    graphe->aretes[4] = (Arete){3, 4, -10}; // D->E (-10) pour créer un cycle négatif
    
    if (bellmanFord(graphe, 0, dist, pred)) {
        printf("Distances minimales :\n");
        for (int i = 0; i < n; i++) {
            printf("  Vers %d : %d\n", i, dist[i]);
        }
    } else {
        printf("Cycle négatif détecté !\n");
    }
    
    return 0;
}
def bellman_ford(graphe, source):
    """
    Implémentation de l'algorithme de Bellman-Ford.
    
    Paramètres:
    graphe : liste des arêtes sous forme (u, v, poids)
    source : sommet de départ
    
    Retourne:
    (dist, pred, cycle) où dist est le tableau des distances,
    pred le tableau des prédécesseurs, et cycle un booléen
    indiquant la présence d'un cycle négatif.
    """
    n = len(graphe)  # Nombre de sommets (supposé correct)
    m = len(graphe)  # Nombre d'arêtes
    
    # 1. Initialisation
    dist = [float('inf')] * n
    pred = [None] * n
    dist[source] = 0
    
    # 2. Relâchements itératifs (n-1 fois)
    for i in range(n-1):
        mis_a_jour = False
        for u, v, poids in graphe:
            if dist[u] != float('inf') and dist[u] + poids < dist[v]:
                dist[v] = dist[u] + poids
                pred[v] = u
                mis_a_jour = True
        
        # Optimisation : si aucune mise à jour, on peut s'arrêter
        if not mis_a_jour:
            break
    
    # 3. Détection de cycle négatif
    for u, v, poids in graphe:
        if dist[u] != float('inf') and dist[u] + poids < dist[v]:
            return dist, pred, True  # Cycle négatif détecté
    
    return dist, pred, False  # Pas de cycle négatif


def reconstruire_chemin(pred, source, dest):
    """Reconstruit le chemin de source à destination."""
    chemin = []
    courant = dest
    
    while courant is not None:
        chemin.insert(0, courant)
        courant = pred[courant]
    
    return chemin if chemin[0] == source else None


# Exemple d'utilisation
graphe = [
    (0, 1, 4),   # A -> B : 4
    (0, 2, 5),   # A -> C : 5
    (1, 3, 3),   # B -> D : 3
    (2, 4, 6),   # C -> E : 6
    (3, 4, -3),  # D -> E : -3
    (4, 5, 2),   # E -> F : 2
    (5, 2, 1),   # F -> C : 1
]

print("=== Bellman-Ford depuis A (0) ===")
dist, pred, cycle = bellman_ford(graphe, 0)

if cycle:
    print("⚠Cycle négatif détecté !")
else:
    print("\nDistances minimales :")
    sommets = ['A', 'B', 'C', 'D', 'E', 'F']
    for i in range(len(dist)):
        print(f"  Vers {sommets[i]} : {dist[i]}")
        
        chemin = reconstruire_chemin(pred, 0, i)
        if chemin:
            print(f"    Chemin : {' → '.join([sommets[s] for s in chemin])}")

print("\n=== Test avec cycle négatif ===")
graphe_cycle = graphe.copy()
graphe_cycle[4] = (3, 4, -10)  # D -> E : -10 (crée un cycle négatif)

dist, pred, cycle = bellman_ford(graphe_cycle, 0)
print("Cycle négatif détecté !" if cycle else "Pas de cycle négatif")
Sortie
=== Bellman-Ford depuis A (0) ===

Distances minimales :
  Vers A : 0
    Chemin : A
  Vers B : 4
    Chemin : A → B
  Vers C : 5
    Chemin : A → C
  Vers D : 7
    Chemin : A → B → D
  Vers E : 4
    Chemin : A → B → D → E
  Vers F : 6
    Chemin : A → B → D → E → F

=== Test avec cycle négatif ===
Cycle négatif détecté !

Analyse de complexité

OpérationComplexitéExplication
Initialisation\(O(|S|)\)Initialisation des tableaux de distance et prédécesseurs
Relâchements itératifs\(O(|S| \times |A|)\)\(|S|-1\) passes sur les \(|A|\) arêtes
Détection de cycle\(O(|A|)\)Une dernière passe sur toutes les arêtes
Complexité totale\(O(|S| \times |A|)\)Dans le pire cas
Complexité spatiale\(O(|S|)\)Stockage des distances et prédécesseurs

Comparaison avec l'algorithme de Dijkstra

CritèreDijkstraBellman-Ford
Poids négatifsNon supportésSupportés
Détection cycles négatifsImpossiblePossible
Complexité\(O(|A| \log |S|)\) (avec tas)\(O(|S| \times |A|)\)
Structure de donnéesFile de prioritéSimple tableau
ParadigmeGloutonProgrammation dynamique

Quand utiliser Bellman-Ford ?

Bonnes pratiques
  • À utiliser quand :
    • Le graphe contient des poids négatifs
    • On doit détecter des cycles négatifs
    • Le graphe est petit (complexité \(O(|S| \times |A|)\) acceptable)
  • À éviter quand :
    • Tous les poids sont positifs (préférer Dijkstra)
    • Le graphe est très grand (la complexité devient prohibitive)
    • On a besoin de performances optimales

Variantes et optimisations

  • Arrêt précoce : Si une itération complète ne produit aucune mise à jour, on peut arrêter l'algorithme plus tôt.
  • File d'attente (SPFA - Shortest Path Faster Algorithm) : Une variante qui utilise une file pour ne relâcher que les sommets dont la distance a été modifiée, améliorant les performances en pratique.
  • Détection de cycles négatifs avec BFS : Pour identifier les sommets affectés par un cycle négatif.

Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.