Exploitez la dualité
Un brasseur voisin fait une proposition inattendue à notre brasserie : plutôt que de brasser cette semaine, pourquoi ne pas lui louer toutes ses ressources, cuves, malt et houblon ? Il reste à fixer un prix. Trop bas, notre brasseur y perd par rapport à sa propre production ; trop haut, le voisin refuse.
Cette question de prix cache l’une des idées les plus belles de l’optimisation : à tout programme linéaire est associé un programme dual, qui en est le reflet exact. Dans ce chapitre, vous allez apprendre à l’écrire, à l’utiliser pour certifier qu’une solution est optimale, à lire les prix des ressources dans le tableau du simplexe, et à résoudre un jeu entre deux concurrents.
Construire le dual par le raisonnement économique
Section intitulée « Construire le dual par le raisonnement économique »Rappelons le programme de la brasserie, que nous appellerons désormais le primal :
Le voisin propose un prix par hectolitre de cuve, par sac de malt et par lot de houblon. Quand notre brasseur acceptera-t-il ? Seulement si louer vaut au moins autant que produire, pour chaque bière :
- les ressources d’un hectolitre de blonde (1 cuve, 1 sac, 1 lot) doivent rapporter au moins 3 : ;
- celles d’un hectolitre d’ambrée (1 cuve, 3 sacs) au moins 2 : .
Le voisin, lui, veut payer le moins possible pour l’ensemble du stock. Il résout donc :
C’est le dual de la brasserie. Regardez bien sa structure : les seconds membres du primal (4, 9, 3) sont devenus les coefficients de l’objectif, les coefficients de l’objectif (3, 2) sont devenus les seconds membres, et la matrice des contraintes a été transposée (les colonnes du primal sont les lignes du dual).
Les règles de passage
Section intitulée « Les règles de passage »Sous forme matricielle, le couple primal-dual « canonique » s’écrit :
où est la matrice des contraintes, le vecteur des seconds membres, celui des coûts, et contient une variable duale par contrainte du primal. Quand le primal n’est pas sous cette forme, on applique les correspondances suivantes (pour un primal de maximisation) :
| Primal (max) | Dual (min) |
|---|---|
| contrainte de type | variable |
| contrainte de type | variable |
| contrainte d’égalité | variable de signe quelconque (libre) |
| variable | contrainte de type |
| variable | contrainte de type |
| variable libre | contrainte d’égalité |
Le tableau se lit aussi de droite à gauche : le dual du dual est le primal.
Dualité faible et dualité forte
Section intitulée « Dualité faible et dualité forte »Prenez n’importe quelle production réalisable et n’importe quels prix réalisables . Alors
La première inégalité utilise et , la seconde et . C’est la dualité faible : la valeur de toute solution du primal (max) est inférieure ou égale à celle de toute solution du dual (min). Économiquement, aucun plan de production ne rapporte plus que ce que coûte n’importe quelle offre de location acceptable.
Conséquence immédiate et très pratique : si vous trouvez un et un réalisables de même valeur, ils sont tous les deux optimaux. Le sert de certificat d’optimalité pour .
La dualité forte affirme que ce n’est pas un hasard : si le primal a une solution optimale, le dual aussi, et leurs valeurs optimales sont égales. Les autres situations sont tout aussi encadrées : si le primal est non borné, le dual est irréalisable (et inversement), ce que la dualité faible suffit d’ailleurs à montrer.
Les écarts complémentaires et les prix des ressources
Section intitulée « Les écarts complémentaires et les prix des ressources »À l’optimum, un lien plus fin relie les deux problèmes, les écarts complémentaires :
En mots : une ressource qui n’est pas épuisée a un prix nul ( si la contrainte n’est pas saturée), et une bière effectivement produite a une contrainte duale saturée (ses ressources, au prix dual, valent exactement son bénéfice).
Mais ces prix, on les avait déjà sous les yeux ! Le dictionnaire final du simplexe (chapitre 2) se terminait par . Les coûts réduits des variables d’écart, au signe près, sont exactement les variables duales : pour (cuves), pour (malt, en base, coût réduit nul), pour (houblon). Un simplexe résout toujours les deux problèmes à la fois.
Ces valeurs ont un sens très concret : ce sont des prix marginaux (ou prix fictifs, shadow prices). Relisez : laisser un hectolitre de cuve vide fait perdre 2 ; symétriquement, une cuve de plus ferait gagner 200 € (tant que la base optimale ne change pas). Un sac de malt de plus ne rapporterait rien, puisqu’il en reste déjà. Si le brasseur doit investir, c’est dans les cuves, pas dans le malt.
from scipy.optimize import linprog
res = linprog([-3, -2], A_ub=[[1, 1], [1, 3], [1, 0]], b_ub=[4, 9, 3], bounds=[(0, None), (0, None)], method="highs")# marginals = dérivée de l'objectif minimisé (-z) par rapport à b_ubprint(res.x, -res.fun) # [3. 1.] 11.0print(-res.ineqlin.marginals) # prix des ressources : 2, 0 et 1Les jeux matriciels à somme nulle
Section intitulée « Les jeux matriciels à somme nulle »La dualité a une application inattendue : la théorie des jeux. Chaque samedi, notre brasserie et un concurrent installent chacun un stand sur l’un de deux marchés rennais, sans savoir ce que fera l’autre. Selon les choix, la brasserie prend au concurrent une part de ses ventes (en centaines d’euros) :
| Brasserie \ Concurrent | Marché 1 | Marché 2 |
|---|---|---|
| Marché 1 | 3 | 1 |
| Marché 2 | 2 | 4 |
Ce que gagne l’un, l’autre le perd : c’est un jeu à somme nulle, décrit par la matrice . La brasserie (le joueur « ligne ») veut maximiser son gain, le concurrent (le joueur « colonne ») veut le minimiser.
Existe-t-il un choix fixe raisonnable ? En jouant la ligne 1, la brasserie est sûre de gagner au moins 1 ; en jouant la ligne 2, au moins 2. Elle peut donc se garantir 2. Le concurrent, en jouant la colonne 1, ne perd jamais plus de 3 ; en jouant la colonne 2, jamais plus de 4 : il se garantit 3. Comme , il n’y a pas de point selle : quel que soit le choix fixe de l’un, l’autre a intérêt à s’adapter. La solution est de jouer au hasard, avec des probabilités bien choisies : c’est une stratégie mixte.
Si la brasserie joue la ligne 1 avec la probabilité , son gain moyen vaut si le concurrent va au marché 1, et s’il va au marché 2. Le concurrent choisira ce qui l’arrange, donc la brasserie maximise le minimum des deux : elle les égalise, , soit et un gain garanti . Le même calcul côté concurrent donne pour la colonne 1, et la même valeur .
Le lien avec la programmation linéaire
Section intitulée « Le lien avec la programmation linéaire »Pour des matrices plus grandes, on ne peut plus égaliser à la main. On écrit le problème du joueur ligne : maximiser sous pour toute colonne , avec et . Si tous les gains sont positifs (on peut toujours ajouter une constante à ), et le changement de variable donne un programme linéaire :
où est le vecteur rempli de 1. La valeur du jeu vaut , et l’on récupère les stratégies par et . Et voici le point clé : ces deux programmes sont duaux l’un de l’autre. La dualité forte garantit donc qu’ils ont la même valeur : c’est le théorème du minimax de von Neumann, qui affirme que le gain que la brasserie peut se garantir est exactement la perte maximale que le concurrent peut s’imposer.
En résumé
Section intitulée « En résumé »- Le dual associe une variable à chaque contrainte du primal ; on transpose et l’on échange les rôles de et .
- Pour un primal de maximisation, une contrainte donne une variable duale positive, une égalité une variable libre, et une variable positive une contrainte duale .
- Dualité faible : primal (max) dual (min) ; dualité forte : égalité à l’optimum. Un couple réalisable de même valeur est optimal.
- Écarts complémentaires : ressource non épuisée, prix nul ; variable primale positive, contrainte duale saturée.
- Les variables duales sont les prix marginaux des ressources, lisibles (au signe près) dans les coûts réduits des écarts du tableau final.
- Un jeu matriciel à somme nulle se résout par deux programmes linéaires duaux ; leur égalité est le théorème du minimax.
Exercices
Section intitulée « Exercices »Exercice · Écrire et résoudre un dual
facileÉcrivez le dual de sous , , , dont l’optimum est . Utilisez les écarts complémentaires pour trouver la solution duale, puis vérifiez la dualité forte.
Voir la solution
Le dual est sous , , .
À l’optimum primal, et : les deux contraintes duales sont saturées, et . On trouve , positifs donc réalisables. Valeur duale : , égale à la valeur primale . La dualité forte est vérifiée. On retrouve d’ailleurs ces valeurs dans le dictionnaire final du chapitre 2, .
En code
import numpy as npfrom scipy.optimize import linprog
A, b, c = np.array([[2, 1], [1, 2]]), np.array([8, 7]), np.array([1, 1])primal = linprog(-c, A_ub=A, b_ub=b) # max c'xdual = linprog(b, A_ub=-A.T, b_ub=-c) # min b'y sous A'y >= cprint(primal.x, -primal.fun) # [3. 2.] 5.0print(dual.x, dual.fun) # [0.333 0.333] 5.0
# Écarts complémentaires : x1, x2 > 0, donc A'y = cy = np.linalg.solve(A.T, c)print(y, b @ y) # [0.333 0.333] 5.0print(-primal.ineqlin.marginals) # [0.333 0.333] : les mêmes prixA <- rbind(c(2, 1), c(1, 2)); b <- c(8, 7); cc <- c(1, 1)# Écarts complémentaires : x1 = 3 > 0 et x2 = 2 > 0, donc A'y = cy <- solve(t(A), cc)y # 0.333 0.333 : positifs, donc réalisablessum(b * y); sum(cc * c(3, 2)) # 5 et 5 : dualité forte vérifiée
# Contrôle : sommets du dual {A'y >= c, y >= 0}, écrit -A'y <= -cG <- rbind(-t(A), diag(-1, 2)); h <- c(-cc, 0, 0)for (i in combn(4, 2, simplify = FALSE)) { if (abs(det(G[i, ])) < 1e-9) next s <- solve(G[i, ], h[i]) if (all(G %*% s <= h + 1e-9)) cat("sommet", round(s, 3), "coût", sum(b * s), "\n")}# sommet 0.333 0.333 coût 5 ; sommet 0 1 coût 7 ; sommet 1 0 coût 8 : le minimum vaut 5Exercice · Le prix des besoins
moyenOn reprend le problème d’achat de céréales du chapitre 3 : sous , , , d’optimum et de coût 9. Écrivez son dual, résolvez-le graphiquement et interprétez ses variables.
Voir la solution
Le problème est une minimisation avec des contraintes et des variables positives : c’est la forme (D) du cours. Son dual est donc de la forme (P) : sous , , .
Les sommets du domaine sont , , et , de valeurs 0, 8, 9 et 6. L’optimum est , de valeur 9, égale au coût minimal : la dualité forte est vérifiée. Ce sont aussi, au signe près, les coûts réduits et des excédents dans le dictionnaire final .
Interprétation : est le coût marginal d’une unité d’amidon exigée en plus (150 €), celui d’une unité d’arômes (50 €). On peut aussi y voir le prix maximal qu’un producteur d’amidon ou d’arômes « purs » pourrait demander pour que la brasserie lui achète directement ses nutriments.
En code
from scipy.optimize import linprog
# Primal : min 2 x1 + 3 x2 sous x1 + x2 >= 4, x1 + 3 x2 >= 6 (écrits en <= avec un signe -)primal = linprog([2, 3], A_ub=[[-1, -1], [-1, -3]], b_ub=[-4, -6])print(primal.x, primal.fun) # [3. 1.] 9.0print(-primal.ineqlin.marginals) # [1.5 0.5] : coût d'une unité de besoin en plus
# Dual : max 4 y1 + 6 y2 sous y1 + y2 <= 2, y1 + 3 y2 <= 3dual = linprog([-4, -6], A_ub=[[1, 1], [1, 3]], b_ub=[2, 3])print(dual.x, -dual.fun) # [1.5 0.5] 9.0# Dual : max 4 y1 + 6 y2 sous y1 + y2 <= 2, y1 + 3 y2 <= 3, y >= 0G <- rbind(c(1, 1), c(1, 3), c(-1, 0), c(0, -1)); h <- c(2, 3, 0, 0)S <- NULLfor (i in combn(4, 2, simplify = FALSE)) { if (abs(det(G[i, ])) < 1e-9) next y <- solve(G[i, ], h[i]) if (all(G %*% y <= h + 1e-9)) S <- rbind(S, y)}cbind(S, valeur = S %*% c(4, 6))# (1.5, 0.5) : 9 ; (2, 0) : 8 ; (0, 1) : 6 ; (0, 0) : 0S[which.max(S %*% c(4, 6)), ] # 1.5 0.5, de valeur 9 = coût minimal du primalExercice · Résoudre un jeu matriciel
difficileDeux joueurs s’affrontent avec la matrice de gains (pour le joueur ligne) . Le jeu a-t-il un point selle ? Trouvez les stratégies optimales et la valeur du jeu, puis vérifiez avec le programme linéaire du joueur ligne.
Voir la solution
Minimum de chaque ligne : 1 et 2, donc le joueur ligne se garantit 2 avec une stratégie fixe. Maximum de chaque colonne : 4 et 3, donc le joueur colonne se garantit de ne pas perdre plus de 3. Comme , il n’y a pas de point selle.
Joueur ligne (ligne 1 avec probabilité ) : gain contre la colonne 1, contre la colonne 2. En égalisant, et .
Joueur colonne (colonne 1 avec probabilité ) : perte contre la ligne 1, contre la ligne 2. En égalisant, et la valeur est bien .
Vérification : le programme sous , a pour solution , qui sature les deux contraintes ( et ) et vaut .
En code
import numpy as npfrom scipy.optimize import linprog
A = np.array([[4, 1], [2, 3]])print(A.min(axis=1).max(), A.max(axis=0).min()) # 2 3 : pas de point selle
# Joueur ligne : min x1 + x2 sous A'x >= 1, x >= 0res = linprog([1, 1], A_ub=-A.T, b_ub=[-1, -1])v = 1 / res.funprint(res.x, v, v * res.x) # [0.1 0.3] 2.5 [0.25 0.75]# La stratégie du joueur colonne est dans les multiplicateursprint(v * -res.ineqlin.marginals) # [0.5 0.5]A <- rbind(c(4, 1), c(2, 3))max(apply(A, 1, min)); min(apply(A, 2, max)) # 2 et 3 : pas de point selle
# Joueur ligne : min x1 + x2 sous A'x >= 1, x >= 0, écrit G x <= hG <- rbind(-t(A), diag(-1, 2)); h <- c(-1, -1, 0, 0)S <- NULLfor (i in combn(4, 2, simplify = FALSE)) { if (abs(det(G[i, ])) < 1e-9) next x <- solve(G[i, ], h[i]) if (all(G %*% x <= h + 1e-9)) S <- rbind(S, x)}x <- S[which.min(rowSums(S)), ]v <- 1 / sum(x)x; v; v * x # 0.1 0.3 ; 2.5 ; p = 0.25 0.75q <- solve(A, rep(1, 2)); q / sum(q) # joueur colonne (A y = 1) : q = 0.5 0.5