Aller au contenu principal

Bornes et méthodes exactes

Ce que ce chapitre apporte 6 points
  • Poser une borne supérieure pour une marge à maximiser, une borne inférieure pour un coût à minimiser, et en déduire un écart garanti à l'optimum sans connaître celui-ci.
  • Calculer la borne de la relaxation continue du sac à dos, et dire pourquoi c'en est une.
  • Poser les bornes simples de la découpe de barres et de l'affectation, et reconnaître quand elles ne sont pas atteintes.
  • Écrire un petit problème en nombres entiers avec PuLP, résoudre aussi sa relaxation continue, et lire le statut.
  • Suivre un arbre de séparation et d'évaluation, et dire pourquoi un nœud est élagué.
  • Formuler la tournée en nombres entiers avec des coupes de sous-tour, et reconnaître ce qui fait qu'une méthode exacte ne suit plus.
Chez Valdrome Mécanique, une règle de bon sens lance C4, C1 et C9 : 97 heures de machine, 13 200 € de marge. Le directeur de production pose alors la seule question qui compte : « est-ce que c'est bien ? ». Répondre « c'est la meilleure » serait faux ou invérifiable, et répondre « je ne sais pas » ne fait avancer personne. Il existe une troisième réponse, plus utile : « c'est à au plus quelques pour cent de ce qu'on pouvait espérer, et voici la preuve ». Ce chapitre construit cette preuve, la borne, à partir d'une remarque du chapitre précédent : l'optimum continu d'un programme linéaire est une borne de son optimum en nombres entiers. Il montre que la borne se calcule sans connaître la meilleure solution, puis donne juste ce qu'il faut de méthode exacte pour résoudre les petits cas et s'en servir de règle. Le reste du parcours cherche de bonnes solutions par des heuristiques ; ce chapitre sert à les juger.

Juger sans connaître la réponse

Retour à la sélection du lundi : C4, C1 et C9, 97 heures, 13 200 €. Le chapitre 1 l'a évaluée ; elle est réalisable, et rien n'indique qu'elle soit la meilleure. Pour dire si elle est bonne, il faudrait connaître l'optimum, et on ne le connaît pas : c'est justement ce qu'on cherche.

Le premier réflexe est de trouver un nombre que l'optimum ne peut pas dépasser, sans le calculer. Un premier nombre s'écrit sans réfléchir : la somme de toutes les marges.

main.py
Sortie
>_ Prêt à exécuter…

C'est une vraie borne : aucune sélection ne peut rapporter plus que la somme de toutes les marges. Mais elle affirme que la sélection est à au plus 54 % de l'optimum, ce qui ne rassure personne. Une borne trop lâche est vraie et inutile. Tout l'art est d'en trouver une serrée, qui coûte peu à calculer.

Borne et écart garanti

Dans un problème où l'on maximise (une marge), une borne supérieure BB est un nombre que l'optimum ne peut pas dépasser : opt≤B\text{opt} \le B.

Dans un problème où l'on minimise (une longueur, un nombre de barres, un temps), une borne inférieure LL est un nombre que l'optimum ne peut pas franchir par en dessous : opt≥L\text{opt} \ge L.

Si une solution réalisable a pour valeur vv, l'écart garanti est la distance de vv à la borne, rapportée à la borne :

maximisation : B−vBminimisation : v−LL\text{maximisation : } \frac{B - v}{B} \qquad\qquad \text{minimisation : } \frac{v - L}{L}

L'écart réel à l'optimum est au plus cet écart garanti. L'optimum, qu'on ignore, est coincé entre la meilleure solution trouvée et la borne.

Pourquoi l'écart garanti majore-t-il l'écart réel ? Pour une maximisation, v≤opt≤Bv \le \text{opt} \le B. L'écart réel est (opt−v)/opt=1−v/opt(\text{opt} - v)/\text{opt} = 1 - v/\text{opt}, et 1−v/x1 - v/x croît avec xx : remplacer l'optimum par la borne, plus grande, ne peut qu'augmenter la fraction. Pour une minimisation, L≤opt≤vL \le \text{opt} \le v, l'écart réel est (v−opt)/opt=v/opt−1(v - \text{opt})/\text{opt} = v/\text{opt} - 1, et v/x−1v/x - 1 décroît avec xx : remplacer l'optimum par la borne, plus petite, ne peut là aussi qu'augmenter la fraction. C'est cette phrase, « à au plus 3 % », que le directeur peut entendre : elle ne suppose pas qu'on ait résolu le problème.

