Aller au contenu

Accueil › MP › Option informatique

MP · Option informatique · semaine 1

Retour sur trace

Exploration d'un arbre de choix avec abandon des branches sans issue, huit reines, sudoku. Lien avec la récursivité et avec l'algorithme min-max.

Sommaire

Le cours

Résumé en cinq points

  1. Le retour sur trace construit une solution choix après choix et revient sur le dernier choix dès qu'une solution partielle ne peut plus aboutir.
  2. L'exploration est un parcours en profondeur de l'arbre des solutions partielles, jamais construit en mémoire. La pile d'appels contient le chemin courant.
  3. L'élagage coupe les sous-arbres des solutions partielles incompatibles. Il ne change pas le résultat et réduit énormément le nombre de nœuds visités, sans changer en général le caractère exponentiel du pire cas.
  4. Avec une structure immuable, le retour en arrière est automatique. Avec une structure mutable, il faut défaire explicitement chaque modification.
  5. Huit reines, sudoku et sous-ensemble de somme donnée suivent le même schéma récursif, que l'on retrouve dans l'algorithme min-max.
Pourquoi ce chapitre

Beaucoup de problèmes n'ont pas d'algorithme direct connu :

  • placer des reines sur un échiquier sans qu'elles se menacent ;
  • remplir une grille de sudoku ;
  • choisir des objets dont les poids font une somme donnée.

La seule méthode générale est d'essayer. Le retour sur trace organise cet essai intelligemment. On construit une solution pas à pas, et dès qu'une solution partielle ne peut plus aboutir, on revient sur le dernier choix pour en essayer un autre, sans explorer tout ce qui en découlait.

Ce chapitre apprend à voir cette exploration comme le parcours d'un arbre, à l'écrire proprement en récursif, et à estimer ce que coûte l'élagage. Aux concours, c'est un thème fréquent de problème, souvent avec une question finale sur le nombre de nœuds explorés.

1Principe du retour sur trace

1.1Solutions partielles et choix successifs

Définition 1.1Solution partielle

On décrit une solution comme une suite de choix x1,x2,…,xnx_1, x_2, \dots, x_n, chacun pris dans un ensemble fini. Une solution partielle est un début de suite x1,…,xkx_1, \dots, x_k, avec k<nk < n.

Exemple 1.2Choix pour deux problèmes

  • Pour les huit reines, xix_i est la colonne de la reine de la ligne ii.
  • Pour un sudoku, xix_i est le chiffre placé dans la ii-ème case vide.

MéthodeRetour sur trace (backtracking)

  1. Partir de la solution partielle vide.
  2. À chaque étape, essayer successivement les valeurs possibles du choix suivant.
  3. Si la solution partielle prolongée respecte les contraintes, continuer récursivement.
  4. Si elle les viole, ou si toutes les suites ont échoué, revenir en arrière, défaire le dernier choix et essayer la valeur suivante.
  5. S'arrêter dès qu'une solution complète est trouvée, ou continuer pour les énumérer toutes.

1.2L'arbre des solutions partielles

L'exploration se représente par un arbre :

  • la racine est la solution partielle vide ;
  • les fils d'un nœud sont ses prolongements d'un choix ;
  • les feuilles de profondeur nn sont les candidats complets.

L'ensemble de cet arbre n'est jamais construit en mémoire. Il est parcouru en profondeur par les appels récursifs, et la pile d'exécution contient à chaque instant le chemin de la racine au nœud courant.

Revenir sur trace, c'est remonter d'un niveau dans cet arbre. L'arbre sert à raisonner sur le flot de contrôle : dans quel ordre les choix sont essayés, quand on remonte, ce qui reste à explorer.

1.3Élagage

Définition 1.3Élagage

L'élagage consiste, dès qu'une solution partielle viole une contrainte, à n'explorer aucun de ses descendants.

Une branche coupée près de la racine supprime un sous-arbre entier. Tout l'art du retour sur trace est de détecter l'échec le plus tôt possible, avec un test de compatibilité qui ne regarde que le dernier choix par rapport aux précédents.

Propriété 1.4Invariant du parcours

Toute solution partielle présente sur la pile respecte les contraintes.

Il suffit donc de vérifier le dernier choix, car les précédents ont déjà été vérifiés entre eux quand on les a faits. C'est ce qui rend le test local et rapide.

Attention

