CPGE 2ᵉ année — MP / PSI · Informatique

Les métaheuristiques — le recuit simulé

Pr. EL HADIQ Zouhair

Quand un problème d'optimisation est trop gros pour être résolu exactement, comment trouver rapidement une bonne solution approchée ? Les métaheuristiques explorent intelligemment l'espace des solutions ; le recuit simulé en est l'exemple au programme.

🎯 Objectifs du chapitre
Sommaire
  1. Les problèmes d'optimisation
  2. Pourquoi les métaheuristiques ?
  3. Qu'est-ce qu'une métaheuristique ?
  4. Recherche locale et voisinage
  5. La descente et le piège des optima locaux
  6. Le principe du recuit simulé
  7. Le critère d'acceptation de Metropolis
  8. L'algorithme du recuit simulé
  9. Le schéma de refroidissement
  10. Exemple complet : le voyageur de commerce
  11. Atouts, limites et réglages
  12. Récapitulatif

1. Les problèmes d'optimisation

Un problème d'optimisation consiste à chercher, parmi un très grand nombre de solutions possibles, celle qui minimise (ou maximise) une certaine quantité. On le décrit par deux ingrédients :

Résoudre le problème, c'est trouver une solution s* ∈ S telle que f(s*) soit minimale : ∀ s ∈ S, f(s*) ≤ f(s). Une telle solution est dite optimale (globale).

Problème Une solution s Coût f(s) à minimiser
Voyageur de commerce Un ordre de visite des villes (un circuit) Longueur totale du circuit
Sac à dos Un sous-ensemble d'objets choisis L'opposé de la valeur emportée (sous contrainte de poids)
Emploi du temps Une affectation cours → créneaux Nombre de conflits
💡 Maximiser, c'est minimiser. Chercher à maximiser g revient à minimiser f = −g. On présente donc tout en minimisation sans perte de généralité.

2. Pourquoi les métaheuristiques ?

Pour beaucoup de problèmes intéressants, l'espace des solutions est gigantesque. Pour le voyageur de commerce à n villes, le nombre de circuits est de l'ordre de (n−1)! / 2 : déjà plus de 60 000 milliards pour seulement 20 villes. Parcourir toutes les solutions (recherche exhaustive) est donc impossible en pratique.

Les méthodes vues précédemment ont leurs limites :

⚠️ Le compromis fondamental. On échange la garantie d'optimalité contre la rapidité. Une métaheuristique ne promet pas la meilleure solution, mais une solution de bonne qualité en un temps raisonnable.

3. Qu'est-ce qu'une métaheuristique ?

Une heuristique est une méthode de résolution approchée, souvent conçue sur mesure pour un problème particulier (par exemple : « toujours aller vers la ville la plus proche »).

Une métaheuristique (le préfixe méta = « au-delà ») est une stratégie générale de haut niveau, indépendante du problème, qui pilote et coordonne des heuristiques pour explorer efficacement l'espace des solutions. On l'applique au voyageur de commerce, au sac à dos, à l'emploi du temps… avec la même mécanique, en changeant seulement la fonction de coût et la notion de voisinage.

Famille Exemples
Basées sur une solution unique qu'on améliore Recuit simulé, recherche tabou
Basées sur une population de solutions Algorithmes génétiques, colonies de fourmis
💡 Au programme MP/PSI. Le représentant étudié et à savoir implémenter est le recuit simulé (simulated annealing). Il ne garantit pas l'optimum, mais trouve une solution approchée de bonne qualité.

4. Recherche locale et voisinage

L'idée d'une recherche locale est de partir d'une solution puis de l'améliorer par petites modifications, sans jamais reconstruire une solution entière. Pour cela on définit le voisinage V(s) d'une solution s : l'ensemble des solutions obtenues par une petite transformation élémentaire.

Un voisin s' ∈ V(s) est dit améliorant si f(s') < f(s). La variation de coût se note :