Une borne ne se calcule pas avec l'optimum, elle se prouve. Ce qu'on demande à une preuve, c'est d'être correcte et de tenir en peu de calcul. Il y a deux sources de bornes, et ce chapitre les emploie toutes les deux.

  • Des raisonnements de bon sens : le nombre de barres ne peut pas être inférieur à la longueur totale divisée par celle d'une barre.
  • Une relaxation : on desserre une contrainte du problème jusqu'à ce qu'il devienne facile à résoudre. Le problème desserré a au moins autant de solutions, donc un optimum au moins aussi bon. C'est l'objet de la section suivante.

Toute solution réalisable est aussi une borne, de l'autre côté : dans une maximisation, elle minore l'optimum. La marge de 13 200 € dit que l'optimum vaut au moins 13 200 €, et la relaxation dira qu'il vaut au plus un certain nombre. Les heuristiques du reste du parcours fournissent le côté « au moins », les relaxations le côté « au plus ».

Vérification rapideon peut se reprendre

Une tournée de perçage mesure 650 mm, et on sait par un raisonnement que toute tournée fait au moins 600 mm. Que peut-on affirmer ?

Relaxer : autoriser une fraction de commande

Retour aux commandes. La contrainte qui rend le problème difficile est le « tout ou rien » : une commande se lance en entier ou pas. Il suffit de la desserrer : on s'autorise à ne lancer qu'une fraction d'une commande, la moitié, un tiers, avec une marge et des heures proportionnelles. C'est la relaxation continue du sac à dos.

Le problème desserré est facile. Chaque heure de machine doit aller à la commande qui la rentabilise le mieux, c'est-à-dire celle dont la marge par heure est la plus grande. On classe donc les commandes par rendement décroissant, on les remplit dans cet ordre tant qu'elles tiennent, et la première qui ne tient plus est prise en partie, juste de quoi combler les heures restantes.

Pourquoi le résultat est-il une borne ? Parce qu'une sélection en tout ou rien est un cas particulier d'une sélection fractionnaire : lancer une commande en entier, c'est en lancer la fraction 1, en ne pas lancer, la fraction 0. Toute sélection réalisable du vrai problème est donc réalisable dans le problème desserré. Ce dernier a plus de solutions, son meilleur optimum est au moins aussi grand, et il majore celui du problème d'origine. La preuve tient en deux lignes et ne suppose rien de l'optimum.

La figure suivante fait faire le chemin dans l'autre sens. On y charge une sélection de commandes dans le conteneur, en glissant les caisses, en les cliquant ou au clavier, et on demande ensuite la comparaison à la borne. La figure ne dit jamais si la sélection est la meilleure : la borne est une preuve, pas une référence.

Commandes sous capacité10 commandes, capacité 100 h
C1 · 38 h5 200 €137 €/hC2 · 12 h1 300 € · 108 €/hC3 · 27 h3 300 €122 €/hC4 · 45 h6 100 €136 €/hC5 · 18 h2 100 €117 €/hC6 · 22 h2 900 €132 €/hC7 · 9 h · 800 €C8 · 31 h4 300 €139 €/hC9 · 14 h1 900 € · 136 €/hC10 · 7 h · 600 €
Heures
0 / 100
Marge
0 €
Capacité
respectée
Charger les commandes du conteneur. La comparaison à la borne se demande une fois la sélection choisie.
Rendementplein : 136 à 139 €/hrayé : 117 à 132 €/hpointillé : 86 à 108 €/h
Les dix commandes de la semaine, capacité de 100 heures de machine. La hauteur d'une caisse suit ses heures ; le trait pointillé marque la capacité. À la demande, la relaxation remplit le conteneur par rendement décroissant et coupe la dernière caisse.
À manipuler
Charger des caisses dans le conteneur, regarder le niveau se remplir et la marge défiler, puis demander la comparaison à la borne. La sélection est mise de côté et le conteneur se remplit par rendement décroissant : les commandes entières, puis la dernière, tranchée à la fraction qui tient, avec ce qui reste dessiné en pointillé au-dessus du trait. Les deux jauges montrent la borne et la marge de la sélection, et le pourcentage annoncé est l'écart garanti. Revenir à sa sélection, en essayer d'autres, une à la fois, et regarder la liste des sélections comparées : l'écart garanti se resserre quand la marge monte. Se demander jusqu'où il peut descendre, et pourquoi il ne peut jamais atteindre zéro tant que la commande coupée reste coupée. Charger au-delà de la capacité montre aussi ce que la borne dit d'une sélection irréalisable : rien.