L'élagage ne change pas le résultat, seulement le nombre de nœuds visités. Il ne change pas non plus, en général, l'ordre de grandeur du pire cas, qui reste exponentiel. Mais sur les instances réelles, il fait passer de milliards de candidats à quelques milliers de nœuds.

2Problème des huit reines

2.1Énoncé et modélisation

Il s'agit de placer huit reines sur un échiquier de 8×88 \times 8 cases de sorte qu'aucune ne menace une autre, c'est-à-dire que deux reines ne partagent ni ligne, ni colonne, ni diagonale. On généralise à nn reines sur un échiquier n×nn \times n.

Première modélisation, qui élague déjà. Comme deux reines ne peuvent pas être sur la même ligne, il y en a exactement une par ligne. On cherche donc, pour chaque ligne ii, sa colonne cic_i. Les contraintes deviennent :

  • ci≠cjc_i \neq c_j pour les colonnes ;
  • ∣ci−cj∣≠∣i−j∣\abs{c_i - c_j} \neq \abs{i - j} pour les diagonales.

Il y a 888^8, environ 16,8 millions, suites de colonnes candidates, et 8!8!, soit 40 32040\,320, si l'on impose des colonnes distinctes. Le problème compte 92 solutions.

2.2Programme récursif

On garde les colonnes déjà placées dans une liste, la plus récente en tête. Le test de compatibilité compare la nouvelle colonne cc à chaque colonne c′c' placée dd lignes plus haut.