ΔE = f(s') − f(s)

Si ΔE < 0, le voisin est meilleur (le coût baisse) ; si ΔE > 0, il est moins bon.

5. La descente et le piège des optima locaux

La recherche locale la plus simple est la descente (ou hill climbing) : à chaque étape, on remplace la solution courante par un voisin strictement meilleur ; on s'arrête quand aucun voisin n'améliore plus.

def descente(s):
    while True:
        voisins = generer_voisins(s)
        meilleur = min(voisins, key=cout)
        if cout(meilleur) < cout(s):
            s = meilleur          # on descend
        else:
            return s             # aucun voisin meilleur : blocage

Le problème : la descente n'accepte jamais de remonter. Elle se fige donc dans le premier optimum local rencontré — une solution meilleure que tous ses voisins, mais pas forcément la meilleure globalement.

🏔️ L'image de la montagne. Imaginez un paysage de coûts avec des vallées. Une bille qui ne fait que descendre se bloque au fond de la première vallée (optimum local), même s'il existe une vallée plus profonde (optimum global) plus loin. Pour en sortir, il faut parfois accepter de remonter temporairement.

6. Le principe du recuit simulé

Le recuit simulé s'inspire d'un procédé de métallurgie, le recuit : pour obtenir un métal sans défaut, on le chauffe fortement (les atomes s'agitent librement) puis on le refroidit très lentement. Les atomes se réorganisent alors progressivement dans une structure d'énergie minimale (cristal régulier). Un refroidissement trop rapide (trempe) fige au contraire les défauts.

On transpose cette idée à l'optimisation :

Physique (recuit) Optimisation
État du matériauSolution s
Énergie de l'étatCoût f(s)
Température TParamètre de contrôle T
Refroidissement lentDécroissance progressive de T
État d'énergie minimaleSolution (quasi) optimale

L'idée maîtresse : pour échapper aux optima locaux, on autorise parfois des mouvements qui dégradent le coût. Cette tolérance est forte quand T est élevée (début : on explore largement) et devient faible quand T baisse (fin : on affine et on se stabilise).

7. Le critère d'acceptation de Metropolis

À chaque étape, on tire un voisin s' au hasard et on calcule ΔE = f(s') − f(s). La décision d'accepter s' suit la règle de Metropolis :

Règle d'acceptation.

Analysons la probabilité P = e−ΔE/T d'accepter une dégradation :

ΔE T élevée (T = 100) T basse (T = 1)
ΔE = 1P ≈ 0,99P ≈ 0,37
ΔE = 10P ≈ 0,90P ≈ 0,000045

Concrètement, accepter « avec la probabilité P » se programme en tirant un nombre aléatoire u dans [0, 1[ : on accepte si u < P.

8. L'algorithme du recuit simulé

On part d'une solution initiale et d'une température élevée. On répète : tirer un voisin, calculer ΔE, décider selon Metropolis, puis refroidir. On garde en mémoire la meilleure solution rencontrée.

import math, random

def recuit_simule(s0, cout, voisin, T0, alpha, n_iter):
    s = s0                       # solution courante
    c = cout(s)
    meilleur, c_meilleur = s, c  # meilleure solution vue
    T = T0
    for _ in range(n_iter):
        s2 = voisin(s)           # un voisin au hasard
        c2 = cout(s2)
        dE = c2 - c              # variation de coût
        if dE <= 0 or random.random() < math.exp(-dE / T):
            s, c = s2, c2        # on accepte le voisin
            if c < c_meilleur:
                meilleur, c_meilleur = s, c
        T = alpha * T            # refroidissement (0 < alpha < 1)
    return meilleur, c_meilleur
💡 Pourquoi garder « meilleur » séparément ? La solution courante s peut se dégrader (on accepte des remontées). On conserve donc à part la meilleure solution jamais rencontrée, qui sera le résultat final.

9. Le schéma de refroidissement

Le schéma de refroidissement (cooling schedule) décrit comment T diminue. C'est le réglage le plus important. Le plus courant est la décroissance géométrique :

Tk+1 = α · Tk,   avec 0 < α < 1 (souvent α entre 0,90 et 0,99)
⚠️ Exploration puis exploitation. Début (T grande) : on parcourt largement l'espace et on franchit des « collines ». Fin (T petite) : on affine localement. Un bon réglage équilibre ces deux phases.

10. Exemple complet : le voyageur de commerce

On dispose de n villes de coordonnées connues. Une solution est un ordre de visite (une permutation des villes), le coût est la longueur totale du circuit fermé, et un voisin s'obtient en échangeant deux villes de la tournée.

import math, random

def distance(a, b):
    return math.hypot(a[0]-b[0], a[1]-b[1])

def longueur(tour, villes):           # coût = circuit fermé
    n = len(tour)
    return sum(distance(villes[tour[i]], villes[tour[(i+1)%n]])
               for i in range(n))

def voisin(tour):                       # échange de deux villes
    t = tour[:]
    i, j = random.sample(range(len(t)), 2)
    t[i], t[j] = t[j], t[i]
    return t

def tsp_recuit(villes, T0=100, alpha=0.995, n_iter=20000):
    s = list(range(len(villes)))
    random.shuffle(s)
    c = longueur(s, villes)
    best, cbest = s[:], c
    T = T0
    for _ in range(n_iter):
        s2 = voisin(s)
        c2 = longueur(s2, villes)
        dE = c2 - c
        if dE <= 0 or random.random() < math.exp(-dE / T):
            s, c = s2, c2
            if c < cbest:
                best, cbest = s[:], c
        T = alpha * T
    return best, cbest

En quelques dizaines de milliers d'itérations, cet algorithme produit un circuit court — généralement très proche de l'optimal — là où l'énumération exhaustive serait totalement hors de portée.

11. Atouts, limites et réglages

Atouts ✅ Limites ⚠️
Simple à implémenter, très général (change juste coût + voisinage). Aucune garantie d'atteindre l'optimum global.
Capable de sortir des optima locaux. Résultat aléatoire : deux exécutions diffèrent.
Bon rapport qualité / temps sur de gros problèmes. Sensible au réglage de T0, α et du nombre d'itérations.
💡 Bonnes pratiques. Choisir T0 pour qu'au départ ~80 % des mouvements soient acceptés ; prendre α proche de 1 (0,99) ; lancer plusieurs exécutions et garder la meilleure.

Récapitulatif

CPGE MP / PSI — Informatique · Chapitre 1‑6 : Métaheuristiques (recuit simulé)