Voici le même calcul, à la main puis par un solveur de programmation linéaire, écrit avec PuLP, la bibliothèque de modélisation du chapitre 3. Les variables y sont continues, chacune entre 0 et 1 : c'est la relaxation. Le premier bloc qui importe pulp met quelques secondes à démarrer : le solveur se charge une fois, puis tout va vite.

main.py
Sortie
>_ Prêt à exécuter…

Le solveur et le calcul à la main s'accordent : la borne de la relaxation est de 13 704,4 €. La relaxation lance C8, C1 et C9 en entier (83 heures) puis 17 des 45 heures de C4, soit 38 % de la commande, pour 2 304,4 € de marge. Aucune sélection réelle ne peut rapporter davantage, donc la sélection du lundi, à 13 200 €, est à au plus 3,7 % de l'optimum. Ce nombre annonce ce qu'il est impossible de dépasser, non ce qu'il est possible d'atteindre : lancer 38 % d'une commande n'est pas une décision d'atelier, et prendre C4 en entier mènerait à 128 heures.

Lire la borne de la relaxation

  • 1.

    Après C8, C1 et C9, il reste 17 heures. Quelle part de la commande C4 (45 heures) la relaxation lance-t-elle, en fraction entre 0 et 1 ?

  • 2.

    Quelle marge cette fraction de C4 rapporte-t-elle, en euros, la marge de C4 étant de 6 100 € ?

  • 3.

    Quelle est la borne de la relaxation, en euros ? Les marges de C8, C1 et C9 sont 4 300, 5 200 et 1 900 €.

  • 4.

    Quel écart garanti, en pour cent, pour la sélection à 13 200 € ? Utiliser la borne 13 704,4.

La borne du glouton du lundi

Une seule étape est fausse. Désigner laquelle.

Une technicienne cherche une borne pour la sélection du lundi, C4, C1 et C9, que le glouton par marge décroissante a choisie. Pour éprouver sa démarche, la capacité de la semaine est notée cc, entre 83 et 100 heures.

Deux bornes en une ligne : la découpe et l'affectation

Deux autres problèmes de Valdrome ont leur borne, et elle tient en une ligne de calcul.

La découpe des barres. Vingt pièces sont à tirer de barres de 6 000 mm. Une barre ne contient au plus que 6 000 mm de pièces, donc il en faut au moins autant que la longueur totale divisée par 6 000, arrondie au-dessus : on ne commande pas 4,92 barres. C'est une borne inférieure.

L'affectation des opérateurs. Huit opérateurs, huit postes, un temps par couple. Chaque opérateur mettra au minimum le temps de sa meilleure case, donc le temps total est au moins la somme des minima de chaque ligne. C'est aussi une borne inférieure : elle relâche la contrainte « un opérateur par poste », en laissant deux opérateurs prendre le même.

main.py
Sortie
>_ Prêt à exécuter…

La découpe donne une borne de 5 barres. Une règle simple, qui range les pièces de la plus longue à la plus courte, en utilise 6 : elle est à au plus 20 % de l'optimum, et il reste à savoir si la barre de trop est évitable. La borne dit qu'on n'ira pas sous 5 ; elle ne dit pas qu'on y arrive. La marge est faible : les 29 500 mm de pièces occupent presque tout l'espace de 5 barres (30 000 mm), il ne reste que 500 mm de jeu.

L'affectation donne une borne de 88 minutes, et le programme montre pourquoi aucune affectation ne l'atteint : chacun prend son meilleur poste, et Dimitri et Amel visent tous deux le tournage, Chloé et Hugo le fraisage, Élise et Farid la peinture, tandis que la soudure, le montage et le contrôle n'ont pas de preneur. Cette affectation-là n'existe pas, puisqu'elle met deux opérateurs au même poste. C'est le cas typique d'une borne qui n'est pas atteinte : elle est correcte, elle est utile, et elle est plus basse que l'optimum. Ce qui trahirait une erreur, c'est l'inverse : une solution réalisable en dessous d'une borne inférieure, ou au-dessus d'une borne supérieure. Ce test, écrit dans un programme, attrape les erreurs de calcul d'un coût sans connaître l'optimum.