let compatible placees c =
  let rec aux l d = match l with
    | [] -> true
    | c' :: q -> c' <> c && abs (c' - c) <> d && aux q (d + 1)
  in aux placees 1

let rec compter n placees k =
  if k = n then 1
  else begin
    let total = ref 0 in
    for c = 0 to n - 1 do
      if compatible placees c then
        total := !total + compter n (c :: placees) (k + 1)
    done;
    !total
  end

L'appel compter 8 [] 0 renvoie 92. Pour obtenir les solutions elles-mêmes et non leur nombre, on remplace le compteur par une liste accumulée, et le cas k = n renvoie la liste [placees], dont on concatène les résultats. Le coût mémoire devient alors proportionnel au nombre de solutions.

Le retour en arrière est ici implicite. Comme la liste des colonnes est immuable, revenir d'un appel récursif retrouve automatiquement l'ancienne liste.

À retenir

Avec une structure immuable, il n'y a rien à défaire au retour d'un appel.

2.3Coût

Le test de compatibilité au niveau kk coûte de l'ordre de kk. Le nombre de nœuds visités est très inférieur à 888^8 grâce à l'élagage.

Exemple 2.1Nombre de nœuds par niveau

  • Pour n=4n = 4, l'arbre des solutions partielles compatibles compte, niveau par niveau, 1, 4, 6, 4 et 2 nœuds, soit 17 nœuds. Les 2 feuilles du dernier niveau sont les deux solutions.
  • Pour n=8n = 8, les niveaux comptent 1, 8, 42, 140, 344, 568, 550, 312 et 92 nœuds, soit 2 0572\,057 nœuds au total, à comparer aux quelque 16,8 millions de suites candidates.

Le nombre de nœuds croît cependant de façon exponentielle avec nn, et le problème des nn reines reste coûteux à énumérer pour nn de l'ordre de trente.

3Résolution d'un sudoku

3.1Modélisation

Une grille de sudoku est un tableau de 9×99 \times 9 cases, certaines déjà remplies. Il faut remplir les cases vides avec des chiffres de 1 à 9, de sorte que chaque ligne, chaque colonne et chacun des neuf carrés de 3×33 \times 3 contienne chaque chiffre une fois.

On représente la grille par une matrice mutable, int array array, avec 0 pour une case vide. Un choix consiste à placer un chiffre dans la première case vide rencontrée.

3.2Retour sur trace avec une structure mutable

MéthodeRésoudre un sudoku

  1. Parcourir les cases dans l'ordre. Si la case courante est remplie, passer à la suivante.
  2. Sinon, essayer chaque chiffre compatible avec sa ligne, sa colonne et son carré.
  3. Si la suite de la résolution réussit, s'arrêter avec succès.
  4. Sinon, remettre la case à 0 avant d'essayer le chiffre suivant.
  5. Si aucun chiffre ne convient, renvoyer un échec au niveau précédent.
let rec resoudre g k =
  if k = 81 then true
  else
    let i = k / 9 and j = k mod 9 in
    if g.(i).(j) <> 0 then resoudre g (k + 1)
    else begin
      let trouve = ref false and v = ref 1 in
      while not !trouve && !v <= 9 do
        if possible g i j !v then begin
          g.(i).(j) <- !v;
          if resoudre g (k + 1) then trouve := true
          else g.(i).(j) <- 0
        end;
        incr v
      done;
      !trouve
    end

Le test possible g i j v vérifie que v n'apparaît ni dans la ligne i, ni dans la colonne j, ni dans le carré de 3×33 \times 3 qui contient la case. Le coin supérieur gauche de ce carré a pour coordonnées 3×(i/3)3 \times (i / 3) et 3×(j/3)3 \times (j / 3) en division entière. Le test parcourt 27 cases au plus, coût constant pour une grille de taille fixe.

let possible g i j v =
  let ok = ref true in
  for k = 0 to 8 do
    if g.(i).(k) = v || g.(k).(j) = v then ok := false
  done;
  let i0 = 3 * (i / 3) and j0 = 3 * (j / 3) in
  for a = i0 to i0 + 2 do
    for b = j0 to j0 + 2 do
      if g.(a).(b) = v then ok := false
    done
  done;
  !ok

Avec une structure mutable, le retour sur trace doit défaire explicitement les modifications, ici remettre la case à 0.

Attention

Oublier cette remise à zéro est l'erreur classique : la grille garde des chiffres d'une branche abandonnée et les tests suivants deviennent faux.

3.3Améliorer l'élagage

L'ordre des choix influe beaucoup sur le coût. Une heuristique efficace consiste à choisir d'abord la case vide qui a le moins de chiffres possibles :

  • une case qui n'en a qu'un ne crée pas de branchement ;
  • une case qui n'en a aucun détecte l'échec immédiatement.

On peut aussi maintenir pour chaque ligne, colonne et carré l'ensemble des chiffres déjà utilisés, pour tester la compatibilité en temps constant.

4Autres exemples et généralités sans théorie

4.1Sous-ensemble de somme donnée

Étant donné une liste d'entiers positifs et une cible ss, existe-t-il une sous-liste de somme ss ? L'arbre des choix est binaire, chaque élément est pris ou non, soit 2n2^n feuilles. Deux règles d'élagage :

  • si la somme partielle dépasse ss, on abandonne la branche, puisque les entiers sont positifs ;
  • si la somme partielle plus la somme de tous les éléments restants n'atteint pas ss, on l'abandonne aussi.

Exemple 4.1Liste 3, 5, 6, 7 et cible 15

On prend 3, puis 5, somme 8, puis 6, somme 14, puis 7 donnerait 21, trop, on revient. Sans 6, on prend 7, somme 15 : solution {3,5,7}\{3, 5, 7\}.

Si l'on veut toutes les solutions, on continue l'exploration jusqu'au bout, et l'on constate que c'est la seule. Les autres sous-ensembles restent en dessous de 15, comme {6,7}\{6, 7\} ou {3,5,6}\{3, 5, 6\}, ou le dépassent, comme {3,6,7}\{3, 6, 7\} ou {5,6,7}\{5, 6, 7\}.

Trier la liste par ordre décroissant avant de commencer rend la première règle d'élagage plus efficace, car les grandes valeurs font dépasser la cible plus tôt.

4.2Énumérer toutes les solutions ou s'arrêter à la première

Les deux variantes s'écrivent de la même manière :

  • pour énumérer, on compte ou on accumule chaque solution complète et on continue ;
  • pour s'arrêter à la première, on renvoie un booléen ou une option et on interrompt la boucle dès un succès, comme dans le sudoku.

En OCaml, une exception peut aussi servir à remonter d'un coup toute la pile au premier succès.

4.3Schéma commun

Tous les exemples suivent le même schéma, que le programme demande de dégager sans le formaliser. Une fonction récursive reçoit la solution partielle :

  • si elle est complète, on la traite ;
  • sinon, pour chaque prolongement possible qui respecte les contraintes, on prolonge et on appelle récursivement, puis on défait le prolongement si la structure est mutable.

Ce schéma repose sur deux preuves simples.

Propriété 4.2Terminaison

La terminaison vient de ce que l'arbre est fini : chaque appel augmente la longueur de la solution partielle, bornée par nn, et chaque nœud a un nombre fini de fils.

Propriété 4.3Correction

Toute solution complète est atteinte, puisqu'on n'élague que des solutions partielles qui ne peuvent pas être prolongées en solutions. Toute feuille atteinte respecte les contraintes, par l'invariant du parcours.

5Lien avec l'algorithme min-max

L'algorithme min-max du tronc commun explore lui aussi un arbre, l'arbre des coups d'un jeu à deux joueurs, en profondeur et par appels récursifs. À chaque nœud, il essaie tous les coups, évalue récursivement la position obtenue, puis revient pour essayer le coup suivant.

C'est la même mécanique que le retour sur trace, à ceci près que le résultat d'un nœud n'est pas un succès ou un échec mais le maximum ou le minimum des valeurs de ses fils, selon le joueur qui a la main. Le programme précise que la notion de retour sur trace est réinvestie à cette occasion.

Remarque

L'élagage alpha-bêta, qui prolongerait l'analogie, n'est pas au programme du tronc commun.

Méthodes

MéthodeRéflexes du chapitre

  1. Décrire d'abord une solution comme une suite de choix, et le test de compatibilité comme une condition sur le dernier choix par rapport aux précédents.
  2. Choisir une modélisation qui intègre déjà une partie des contraintes, une reine par ligne par exemple. C'est l'élagage le plus rentable.
  3. Avec une structure immuable, le retour en arrière est gratuit. Avec une structure mutable, défaire chaque modification au retour de l'appel.
  4. Distinguer clairement les deux variantes, trouver une solution ou les énumérer toutes, et adapter la valeur de retour.
  5. Pour estimer le coût, majorer le nombre de nœuds de l'arbre par le produit des nombres de choix à chaque niveau, puis dire ce que l'élagage retire.

Ce qu'il faut savoir refaire en khôlle

  1. Expliquer le principe du retour sur trace et dessiner les premiers niveaux de l'arbre des solutions partielles pour le problème des quatre reines.
  2. Écrire en OCaml le comptage des solutions du problème des nn reines, avec le test de compatibilité sur les colonnes et les diagonales.
  3. Écrire un solveur de sudoku par retour sur trace sur une matrice mutable, en expliquant la remise à zéro.
  4. Résoudre par retour sur trace le problème du sous-ensemble de somme donnée, avec deux règles d'élagage.
  5. Majorer le nombre de nœuds explorés et expliquer pourquoi le pire cas reste exponentiel.
  6. Comparer le retour sur trace à l'algorithme min-max.

Méthodes et exercices

Les résultats à connaître

Le programme présente le retour sur trace « sans théorie générale ». Les résultats ci-dessous ne sont donc pas des théorèmes de cours. Ce sont des propriétés à savoir établir sur chaque exemple.

  • Terminaison. Supposons que chaque appel prolonge la solution partielle d'un choix, que la longueur soit bornée par n et que chaque choix soit pris dans un ensemble fini. Alors l'exploration termine. Exigible avec démonstration sur l'exemple traité.
  • Validité. Supposons que la racine respecte les contraintes et que le test du dernier choix soit exact. Alors toute solution partielle présente sur la pile respecte toutes les contraintes, et toute solution renvoyée est correcte. Exigible avec démonstration.
  • Complétude. Si l'on n'élague que des nœuds dont aucun descendant n'est une solution, toute solution est atteinte. Exigible avec démonstration.
  • Pile. Le nombre d'appels imbriqués vaut au plus n + 1, sans compter les fonctions auxiliaires. Exigible.
  • Majoration du nombre de nœuds. Si le niveau i offre au plus mᵢ choix, l'arbre a au plus 1 + m₁ + m₁m₂ + … + m₁m₂⋯mₙ nœuds. Exigible.
  • n reines. Il y a 1 solution pour n = 1, aucune pour n = 2 et n = 3, et 2 pour n = 4, ce qui se vérifie à la main. La valeur 92 pour n = 8 est un résultat de calcul. L'existence d'une solution pour tout n ≥ 4 est admise et hors programme.
  • n reines avec des colonnes distinctes. L'arbre a au plus Σ n!/(n − k)! nœuds, pour k de 0 à n, et cette somme est au plus e·n!. Exigible en exercice.
  • Sous-ensemble de somme donnée. L'élagage « la somme partielle dépasse la cible » est correct si les entiers sont positifs ou nuls. Exigible.
  • Min-max. C'est le même parcours en profondeur, mais la valeur d'un nœud est le maximum ou le minimum de celles de ses fils. Exigible au tronc commun.
  • Hors programme : élagage alpha-bêta, propagation de contraintes, algorithme des liens dansants, et NP-complétude dans ce chapitre.

Clés de démonstration

Terminaison. On prend pour mesure n − k, où k est la longueur de la solution partielle. Chaque appel récursif augmente k de 1, et chaque boucle sur les choix est finie. Astuce : la mesure est un entier naturel qui décroît strictement le long de toute chaîne d'appels.

Validité. On raisonne par récurrence sur la profondeur. La solution vide respecte les contraintes. On ne descend que si le nouveau choix est compatible avec chacun des précédents. Or, dans les exemples du programme, les contraintes portent sur des paires de choix. Astuce : ce sont des contraintes binaires, donc tester le dernier choix contre les autres suffit.

Complétude. Soit s une solution complète. Chacun de ses préfixes respecte les contraintes, donc aucun n'est élagué. Par récurrence sur la longueur du préfixe, la boucle, qui essaie toutes les valeurs, finit par descendre jusqu'à s. Astuce : un préfixe de solution est toujours compatible. Dans la variante « première solution », on en déduit que la fonction réussit dès qu'une solution existe.

Nombre de nœuds. Le niveau k contient au plus m₁⋯mk nœuds. Pour les reines à colonnes distinctes, cela donne n!/(n − k)!. Avec j = n − k, la somme vaut n!·Σ 1/j!, pour j de 0 à n, qui est au plus e·n!. Astuce : changer d'indice pour reconnaître la série de l'exponentielle.

Coût total. On multiplie le nombre de nœuds par le travail fait en un nœud, qui est le nombre de choix essayés fois le coût du test. Astuce : un test en temps constant fait gagner un facteur n sur tout l'arbre.

Exemples et contre-exemples à connaître

  1. Deux et trois reines. L'arbre est parcouru en entier et renvoie 0. Pour prouver qu'il n'y a pas de solution, l'exploration doit être complète.
  2. Quatre reines. Les deux solutions sont, colonnes numérotées de 0, (1, 3, 0, 2) et (2, 0, 3, 1). Elles sont symétriques l'une de l'autre par rapport à l'axe vertical.
  3. Huit reines. Il y a 92 solutions, et 12 seulement à symétrie de l'échiquier près. Les symétries permettent un élagage supplémentaire.
  4. Somme donnée avec un entier négatif. Pour la liste 5, −3 et la cible 2, la somme partielle 5 dépasse la cible, mais 5 − 3 = 2. Une règle d'élagage doit être une vraie condition d'échec.
  5. Permutations. On passe de nⁿ feuilles à n! feuilles en refusant une valeur déjà utilisée. Une bonne modélisation vaut un élagage.
  6. Mots bien parenthésés. L'élagage est parfait : chaque nœud mène à une solution, et il n'y a aucun échec (exercice 6).
  7. Sudoku. La borne brute est 9⁸¹, environ 2·10⁷⁷ remplissages. Il n'existe pourtant qu'environ 6,7·10²¹ grilles complètes valides. Sur une grille réelle, le nombre de nœuds visités est infime devant ces nombres.
  8. Min-max sur un arbre de profondeur 2. La racine maximise et a deux fils minimisants, de feuilles (3, 5) et (2, 9). Leurs valeurs sont 3 et 2, la racine vaut 3. Min-max sans alpha-bêta visite toutes les feuilles, c'est un retour sur trace sans élagage.
  9. Un triangle n'est pas coloriable avec 2 couleurs. L'échec apparaît au troisième sommet, puis on remonte jusqu'à la racine.

Techniques du chapitre

Technique 1 : fixer les choix et leur domaine. On veut les mots binaires de longueur n sans deux 1 consécutifs. Le choix xᵢ vaut 0 ou 1, et le test refuse xᵢ = 1 si x₍ᵢ₋₁₎ = 1. Pour n = 3 on obtient 000, 001, 010, 100 et 101, soit 5 mots.

Technique 2 : choisir le type de retour. Pour compter, on renvoie un entier. Pour tester l'existence, on renvoie un booléen. Pour une solution, on renvoie une option. Pour toutes les solutions, on renvoie une liste. Une exception remonte d'un coup au premier succès (exercice 8).

Technique 3 : défaire dans une structure mutable. On marque, on appelle, on démarque : utilise.(v) <- true, puis aux (k + 1), puis utilise.(v) <- false. Le démarquage se place juste après l'appel, dans le même bloc que le marquage.

Technique 4 : rendre le test incrémental. Pour les reines, on marque les colonnes, les diagonales où i + c est constant et celles où i − c est constant. Le test coûte alors O(1) au lieu de O(k). L'indice i − c + n − 1 évite les indices négatifs.

Technique 5 : élaguer par une borne. Pour la liste 2, 3, 4 et la cible 10, la somme totale 9 est inférieure à 10. On élague dès la racine. Seul ce nœud est visité, aucun de ses descendants n'est exploré.

Technique 6 : compter les nœuds niveau par niveau. Pour les permutations de 3 éléments, on trouve 1 + 3 + 6 + 6 = 16 nœuds. Les feuilles ne sont qu'une partie du coût.

Technique 7 : essayer d'abord le choix le plus contraint. Au sudoku, une case qui n'a qu'un candidat ne crée pas de branchement. Il vaut mieux la remplir avant une case qui en a quatre.

Exercices classiques corrigés

Choisis ton niveau. Difficulté indicative ; les exercices qui se suivent restent dans l'ordre.

Exercice 1. Énumérer les permutations

Classiques de khôlle et concours · Khôlle

Renvoyer la liste des permutations de {0, …, n − 1} par retour sur trace, puis compter les nœuds de l'arbre.

Technique marquage mutable et démarquage.

Voir le corrigé
  1. Le choix k est la k-ième valeur. Un tableau de booléens marque les valeurs déjà prises, et la permutation se construit dans une liste immuable.
let permutations n =
  let utilise = Array.make n false and res = ref [] in
  let rec aux k acc =
    if k = n then res := acc :: !res
    else
      for v = 0 to n - 1 do
        if not utilise.(v) then begin
          utilise.(v) <- true;
          aux (k + 1) (v :: acc);
          utilise.(v) <- false
        end
      done in
  aux 0 [];
  !res
  1. La liste acc est construite à l'envers. Comme l'image miroir est une bijection sur les permutations, l'ensemble obtenu est bien le bon.
  2. Le niveau k a n!/(n − k)! nœuds, d'où au plus e·n! nœuds au total.

piège : On mélange deux mécanismes. Le tableau doit être démarqué, mais la liste acc n'a rien à défaire.

Exercice 2. Trace des quatre reines

Classiques de khôlle et concours · Khôlle

On essaie les colonnes 0 à 3 dans l'ordre. Donner la première solution trouvée et le nombre de nœuds compatibles visités jusqu'à elle.

Technique suivre le parcours en profondeur.

Voir le corrigé
  1. Avec la reine de la ligne 0 en colonne 0, la ligne 1 accepte 2 ou 3. Après 2, la ligne 2 n'a plus aucune colonne. Après 3, la ligne 2 accepte 1, puis la ligne 3 est bloquée. On remonte à la racine.
  2. Avec la reine de la ligne 0 en colonne 1, la ligne 1 accepte seulement 3. La ligne 2 accepte alors 0, et la ligne 3 accepte 2. La solution est (1, 3, 0, 2).
  3. Les nœuds visités sont la racine, (0), (0, 2), (0, 3), (0, 3, 1), (1), (1, 3), (1, 3, 0) et (1, 3, 0, 2), soit 9 des 17 nœuds de l'arbre.

piège : On compte parfois les candidats testés au lieu des nœuds compatibles. Il faut préciser ce que l'on compte.

Exercice 3. Sous-ensemble de somme donnée

Classiques de khôlle et concours · Khôlle

Soit une liste d'entiers positifs ou nuls. Renvoyer une sous-liste de somme s si elle existe, avec deux règles d'élagage.

Technique arbre binaire et élagage par borne.

Voir le corrigé
  1. On transmet la somme reste des éléments non encore examinés et la somme s encore à atteindre.
let rec cherche l reste s =
  if s = 0 then Some []
  else if s < 0 || reste < s then None
  else match l with
    | [] -> None
    | x :: q ->
        match cherche q (reste - x) (s - x) with
        | Some sol -> Some (x :: sol)
        | None -> cherche q (reste - x) s
  1. On appelle cherche l t s, où t est la somme de l. Si s < 0, aucun ajout d'entier positif ou nul ne peut ramener à 0. Si reste < s, même tout prendre ne suffit pas.
  2. L'arbre a au plus 2ⁿ⁺¹ − 1 nœuds, et le pire cas reste exponentiel.

piège : Dans la branche « sans x », on oublie de retirer x de reste. La borne devient alors fausse par excès, et elle élague moins.

Exercice 4. Coloration de graphe

Classiques de khôlle et concours · Khôlle

Un graphe à n sommets est donné par ses listes d'adjacence, de type int list array. Décider s'il est coloriable avec k couleurs, deux voisins recevant des couleurs différentes.

Technique structure mutable et arrêt au premier succès.

Voir le corrigé
  1. Le choix s est la couleur du sommet s. Un sommet non colorié porte −1, donc seuls les voisins déjà coloriés gênent.
let rec libre coul c l = match l with
  | [] -> true
  | v :: q -> coul.(v) <> c && libre coul c q
let colorable g k =
  let n = Array.length g in let coul = Array.make n (-1) in
  let rec aux s =
    if s = n then true
    else begin
      let ok = ref false in
      for c = 0 to k - 1 do
        if not !ok && libre coul c g.(s) then begin
          coul.(s) <- c;
          if aux (s + 1) then ok := true else coul.(s) <- -1
        end
      done;
      !ok
    end in
  aux 0
  1. Après un succès, on ne remet pas la case à −1, et la coloration trouvée reste dans coul.
  2. Imposer la couleur 0 au sommet 0 divise l'arbre par k, puisque les couleurs sont interchangeables.

piège : On démarque aussi après un succès, ce qui efface la solution.

Exercice 5. Reines avec test en temps constant

Classiques de khôlle et concours · D'après un écrit CCINP MP

Compter les solutions des n reines avec trois tableaux de booléens, puis comparer le coût par nœud avec la version de la fiche.

Technique test incrémental.

Voir le corrigé
  1. Les cases (i, c) d'une même diagonale ont i + c constant, ou i − c constant. Ces indices vont de 0 à 2n − 2, après décalage de n − 1 pour le second.
let reines n =
  let col = Array.make n false in
  let d1 = Array.make (2 * n - 1) false and d2 = Array.make (2 * n - 1) false in
  let rec aux i =
    if i = n then 1 else
    let t = ref 0 in
    for c = 0 to n - 1 do
      if not (col.(c) || d1.(i + c) || d2.(i - c + n - 1)) then begin
        col.(c) <- true; d1.(i + c) <- true; d2.(i - c + n - 1) <- true;
        t := !t + aux (i + 1);
        col.(c) <- false; d1.(i + c) <- false; d2.(i - c + n - 1) <- false
      end
    done;
    !t in
  aux 0
  1. Invariant : les tableaux marquent exactement les colonnes et les diagonales des reines placées sur les lignes 0 à i − 1.
  2. Un nœud coûte O(n), contre O(n²) avec la fonction compatible de la fiche. L'arbre est le même.

piège : On indexe par i − c sans décalage, ce qui produit un indice négatif et lève une exception.

Exercice 6. Mots bien parenthésés

Classiques de khôlle et concours · Khôlle

Compter les mots de n parenthèses ouvrantes et n fermantes dont aucun préfixe n'a plus de fermantes que d'ouvrantes. Montrer que l'exploration ne rencontre aucun échec.

Technique élagage exact.

Voir le corrigé
  1. On suit le nombre o d'ouvrantes et le nombre f de fermantes. On peut ouvrir si o < n, et fermer si f < o.
let rec dyck n o f =
  if f = n then 1
  else (if o < n then dyck n (o + 1) f else 0)
     + (if f < o then dyck n o (f + 1) else 0)
  1. Tout nœud vérifiant f ≤ o ≤ n se complète en ouvrant jusqu'à n puis en fermant. Aucune branche n'est donc stérile.
  2. Chaque nœud est préfixe d'une solution, d'où au plus (2n + 1) fois le nombre de solutions de nœuds. On obtient 1, 2, 5 et 14 pour n de 1 à 4.

piège : On autorise une fermante dès que f < n au lieu de f < o. Les préfixes ne sont plus contrôlés, et le programme renvoie le coefficient binomial C(2n, n), soit 2, 6, 20 et 70 pour n de 1 à 4, au lieu de 1, 2, 5 et 14. En revanche, s'arrêter sur o = n donne encore le bon compte, car la fin du mot est alors forcée.

Exercice 7. Chiffrer l'arbre des reines

Approfondissement · Type écrit Mines

Majorer le nombre de nœuds de l'arbre des n reines à colonnes distinctes, et faire le calcul pour n = 8.

Technique produit des choix et changement d'indice.

Voir le corrigé
  1. Au niveau k, les k colonnes sont distinctes, d'où au plus n(n − 1)⋯(n − k + 1) = n!/(n − k)! nœuds.
  2. Avec j = n − k, le total est n!·Σ 1/j!, pour j de 0 à n, qui est au plus e·n!.
  3. Pour n = 8, on trouve 1 + 8 + 56 + 336 + 1 680 + 6 720 + 20 160 + 40 320 + 40 320 = 109 601. Les diagonales réduisent ce nombre à 2 057.
  4. Avec la version de l'exercice 5, le coût est O(n·n!) dans cette majoration.

piège : On confond la borne 8⁸ des suites quelconques avec celle des permutations. Elles répondent à deux modélisations différentes.

Exercice 8. Première solution par exception

Classiques de khôlle et concours · Khôlle

Renvoyer une solution des n reines sous forme d'option, en interrompant l'exploration au premier succès.

Technique exception pour vider la pile.

Voir le corrigé
  1. La fonction auxiliaire renvoie unit. Une solution complète lève l'exception, rattrapée une seule fois au sommet.
exception Trouve of int list

let premiere n =
  let rec aux placees k =
    if k = n then raise (Trouve placees)
    else
      for c = 0 to n - 1 do
        if compatible placees c then aux (c :: placees) (k + 1)
      done in
  try aux [] 0; None with Trouve l -> Some (List.rev l)
  1. Si l'arbre est épuisé sans exception, on renvoie None, par exemple pour n = 2 ou 3.
  2. La liste renversée donne les colonnes des lignes 0 à n − 1.

piège : On place le try à l'intérieur de aux. Chaque niveau rattrape alors l'exception, et l'exploration continue.

Exercice 9. Un jeu de Nim et min-max

Approfondissement · Type oral Centrale

Une pile contient n allumettes. Chaque joueur retire 1, 2 ou 3 allumettes, et celui qui prend la dernière gagne. Écrire gagnante n, qui dit si le joueur qui a la main peut gagner. Caractériser les positions perdantes.

Technique retour sur trace sur l'arbre d'un jeu.

Voir le corrigé
  1. Une position est gagnante si l'un des coups mène à une position perdante pour l'adversaire. Avec 0 allumette, le joueur a déjà perdu.
let rec gagnante n =
  let res = ref false in
  for k = 1 to min 3 n do
    if not !res && not (gagnante (n - k)) then res := true
  done;
  !res
  1. Par récurrence forte, n est perdante si et seulement si n est multiple de 4. Si 4 divise n, tout coup mène à un non-multiple. Sinon, on retire le reste de n modulo 4.
  2. Avec +1 pour un gain et −1 pour une perte, c'est min-max. L'arrêt dès un coup gagnant est un élagage que min-max ne fait pas.
  3. Le nombre d'appels croît exponentiellement. La mémoïsation, vue en tronc commun, le rend linéaire.

piège : On donne la valeur vrai à gagnante 0. Cela inverse toute la caractérisation.

Erreurs fréquentes

  1. Oublier de défaire une modification d'une structure mutable, ou la défaire aussi après un succès. Dans le premier cas les tests suivants sont faux, dans le second la solution est effacée.
  2. Élaguer avec une condition qui n'est pas une vraie condition d'échec, par exemple « la somme dépasse la cible » quand des entiers sont négatifs. Des solutions sont alors perdues sans aucun message.
  3. Retester toute la solution partielle à chaque nœud au lieu du seul dernier choix. Le coût de chaque nœud est alors multiplié par la profondeur.
  4. Confondre les deux variantes : s'arrêter au premier succès alors qu'on doit compter, ou continuer après un succès alors qu'on doit renvoyer une solution.
  5. Conclure à un coût polynomial parce que l'exemple va vite, ou confondre le nombre de nœuds, le nombre de feuilles et la hauteur de pile. Seule la hauteur de pile est linéaire.

Fiche de cours rédigée à partir du programme officiel de l'option informatique de la filière MPSI-MP (2021), programme de deuxième année, partie 2.1 « Applications des arbres », point « Retour sur trace ». Le programme aborde les arbres comme support de pensée pour raisonner sur le flot de contrôle d'une stratégie algorithmique. Il demande de présenter le retour sur trace à travers quelques exemples, sans théorie générale, par exemple le problème des huit reines ou la résolution d'un sudoku, et précise que la notion est réinvestie avec l'algorithme min-max du tronc commun. Les exemples sont écrits en OCaml. La file de priorité, autre point de la partie 2.1, fait l'objet du chapitre suivant.