Vue d'ensemble
Comment un programme décide-t-il de son prochain coup au morpion, au puissance 4 ou aux échecs ? L'idée centrale est étonnamment simple : explorer tous les avenirs possibles de la partie et jouer en supposant que l'adversaire, lui aussi, jouera de son mieux. C'est exactement ce que formalise l'algorithme minimax. On modélise la partie par un arbre, on attribue un score aux positions finales, puis on fait « remonter » ces scores jusqu'à la position actuelle en alternant maximisation (pour nous) et minimisation (pour l'adversaire).
Cette fiche s'inscrit dans le cadre des jeux à deux joueurs, à somme nulle, à information complète et sans hasard. C'est une application directe de la récursivité sur les arbres : si tu maîtrises « une fonction qui s'appelle sur ses sous-structures », tu tiens déjà 80 % du minimax.
Prérequis
- Récursivité : fonction qui s'appelle elle-même, cas de base / cas récursif, remontée des valeurs.
- Arbres (ou piles/files) : vocabulaire nœud, arête, racine, feuille, fils, profondeur.
- Complexité temporelle : ordre de grandeur, croissance exponentielle .
- Bases de Python : fonctions, listes,
max/min, testsisinstance.
Minimax, c'est de la récursivité déguisée en stratégie. Beaucoup d'élèves calent non pas sur le jeu, mais sur « qui joue à ce niveau ». Nos mentors alumni X · Centrale · Mines te font dérouler un arbre à la main jusqu'à ce que l'alternance min/max devienne un réflexe.
Trouver un mentor →1. Le cadre : jeux à deux joueurs à somme nulle
Tous les jeux visés ici partagent quatre propriétés. Elles garantissent que la partie est entièrement calculable, au moins en théorie.
On considère un jeu opposant deux joueurs qui jouent à tour de rôle, tel que :
- Somme nulle : ce que gagne l'un, l'autre le perd. Un unique score chiffre l'issue ; l'un cherche à le rendre grand, l'autre à le rendre petit.
- Information complète : à tout instant, chaque joueur connaît entièrement la position (aucune carte cachée).
- Sans hasard : aucun dé, aucun tirage ; le coup joué détermine seul la position suivante.
Exemples : morpion, puissance 4, échecs, dames, go. Contre-exemples : le poker (information cachée), le backgammon (dés).
On nomme les deux joueurs d'après leur objectif sur le score :
- MAX : le joueur qui veut maximiser le score final.
- MIN : le joueur qui veut le minimiser.
Par convention, on chiffre les issues du point de vue de MAX. Au morpion par exemple : si MAX gagne, si MIN gagne, en cas de match nul. Rien n'interdit des scores plus fins (matériel aux échecs, différence de pions…), mais le principe reste le même.
2. L'arbre de jeu
Pour raisonner sur toutes les suites possibles, on déplie la partie en un arbre.
L'arbre de jeu associé à une position de départ est l'arbre dont :
- chaque nœud représente une position du jeu ;
- chaque arête représente un coup légal menant à la position fille ;
- la racine est la position à évaluer (celle où l'on doit choisir) ;
- les feuilles sont les positions terminales (victoire, défaite ou nul), auxquelles on attribue directement un score.
Les niveaux alternent le joueur au trait : si la racine est à MAX, ses filles sont à MIN, leurs filles à MAX, et ainsi de suite.
3. La valeur minimax d'une position
Le cœur de l'algorithme tient en une seule définition récursive.
La valeur minimax d'une position est définie par :
- si est terminale : est le score de ;
- sinon, si c'est à MAX de jouer : ;
- sinon, si c'est à MIN de jouer : .
Elle représente le score final que MAX peut garantir depuis , lorsque les deux joueurs jouent de façon optimale.
Le coup optimal à la racine (à MAX) est celui qui mène à la fille dont la valeur est maximale : c'est la fille qui « réalise » le max. On obtient donc à la fois la valeur de la position et le coup à jouer.
4. Le code récursif
On représente ici un arbre de jeu par des listes imbriquées : une feuille est un entier (son score), un nœud interne est la liste de ses filles. Le booléen est_max indique si c'est à MAX de jouer dans la position courante.
def minimax(position, est_max):
if isinstance(position, int): # position terminale : c'est un score
return position
if est_max: # c'est a MAX de jouer
return max(minimax(f, False) for f in position)
else: # c'est a MIN de jouer
return min(minimax(f, True) for f in position)def minimax(position, est_max):La fonction prend la position à évaluer et un booléen : True si MAX joue, False si MIN joue. Elle renvoie la valeur minimax de la position.if isinstance(position, int):Cas de base de la récursion : si la position est un entier, c'est une feuille (position terminale). Le test isinstance distingue une feuille (int) d'un nœud interne (list).return positionOn renvoie directement le score de la feuille. Sans ce cas d'arrêt, la récursion ne se terminerait jamais.if est_max:Cas récursif, position non terminale. Si c'est à MAX de jouer, on veut la meilleure valeur possible pour MAX.return max(minimax(f, False) for f in position)Pour chaque fille f, on calcule sa valeur minimax en passant la main à l'adversaire (est_max=False : ce sera à MIN de jouer plus bas), puis on garde le maximum. Le for f in position parcourt les filles (la liste).else:C'est à MIN de jouer.return min(minimax(f, True) for f in position)Même parcours des filles, mais on repasse la main à MAX (est_max=True) et on garde cette fois le minimum. C'est l'unique différence avec la branche MAX : min au lieu de max, et le booléen inversé.est_max est inversé : les deux joueurs jouent à tour de rôle, donc le joueur au trait alterne à chaque descente d'un niveau. C'est cette alternance qui produit la succession min, max, min, max… en remontant.+1 pour MAX, -1 pour MIN. On passe alors -joueur à l'appel récursif (inversion) et on choisit max si joueur == +1, min sinon. C'est exactement la même logique ; seul le nom du paramètre change.Table de trace sur un petit arbre
Prenons l'arbre binaire de profondeur 3 ci-dessous, racine à MAX. Les feuilles portent les scores. On note les nœuds par leur chemin (L = fille gauche, R = fille droite) :
# feuille = int, noeud interne = liste des filles
arbre = [
[ [3, 5], [6, 9] ], # L (MIN) : filles LL et LR, toutes deux MAX
[ [1, 2], [0, -1] ], # R (MIN) : filles RL et RR, toutes deux MAX
]
print(minimax(arbre, True)) # racine a MAXarbre = [ ... ]La racine (à MAX, profondeur 0) a deux filles L et R, chacune à MIN. Chaque fille MIN a elle-même deux filles à MAX, dont les filles sont les feuilles-scores.[ [3, 5], [6, 9] ]La fille gauche L (MIN). Sa fille LL est le nœud MAX [3, 5], sa fille LR le nœud MAX [6, 9].[ [1, 2], [0, -1] ]La fille droite R (MIN), avec RL = [1, 2] et RR = [0, -1].print(minimax(arbre, True))On lance l'évaluation avec est_max=True car c'est à MAX de jouer à la racine. La valeur affichée est 5.On évalue de bas en haut. Les nœuds MAX de profondeur 2 prennent le max de leurs deux feuilles ; les nœuds MIN de profondeur 1 prennent le min de leurs deux enfants ; la racine MAX prend le max des deux nœuds MIN.
| Nœud | Profondeur | Joueur | Enfants (valeurs) | Opération | Valeur |
|---|---|---|---|---|---|
| LL | 2 | MAX | 3, 5 | max | 5 |
| LR | 2 | MAX | 6, 9 | max | 9 |
| L | 1 | MIN | 5, 9 | min | 5 |
| RL | 2 | MAX | 1, 2 | max | 2 |
| RR | 2 | MAX | 0, −1 | max | 0 |
| R | 1 | MIN | 2, 0 | min | 0 |
| Racine | 0 | MAX | 5, 0 | max | 5 ✓ |
La valeur de la position est donc . Le coup optimal de MAX à la racine est d'aller vers L (valeur ), pas vers R (valeur ). Remarque parlante : la feuille est la plus grosse de l'arbre, mais MAX ne l'obtiendra jamais, car MIN, au nœud L, choisira l'enfant de valeur plutôt que celui de valeur .
La table de trace, c'est LE geste de l'écrit. Aux concours, on te donne un arbre et on demande la valeur remontée. Nos mentors alumni X · Centrale · Mines t'entraînent à dérouler proprement le tableau min/max sous pression, sans sauter de niveau.
Trouver un mentor →5. Élagage alpha-bêta et complexité
Complexité : exponentielle en la profondeur
Minimax visite chaque nœud de l'arbre. Si le facteur de branchement est (nombre de coups par position) et la profondeur explorée est , l'arbre possède environ feuilles, et le nombre total de nœuds est du même ordre. La complexité en temps est donc :
C'est une croissance exponentielle en la profondeur. Aux échecs, : explorer ne serait-ce que 5 demi-coups représente déjà feuilles. Explorer une partie entière est hors de portée — d'où deux réponses en pratique : limiter la profondeur (avec une heuristique d'évaluation des positions non terminales) et… élaguer.
L'élagage alpha-bêta (à connaître d'existence)
L'élagage alpha-bêta est une optimisation de minimax qui donne exactement la même valeur, mais en évitant d'explorer des branches inutiles. L'idée : pendant le parcours, on retient deux bornes, (le meilleur score déjà garanti à MAX) et (le meilleur déjà garanti à MIN). Dès qu'on découvre qu'une branche ne pourra pas améliorer le choix d'un joueur — parce qu'elle est déjà « battue » par une valeur connue plus haut — on l'abandonne sans finir de l'explorer (une coupure).
Dans le meilleur cas (bon ordre d'exploration des coups), alpha-bêta fait passer la complexité de à environ : on peut explorer deux fois plus profond à coût égal. Le programme n'est pas exigible en détail ; retiens son existence, son rôle (accélérer sans changer le résultat) et son principe (couper les branches condamnées).
6. Exercices corrigés
La racine est à MAX et a trois filles à MIN, chacune ayant pour enfants des feuilles :
A = (8, 3, 5) · B = (6, 6, 6) · C = (9, 1, 4). Calcule la valeur minimax de la racine et indique le coup optimal.
Voir la correction détaillée
On évalue arbre = [[[2, 7], [8, 1]], [[9, 3], [4, 4]]] avec la fonction minimax de la fiche, mais cette fois la racine est à MIN (on appelle donc minimax(arbre, False)). Donne la valeur remontée et détaille les niveaux.
Voir la correction détaillée
La fonction de la fiche ne renvoie que la valeur. Écris meilleur_coup(position) qui, pour une position à MAX représentée comme une liste de filles, renvoie l'indice de la fille de valeur minimax maximale (le coup à jouer). Teste-la sur l'arbre de trace de la section 4.
Voir la correction détaillée
minimax(f, False) (après le coup de MAX, c'est à MIN de jouer), puis renvoyer l'indice du maximum.def meilleur_coup(position):
# position est a MAX et non terminale : liste de filles
valeurs = [minimax(f, False) for f in position]
meilleure = max(valeurs)
return valeurs.index(meilleure) # indice de la 1re fille optimalearbre de la section 4 : les deux filles L et R valent et . valeurs = [5, 0], le max est , index(5) = 0 : le coup optimal est la fille d'indice 0 (L), cohérent avec la valeur de racine .[minimax(f, False) for f in position] évalue chaque coup possible ; max(valeurs) est la valeur minimax de la position (à MAX) ; valeurs.index(...) retrouve quel coup l'atteint.Récap final — Ce qu'il faut absolument retenir
Minimax = récursivité sur l'arbre de jeu, avec alternance max (MAX) / min (MIN) en remontant les scores des feuilles. Vérifie chaque point avant l'épreuve.
- Sais-tu citer les quatre propriétés du cadre (deux joueurs, somme nulle, information complète, sans hasard) et donner un contre-exemple ?
- Sais-tu construire l'arbre de jeu : nœud = position, arête = coup, feuille = position terminale scorée ?
- Sais-tu énoncer la définition récursive de la valeur minimax (feuille → score ; MAX → max des filles ; MIN → min des filles) ?
- Sais-tu expliquer pourquoi chaque joueur suppose l'adversaire optimal, et pourquoi MAX ne vise pas forcément la plus grosse feuille ?
- Sais-tu écrire la fonction récursive
minimax(position, est_max)avec son cas de base et l'inversion du booléen ? - Sais-tu dérouler une table de trace en remontant les valeurs des feuilles jusqu'à la racine, min et max alternés ?
- Sais-tu que l'élagage alpha-bêta donne la même valeur en explorant moins de nœuds, et qu'il n'est pas exigible dans le détail ?
- Sais-tu estimer la complexité et expliquer pourquoi elle est exponentielle en la profondeur ?