Les bornes de la découpe et de l'affectation

  • 1.

    Combien vaut la longueur totale des vingt pièces divisée par la longueur d'une barre ? Ne pas encore arrondir.

  • 2.

    Quel écart garanti, en pour cent, pour une découpe en 6 barres, la borne étant de 5 barres ?

  • 3.

    Quel écart garanti, en pour cent, pour l'affectation « chacun au poste de même numéro », qui coûte 172 minutes, la borne étant de 88 minutes ?

Résoudre les petits cas : un solveur en nombres entiers

Un programme linéaire dont certaines variables sont contraintes d'être entières s'appelle, en français, un programme linéaire en nombres entiers (PLNE) ; en anglais, mixed integer linear programming (MILP). PuLP l'écrit comme un programme linéaire : une variable se déclare entière à sa création, cat="Integer", ou cat="Binary" pour une variable qui vaut 0 ou 1. Le même modèle se résout en nombres entiers par prob.solve(), et sa relaxation continue par prob.solve(HiGHS_SCIPY(mip=False)). Dans la page, PuLP confie le calcul à HiGHS, le solveur de scipy ; sur un poste, il appelle par défaut un autre programme de résolution, CBC, qui rend les mêmes valeurs optimales, mais peut rendre une autre solution quand plusieurs sont à égalité. Les trois problèmes de Valdrome tiennent chacun en quelques lignes.

Les commandes. Une variable par commande, égale à 1 si elle est lancée et 0 sinon. Une contrainte : la somme des heures des commandes lancées ne dépasse pas la capacité. Le programme résout d'abord la relaxation, pour comparer, puis le problème en nombres entiers, en redonnant chaque fois son solveur à solve : sans argument, le second appel reprendrait le premier, mip=False compris.

main.py
Sortie
>_ Prêt à exécuter…

Le solveur retient C4, C6 et C8 : 98 heures, marge de 13 300 €. C'est l'optimum, prouvé par le solveur, et le seul : aucune autre sélection n'atteint 13 300 €, si bien que CBC rendrait les trois mêmes commandes. La sélection du lundi (13 200 €) était donc à 100 € de l'optimum, bien mieux que ce que l'écart garanti de 3,7 % laissait craindre, et la relaxation à 13 704,4 € en était à 404 € au-dessus.

L'affectation. Une variable xipx_{ip} par couple opérateur-poste, égale à 1 quand l'opérateur ii tient le poste pp. Chaque opérateur tient un poste, chaque poste a un opérateur : 16 égalités.

main.py
Sortie
>_ Prêt à exécuter…

Le solveur rend 95 minutes, avec poste_de = [6, 2, 4, 0, 7, 5, 3, 1], la seule affectation à 95 minutes parmi les 40 320 possibles. La borne des minima de ligne, 88, était 7 minutes sous l'optimum : elle n'était pas fausse, elle n'était pas atteinte, parce qu'elle ne pouvait pas savoir que trois postes étaient sans preneur.

Une remarque sur la relaxation de l'affectation : si l'on donne au solveur les mêmes 64 variables sans exiger qu'elles soient entières, par prob.solve(HiGHS_SCIPY(mip=False)), il rend 95 exactement, la même valeur. C'est un fait propre à cette structure, que le problème de transport du chapitre 3 montre aussi (les sommets du domaine réalisable, en dimension 64, sont tous entiers), et ce n'est vrai ni du sac à dos, dont la relaxation était à 13 704, ni de la découpe. La relaxation est parfois exacte, souvent serrée, jamais garantie de l'être. L'affectation est d'ailleurs le seul des cinq problèmes de Valdrome à se résoudre en temps polynomial : la méthode hongroise (scipy.optimize.linear_sum_assignment) rend 95 minutes instantanément.

La découpe. Une variable xibx_{ib} égale à 1 quand la pièce ii est dans la barre bb, une variable yby_b égale à 1 quand la barre bb sert. Chaque pièce est dans une barre, et la longueur des pièces d'une barre ne dépasse pas 6 000 mm si la barre sert. On minimise le nombre de barres qui servent. On offre au modèle 6 barres, ce que la règle simple a déjà réussi.

main.py
Sortie
>_ Prêt à exécuter…

