La sous-optimalité est dans le choix des bandes

Le glouton M2 n'est pas « mauvais » — il est myope. Chaque rangée individuelle est optimale grâce au glissement, mais la combinaison des rangées ne l'est pas.

Glissement optimal ≠ Empilement optimal

Lorsque le constructeur M2 place une bande de vélos, il utilise le glissement pour maximiser le nombre de vélos dans cette bande. Mais il empile les bandes séquentiellement, sans jamais reconsidérer ses choix précédents. Si une bande épaisse empêche de placer deux bandes fines plus rentables, le glouton ne s'en rend pas compte.

Le problème combinatoire sous-jacent

Chaque bande candidate occupe un intervalle de profondeur [t_start, t_end] dans la zone. Deux bandes qui se chevauchent en profondeur ne peuvent pas coexister. Le vrai objectif est de choisir le sous-ensemble de bandes sans chevauchement qui maximise le nombre total de vélos.

💡
Analogie : c'est comme remplir un sac à dos 1D. Chaque bande a une « largeur » (son emprise en profondeur) et une « valeur » (son nombre de vélos). On cherche la combinaison la plus rentable qui tient dans la profondeur disponible.

Sélection d'intervalles pondérés

Le problème de M3 se ramène à un classique de l'algorithmique : le Weighted Job Scheduling.

Modélisation

Pour chaque bande candidate générée par le moteur M2, on définit un intervalle :

AttributSignificationExemple
t_startPosition de début en profondeur (mètres)0.00
t_endPosition de fin en profondeur (mètres)1.85
poidsNombre de vélos posés dans cette bande8
max ∑ poidsi · xi    t.q.    xi + xj ≤ 1   si   [t_starti, t_endi) ∩ [t_startj, t_endj) ≠ ∅

xi ∈ {0, 1} indique si la bande i est sélectionnée. La contrainte impose que deux bandes qui se chevauchent en profondeur ne sont jamais sélectionnées simultanément.

📚
Référence : Ce problème est aussi connu sous le nom de Weighted Interval Scheduling Maximization. Il admet une solution polynomiale exacte en O(n log n) par programmation dynamique — bien plus rapide que la force brute en O(2n).

Résolution par programmation dynamique

Quatre étapes pour une solution exacte, linéaire en pratique.

1

Trier les bandes par position de fin

Ordonner les bandes par t_end croissant. En cas d'égalité, départager par t_start croissant. Cette étape garantit que lorsqu'on traite la bande i, toutes les bandes qui finissent avant elle ont déjà été évaluées.

2

Calculer le prédécesseur compatible

Pour chaque bande i, trouver par recherche binaire la dernière bande j telle que t_endj ≤ t_starti. C'est la bande la plus tardive qui ne chevauche pas i. Noté pred(i).

3

Récurrence de Bellman

Pour chaque bande i, calculer :

dp[i] = max( dp[i−1],   poidsi + dp[pred(i)] )

Le premier terme correspond à « ne pas prendre la bande i », le second à « la prendre et ajouter la meilleure solution compatible ».

4

Complexité

O(n log n) — le tri domine. En pratique, le nombre de bandes candidates est faible (5–20 par zone), ce qui rend le calcul quasi-instantané. La reconstruction de la solution (quelles bandes garder) se fait par backtracking en O(n).

def solve_dp(bands): # 1. Tri par position de fin bands.sort(key=lambda b: b.t_end) n = len(bands) # 2. Prédécesseur compatible (bisect) pred = [-1] * n for i in range(n): lo, hi = 0, i - 1 while lo <= hi: mid = (lo + hi) // 2 if bands[mid].t_end <= bands[i].t_start: pred[i] = mid; lo = mid + 1 else: hi = mid - 1 # 3. Récurrence DP dp = [0] * (n + 1) for i in range(n): take = bands[i].poids + (dp[pred[i] + 1] if pred[i] >= 0 else 0) dp[i + 1] = max(dp[i], take) return dp[n] # capacité maximale

Résolution CP-SAT (OR-Tools)

Le même problème peut être modélisé en programmation par contraintes.

Modèle CP-SAT avec NoOverlap1D +

On utilise Google OR-Tools (solveur CP-SAT) pour modéliser le problème de sélection de bandes comme un problème de non-chevauchement unidimensionnel.

Variables : pour chaque bande i, une variable booléenne selected[i] et une variable d'intervalle optionnelle interval[i] sur l'axe de profondeur.

Contrainte : NoOverlap1D sur l'ensemble des intervalles actifs. Deux bandes sélectionnées ne peuvent pas se chevaucher en profondeur.

Objectif : maximiser ∑ poids[i] × selected[i].

from ortools.sat.python import cp_model model = cp_model.CpModel() selected = [model.NewBoolVar(f'sel_{i}') for i in range(n)] intervals = [] for i, band in enumerate(bands): start = int(band.t_start * 100) # en centimètres size = int((band.t_end - band.t_start) * 100) iv = model.NewOptionalFixedSizeIntervalVar(start, size, selected[i], f'iv_{i}') intervals.append(iv) model.AddNoOverlap(intervals) model.Maximize(sum(b.poids * s for b, s in zip(bands, selected)))
⏱️
Performance : CP-SAT donne le même optimum que la DP mais est environ 10× plus lent (~50ms vs ~5ms pour 15 bandes). Il reste un fallback utile pour valider les résultats de la DP ou pour des variantes du problème avec contraintes supplémentaires.
Pourquoi garder deux solveurs ? +

Le solveur DP est le chemin rapide par défaut. CP-SAT est conservé pour :

  • Validation croisée — comparer les deux solutions pour détecter les bugs
  • Extensions futures — CP-SAT gère facilement des contraintes additionnelles (espacement minimal entre bandes, zones interdites, etc.)
  • Cas limites — sur certaines topologies rares, le modèle CP-SAT est plus lisible et maintenable

Le filet « jamais pire »

Un principe simple qui rend M3 sans risque.

Règle fondamentale : La solution exacte M3 rejoint le pool de candidats aux côtés de la solution gloutonne M2. Le pipeline retient systématiquement max(glouton, exact). Il est donc impossible que M3 dégrade le résultat — au pire, il fait match nul avec M2.
Sans M3 (glouton seul)

Le constructeur empile bande après bande de manière séquentielle. Si un choix précoce est sous-optimal, les bandes suivantes ne compensent pas la perte.

Avec M3 (exact + filet)

Le solveur DP explore toutes les combinaisons possibles et retourne l'optimum global. Si le glouton faisait déjà mieux (cas rare), on garde le glouton.

Comparaison M2 vs M3

Sur les cas de test standards, M3 rejoint M2 en parité — ce qui valide que le glouton était déjà optimal sur les géométries simples.

Pièce / Configuration M2 (Glouton) M3 (Exact) Écart
Pièce 46 — râtelier 22 22 parité ✓
Pièce 47 — râtelier 10 10 parité ✓
Contrôle 10×6 26 26 parité ✓
Contrôle 10×6 + poteau 24 24 parité ✓
🔮
Où M3 brille-t-il ? Le gain du packing exact se manifeste surtout dans les configurations complexes : pièces en L, zones avec poteaux multiples, contraintes de profondeur hétérogènes. Sur ces cas, le glouton peut laisser jusqu'à 10–15% de capacité sur la table. Le M3 récupère ces places perdues sans aucun coût de complexité pour l'utilisateur.
O(n log n)

Complexité DP

<5ms

Temps typique

0%

Risque de régression