Le solveur trouve 5 barres, la borne. Une solution qui atteint une borne inférieure est optimale, et la preuve est complète : personne ne fera mieux que 5, et le modèle en a trouvé une avec 5. La barre de trop de la règle simple était donc évitable.

Ce que dit le statut, et ce qu'il ne dit pas. Sur un problème plus gros, une limite de temps, prob.solve(HiGHS_SCIPY(timeLimit=60)), peut arrêter le solveur avec la meilleure solution trouvée, sans avoir prouvé l'optimum. Le statut dit alors encore "Optimal", comme avec CBC, et c'est prob.sol_status qui distingue : 1 pour un optimum prouvé, 2 pour une solution trouvée sans preuve. Par défaut, HiGHS s'arrête aussi dès que l'écart entre sa meilleure solution et sa borne passe sous 0,01 % ; cela ne change rien aux petits cas de ce chapitre, et pour une preuve exacte on lui demande un écart nul, HiGHS_SCIPY(gapRel=0), comme le fait la boucle de la tournée plus loin. Le chapitre 13 y revient.

Séparer et évaluer

Quand le solveur prouve un optimum, il ne teste pas toutes les solutions. Il fait ce que fait l'arbre suivant, appelé séparation et évaluation (en anglais branch and bound).

Séparation, évaluation, élagage

La séparation coupe l'ensemble des solutions possibles en deux : on choisit une variable et on regarde d'un côté les solutions où elle vaut 1, de l'autre celles où elle vaut 0. Chaque moitié est un nœud de l'arbre.

L'évaluation calcule, pour chaque nœud, une borne du meilleur qu'on puisse y trouver, ici la relaxation continue avec les variables déjà fixées.

L'élagage abandonne un nœud dont la borne n'est pas meilleure que la meilleure solution entière déjà connue : rien de ce qu'il contient ne peut la battre. C'est là que se gagne le temps.

Sur le sac à dos, la variable qu'on sépare est celle que la relaxation a coupée en fraction : c'est elle qui empêche la solution relâchée d'être une vraie sélection. Au sommet de l'arbre, la relaxation coupe C4 en fraction. On sépare donc « C4 est lancée » d'un côté, « C4 n'est pas lancée » de l'autre, et chaque côté a sa propre relaxation, calculée avec cette décision fixée. Quand une relaxation ne coupe plus aucune commande, elle est une vraie sélection, une solution entière, et l'on peut mettre à jour la meilleure connue.

Le programme suivant déroule l'arbre en profondeur, la branche « lancée » d'abord. Il n'affiche que les nœuds qui apprennent quelque chose : les deux premiers niveaux, les améliorations de la meilleure solution, et les premiers élagages.

main.py
Sortie
>_ Prêt à exécuter…

Le sommet de l'arbre a pour borne 13 704,4 et sépare sur C4 : la branche « C4 lancée » vaut au plus 13 684,2, la branche « C4 non lancée » au plus 13 640,9. Aucune ne peut être élaguée tout de suite, elles peuvent l'une comme l'autre contenir de bonnes sélections. L'arbre descend, trouve d'abord une sélection à 12 600 €, puis 12 700, 13 200 et enfin 13 300. Dès qu'il détient 13 200, tout nœud dont la relaxation est à 13 200 ou moins est coupé, et il y en a beaucoup : la borne a servi de détecteur de branches sans avenir.

À la fin, 93 nœuds ont été visités au lieu des 2 047 de l'arbre complet, et la dernière meilleure valeur, 13 300, est prouvée : toute branche non explorée avait une borne d'au plus 13 300. C'est la même preuve que le solveur.

Ce résumé ne dit pas tout, et ce qu'il ne dit pas compte. Le nombre de nœuds dépend de l'ordre dans lequel on visite les branches et de la meilleure solution déjà connue au départ : un meilleur point de départ élague davantage. Un solveur industriel ajoute d'autres bornes, d'autres règles de choix de la variable, et des heuristiques pour trouver de bonnes solutions tôt. L'idée reste celle-ci : des bornes sûres pour ne pas tout explorer.

Vérification rapideon peut se reprendre

Dans un arbre de séparation et évaluation pour une maximisation, la meilleure solution entière connue vaut 13 200. Un nœud a pour relaxation 13 150. Que fait-on ?

Et la tournée ?

Le voyageur de commerce de Valdrome, la tournée de la perceuse, se met lui aussi en nombres entiers, et c'est là que le solveur montre à quoi servent les coupes. Soit la plaque P-217 : le départ et les douze trous font treize points.

Le modèle. Une variable par arête, c'est-à-dire par paire de points : elle vaut 1 quand l'outil passe directement de l'un à l'autre, 0 sinon. Treize points font 13×12/2=7813 \times 12 / 2 = 78 variables. On minimise la somme des longueurs des arêtes retenues, et chaque point doit avoir exactement deux arêtes retenues, celle par où l'outil arrive et celle par où il repart : treize égalités.

Ce que le modèle laisse passer. « Chaque point a deux arêtes » décrit aussi bien une seule grande boucle, la tournée, que plusieurs boucles séparées qui couvrent ensemble les treize points. C'est le piège du chapitre 1 sous une autre forme : la représentation « pour chaque trou, le trou suivant » passait son test de validité avec deux boucles de trois trous. Le modèle a donc plus de solutions que le problème : c'est une relaxation, et sa valeur est une borne inférieure de la longueur de toute tournée.

La coupe. Pour une boucle formée d'un ensemble SS de points, qui ne contient pas tous les points, on ajoute la contrainte suivante, où ∣S∣|S| est le nombre de points de SS : parmi les arêtes dont les deux bouts sont dans SS, au plus ∣S∣−1|S| - 1 sont retenues. Une boucle qui se ferme sur SS en retiendrait exactement ∣S∣|S| : elle est interdite. Une vraie tournée, qui entre dans SS et en ressort, n'a jamais ∣S∣|S| arêtes à l'intérieur de SS : elle n'est pas touchée. Une contrainte qui écarte des solutions fausses sans écarter une seule solution vraie s'appelle une coupe ; celle-ci est une coupe de sous-tour.

La boucle. On résout, on cherche les boucles de la solution, on ajoute une coupe par boucle, et l'on recommence, jusqu'à ce que la solution soit d'un seul tenant. Une solution d'un seul tenant est une tournée, et comme sa longueur est le minimum du modèle relâché, aucune tournée ne peut faire mieux : c'est l'optimum.

main.py
Sortie
>_ Prêt à exécuter…

La première passe rend 633,23 mm, mais en deux boucles : l'une autour du départ (départ, T1, T4, T7, T9), l'autre avec les huit autres trous. Ce n'est pas une tournée, et pourtant ce nombre est déjà une borne inférieure de toute tournée. À la passe suivante, après une coupe par boucle, le solveur rend une seule boucle, de la même longueur : elle atteint la borne, elle est optimale, et c'est le 633,23 mm que la programmation dynamique de Held et Karp avait donné au chapitre 2. Un hasard de la plaque fait ici que les deux solutions ont la même longueur : T7, T9, T10 et T11 forment un carré de 30 mm de côté, et l'on peut en relier les côtés de deux façons de même longueur, l'une qui ferme une boucle à part, l'autre qui garde une seule tournée.

Le plus souvent, la solution à plusieurs boucles est plus courte que la meilleure tournée, et il faut plusieurs passes. Sur les soixante trous de la plaque P-600, la longueur rendue par le solveur monte de 2 607 mm (onze boucles) à 2 788,70 mm (une seule) en quatre passes : c'est l'optimum que le chapitre recherche locale prend pour référence. Held et Karp n'est donc qu'une méthode exacte parmi d'autres, et celle-ci va beaucoup plus loin en pratique. Elle n'offre pourtant aucune garantie de temps, ce que la section suivante mesure.

Quand l'exact ne suit plus

Le sac à dos de dix commandes tient dans un arbre de 93 nœuds. Que devient-il avec vingt, trente, quarante commandes ? Le programme suivant engendre des instances par une formule reproductible et compte les nœuds du même arbre, jusqu'à trente commandes. Les poids sont pris entre 20 et 80 et la marge égale le poids plus 100 ; la capacité est la moitié du poids total. C'est une famille dite fortement corrélée, réputée difficile pour la séparation et l'évaluation : la marge d'une sélection vaut son poids total plus 100 par commande retenue, si bien que les sélections qui remplissent la capacité avec le même nombre de commandes rapportent presque la même chose, et que la relaxation élague mal. Les rendements ne sont pourtant pas égaux (de 2,25 à 6,0 sur l'instance de deux cents commandes plus bas) : c'est la ressemblance des sélections pleines qui gêne, pas celle des commandes.

main.py
Sortie
>_ Prêt à exécuter…

Le nombre de nœuds est très inégal d'une instance à l'autre, à taille égale : à trente commandes, de 41 à 120 283 nœuds, et l'on ne sait pas d'avance de quel côté on tombera. Il explose ensuite. Deux mesures faites à part, trop longues pour ce bloc, le montrent : à quarante commandes, les mêmes cinq graines donnent de 508 à 247 742 nœuds, alors que l'arbre complet en compterait plus de deux mille milliards ; et sur quarante instances par taille, avec une limite de deux millions de nœuds, aucune instance de trente commandes ne la dépasse, deux instances de quarante la dépassent, sept de cinquante et treize de soixante.

Cela dit ce que vaut cet arbre, qui n'emploie que la relaxation la plus simple et une exploration en profondeur. Cela ne dit pas que le sac à dos est hors de portée de l'exact. Voici les mêmes instances, avec deux cents et mille commandes en plus, résolues de deux autres façons exactes, puis jugées : l'écart d'une règle gloutonne à l'optimum, et sa garantie donnée par la borne.

main.py
Sortie
>_ Prêt à exécuter…

Les deux méthodes trouvent le même optimum, en moins d'une seconde, jusqu'à mille commandes. La programmation dynamique du chapitre gloutons et programmation dynamique profite de ce que la capacité reste un petit nombre entier (5 067 heures pour deux cents commandes) : son travail est de l'ordre du nombre de commandes multiplié par la capacité, et non de 2n2^n. Le sac à dos est NP-difficile, mais seulement au sens « faible » : sa difficulté tient à la taille de ses nombres. HiGHS, le solveur que PuLP appelle dans la page, ajoute à la séparation et évaluation des bornes plus fines, des coupes et des heuristiques que l'arbre du bloc précédent n'a pas. Le premier appel du bloc, sur une seule commande, ne sert qu'à charger le solveur, pour que son chargement ne compte pas dans la première mesure.

Une frontière existe pourtant, et elle dépend de la méthode et de l'instance plus que de la taille seule. Dans cette famille, la capacité grandit avec le nombre de commandes, et le travail de la programmation dynamique avec le produit des deux : il est multiplié par trois à quatre quand le nombre de commandes double, et à vingt mille commandes ce bloc demanderait une quinzaine de secondes dans la page. Pour la tournée, la boucle de la section précédente prouve l'optimum des soixante trous de P-600 en moins d'une seconde, puis met de l'ordre de dix secondes pour cent trous comme pour cent cinquante, et de quarante secondes pour deux cents (les 100, 150 et 200 premiers trous de la formule qui engendre P-600, décrite au chapitre recherche locale ; hors de la page, sur un poste, avec PuLP et HiGHS et une formulation sans raffinement : des logiciels spécialisés vont beaucoup plus loin). Deux leçons : le temps d'une méthode exacte se prévoit mal, il dépend de la structure de l'instance autant que de sa taille ; et une meilleure méthode déplace la frontière de plusieurs tailles, sans la faire disparaître.

Les bornes, elles, ne dépendent d'aucun solveur, et elles restent utiles quand on ne cherche pas l'optimum. La dernière colonne du tableau ne coûte qu'un tri, quelle que soit la taille, et elle certifie qu'une règle gloutonne est à moins de 3,5 % de l'optimum pour quarante commandes, à moins de 0,8 % pour deux cents, à moins de 0,2 % pour mille, sans avoir besoin de connaître l'optimum. Ici on le connaît (3 466, 17 667 et 89 024) : l'écart réel est de 1,2 %, 0,27 % et 0,06 %, ce qui rappelle qu'une borne garantit moins que ce que la solution vaut. C'est le contrat que la suite du parcours reprend : quand on ne cherche pas l'optimum, ou qu'on ne peut pas le prouver, on cherche de bonnes solutions par des heuristiques, et une borne bon marché dit jusqu'où elles pourraient progresser.

Ce que l'exact apporte au parcours. Trois choses, et pas une de plus. Un optimum de référence sur les cas qu'il sait résoudre, pour mesurer honnêtement une heuristique. Des bornes sur tous les autres, pour dire jusqu'où une solution peut encore être améliorée. Une preuve quand une solution atteint la borne. Quand la méthode exacte ne suit plus, on cherche et on borne, et les chapitres 6 à 11 s'occupent de chercher.

Les erreurs de débutant

Prendre la borne pour l'optimum
« La relaxation vaut 13 704, donc l'optimum vaut 13 704 » : faux. C'est un majorant, dont l'écart à l'optimum se compte en centaines d'euros ici. Ne jamais afficher une borne comme un résultat, et ne jamais annoncer « optimal » sans avoir vu une solution qui atteint la borne ou un solveur qui prouve l'optimum.
Se tromper de sens
Pour un coût à minimiser, toute tournée réalisable est déjà une borne supérieure : elle ne mesure aucun écart. Il faut un plancher que rien ne peut franchir. Vérifier le sens en posant la question : la solution trouvée peut-elle tomber du mauvais côté de la borne ? Si oui, il y a une erreur dans la borne ou dans le coût.
Oublier que le problème minimise
Un problème PuLP créé sans sens, LpProblem("commandes"), cherche un minimum : pour une marge, il faut écrire LpMaximize. Un oubli fait chercher les commandes qui rapportent le moins : sur les commandes de la semaine, le résultat est la sélection vide, à 0 €, et il faut penser à lire le résultat ; sur un problème où une contrainte impose un minimum, c'est un nombre plausible, et l'erreur est discrète. La forme brute d'un solveur, celle de scipy.optimize.milp, minimise toujours : le même oubli y consiste à ne pas changer le signe de la marge.
Lancer l'exact sans limite de temps
Le temps d'une méthode exacte ne se lit pas sur la taille seule : à trente commandes, le même arbre visite de 41 à 120 283 nœuds selon l'instance, et l'on ne sait pas d'avance de quel côté on tombe. Toujours fixer une limite de temps au solveur (prob.solve(HiGHS_SCIPY(timeLimit=60)) dans la page, PULP_CBC_CMD(timeLimit=60) sur un poste), lire son statut et prob.sol_status, et garder sous la main une borne à bas prix pour savoir quoi conclure d'une solution partielle.

Mettre en pratique

Le plus proche voisin donne, pour la plaque P-217, une tournée de 692,06 mm. Trouver une borne inférieure de la longueur d'une tournée sans rien calculer d'autre, et en déduire l'écart garanti.

Toute tournée doit aller de l'origine jusqu'au trou le plus éloigné, T2 en (180 ; 105), et en revenir. Cela mesure au moins 2×1802+1052≈2×208,39=416,772 \times \sqrt{180^2 + 105^2} \approx 2 \times 208{,}39 = 416{,}77 mm : c'est une borne inférieure, correcte et facile à prouver. L'écart garanti est (692,06−416,77)/416,77≈66 %(692{,}06 - 416{,}77)/416{,}77 \approx 66\,\%.

La tournée est donc « à au plus 66 % de l'optimum », ce qui n'apprend presque rien : la borne est vraie mais lâche, parce qu'elle ne compte que le trajet vers un seul trou. Une borne plus serrée demande de tenir compte de tous les trous, et l'on voit qu'une borne se mérite.

Un nœud de l'arbre a pour relaxation 13 250,3 €. Les marges sont toutes des multiples de 100 €. La meilleure solution connue vaut 13 200 €. Peut-on l'élaguer ?

Pas d'après la règle simple : 13 250,3 est strictement au-dessus de 13 200. Mais toute sélection réalisable de ce nœud rapporte un multiple de 100 €, donc au plus 13 200 € (le multiple de 100 juste en dessous de 13 250,3). Elle ne peut pas battre la meilleure connue : on peut l'élaguer. Ce raffinement, qui arrondit la borne vers le bas au plus grand multiple de 100 qu'elle contient, est un exemple de ce que les solveurs ajoutent pour élaguer davantage.

Pour lancer un solveur sur l'affectation, quel est le nombre de variables et de contraintes ?

Huit opérateurs et huit postes donnent 64 variables xipx_{ip}, égales à 0 ou 1. Chaque opérateur tient exactement un poste (8 égalités) et chaque poste est tenu par exactement un opérateur (8 égalités) : 16 contraintes. Si la famille « chaque poste est tenu par un opérateur » manque, plusieurs opérateurs peuvent tenir le même poste et le solveur retombe sur la borne des lignes, 88 minutes ; si c'est la famille « chaque opérateur tient un poste » qui manque, un opérateur peut tenir plusieurs postes et le solveur retombe sur la borne des colonnes, 83 minutes. Dans les deux cas, aucun atelier ne réalise ce résultat.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices