TP — Tour de cavaliers¶
Travaux pratiques · MP2I · 2021-2022 · OCaml
Cours : Recherche par retour sur trace
Attention ce qu'on va faire ici n'est pas facile. Je vous demande d'abord de reprendre les deux exemples des huit reines puis du Sudoku du chapitre sur la force brute et de faire marcher le code ici dans un autre notebook. C'est en comprenant bien les exemples du cours qu'on peut construire la suite dessus.
On rappelle qu'un cavalier, aux échecs, se déplace en L : une case dans une direction, horizontale ou verticale, deux cases dans l'autre.
Dans ce TP on va résoudre le problème du cavalier d'Euler : étant donné un échiquier rectangulaire \(n \times m\) et une position initiale d'un cavalier en \((x_0, y_0)\) :
- existe-t-il une succession de mouvements du cavalier permettant de visiter chaque case de l'échiquier une fois et une seule ? On parle de tour du cavalier.
- existe-t-il un tour du cavalier qui permette avec un mouvement supplémentaire de revenir au point de départ \((x_0, y_0)\) ? On parle de tour fermé du cavalier.
On va représenter un échiquier par le type OCaml :
type echiquier = int option array array
- L'idée est qu'une valeur
Nonesignifie que la case n'a pas été visitée. - Une valeur
Some ksignifie que le cavalier a visité cette case en \(k\)-ème position dans son tour, en commençant à numéroter à 1 pour la case initiale.
Exemple de numérotation finale d'un tour fermé d'un échiquier 6x6 :
1| 6|15|18|35|26|
8|19|36|25|14|17|
5| 2| 7|16|27|34|
20| 9| 4|31|24|13|
3|30|11|22|33|28|
10|21|32|29|12|23|
On va faire le choix d'énumérer par colonnes, ce qui permet d'écrire
e.(x).(y) pour avoir le point de coordonnées (x,y). Ainsi, l'échiquier
| | |
1| 2| 3|
sera représenté par la valeur
[| [| None; Some 1 |]; [| None; Some 2 |]; [| None; Some 3 |] |]
On donne les fonctions suivantes :
let libre (e:echiquier) (x,y) : bool =
(* vérifie si on peut se déplacer en (x,y) *)
e.(x).(y) = None
let dans_echiquier (e:echiquier) (x,y) : bool =
(* vérifie que les coordonnées (x,y) sont valides pour cet échiquier *)
let w = Array.length e in
let h = Array.length e.(0) in
0 <= x && x < w && 0 <= y && y < h
let valide (e:echiquier) (x,y) : bool =
(* vérifie que (x,y) est une coordonnée valide et que la case est libre *)
dans_echiquier e (x,y) && libre e (x,y)
let deplacements (x,y) : (int * int) list =
(* renvoie les 8 déplacements possibles d'un cavalier en (x,y)
quitte à avoir des coordonnées invalides *)
[ (x-2,y-1); (x-2,y+1);
(x-1,y-2); (x-1,y+2);
(x+1,y-2); (x+1,y+2);
(x+2,y-1); (x+2,y+1) ]
Rappel List.filter permet de ne sélectionner dans une 'a list que les
éléments vérifiant un prédicat 'a -> bool. Plus d'info ici :
https://ocaml.org/api/List.html#1_Listsearching
List.filter (fun x -> x mod 2 = 1) [0;1;2;3;4;5]
- : int list = [1; 3; 5]
Question 1
Compléter la fonction deplacements_valides :
let deplacements_valides (e:echiquier) (x,y) : (int * int) list =
(* renvoie les déplacements valides depuis (x,y) dans e *)
(* on pourra utiliser List.filter *)
Corrigé
let deplacements_valides (e:echiquier) (x,y) : (int * int) list =
List.filter (fun (x,y) -> valide e (x,y))
(deplacements (x,y))
Pour la fonction suivante, on aura besoin de s'assurer que les nombres sont
affichés avec deux chiffres. Pour cela, on peut écrire %2d dans le format
de Printf.printf.
Printf.printf "|%d|%d|\n|%2d|%2d|\n" 3 64 3 64;
flush stdout (* nécessaire pour être sûr de faire l'affichage *)
|3|64|
| 3|64|
Question 2
On vous demande d'afficher l'échiquier sous la forme suivante :
| 5|12|17|22|
13|18|23| 6|11|
4| 1| 8|21|16|
19|14| 3|10| 7|
2| 9|20|15| |
avec un alignement des | et des cases vides si on n'est pas passé
dessus.
Pour vous simplifier la vie, vous pourrez faire un flush stdout avant la
fin de votre fonction. flush c'est la chasse d'eau, ça envoie vraiment
tous les print sur l'affichage.
let print_echiquier (e:echiquier) : unit =
(* Affiche l'échiquier *)
Corrigé
let print_echiquier (e:echiquier) : unit =
let w = Array.length e in
let h = Array.length e.(0) in
for y = 0 to h-1 do
for x = 0 to w-1 do
match e.(x).(y) with
| None -> Printf.printf " |"
| Some k -> Printf.printf "%2d|" k
done; print_newline ()
done;
flush stdout
Pour chercher un tour, on va adapter la méthode du Sudoku : une exception
Solution et une fonction auxiliaire qui effectue des essais de déplacement
en lançant Solution si on a trouvé un tour.
Le principe va donc être celui-là quand on appelle aux n (x,y) pour dire
que (x,y) est le n-ième mouvement du cavalier :
- On marque
Some ndans l'échiquier à cette case. - Si
nmarque le dernier mouvement (facile avec le nombre de cases), on lanceSolution. - Sinon, on itère
aux (n+1)sur tous les déplacements valides.- Soit on a une
Solutionlancée pendant cette itération et donc on sort. - Soit c'est que ce déplacement ne permet pas d'aboutir à une solution,
donc on repasse à
Nonela case(x,y).
- Soit on a une
Question 3
Compléter la fonction tour :
exception Solution
let tour w h (x0, y0) : echiquier =
let e = Array.make_matrix w h None in
try
let rec aux n (x,y) =
failwith "C'est à vous !"
in
aux 1 (x0,y0);
raise Not_found (* pas de solution ! *)
with Solution -> e (* on renvoie e avec la solution *)
print_echiquier (tour 5 5 (0,0)) (* doit permettre d'afficher le tour *)
Question 4
Compléter la fonction tour_ferme :
let tour_ferme w h (x0,y0) : echiquier =
(* Cherche un tour fermé *)
failwith "C'est à vous !"
Corrigé (fichier TP12_cavalier.ml)
Le corrigé de l'auteur est le programme
TP12_cavalier.ml. Ses fonctions
tour et tour_ferme prennent directement l'échiquier en paramètre et
animent la recherche avec la bibliothèque Graphics ; il contient aussi
les fonctions tous_tour et tous_tour_ferme qui énumèrent tous les
tours (première extension ci-dessous).
open Graphics
type echiquier = int option array array
let libre echiquier (x, y) =
(* vérifie si on peut se déplacer en (x,y) *)
echiquier.(x).(y) = None
let dans_echiquier echiquier (x, y) =
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
0 <= x && x < w && 0 <= y && y < h
let valide echiquier (x, y) =
dans_echiquier echiquier (x, y)
&& libre echiquier (x, y)
let deplacements (x, y) =
[ (x-2,y-1);(x-2,y+1);
(x-1,y-2);(x-1,y+2);
(x+1,y-2);(x+1,y+2);
(x+2,y-1);(x+2,y+1) ]
let deplacements_valides echiquier (x, y) =
List.filter (valide echiquier) (deplacements (x,y))
let rec accumule f l =
match l with
| [] -> []
| t::q -> f t @ accumule f q
let print_echiquier echiquier =
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
for j = 0 to h-1 do
for i = 0 to w-1 do
match echiquier.(i).(j) with
| None -> print_string " |"
| Some d -> Printf.printf "%2d|" d
done;
print_newline ()
done
exception Trouve of int * int
let cherche echiquier n =
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
try
for i = 0 to w - 1 do
for j = 0 to h - 1 do
if echiquier.(i).(j) = Some n
then raise (Trouve (i,j))
done
done;
raise Not_found
with Trouve (i,j) -> (i,j)
let rec chemin echiquier n =
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
if n = w * h
then []
else cherche echiquier n :: chemin echiquier (n+1)
let z = 50
let affiche_echiquier e =
let w = Array.length e in
let h = Array.length e.(0) in
for i = 0 to h do
moveto 0 (z*i);
lineto (z*w) (z*i)
done;
for i = 0 to w do
moveto (z*i) 0;
lineto (z*i) (z*h)
done;
set_text_size (20);
try
for i = 1 to w*h - 1 do
let x1, y1 = cherche e i in
let x2, y2 = cherche e (i+1) in
moveto (z*x1+z/2) (z*y1+z/2);
draw_string (Printf.sprintf "%d" i);
lineto (z*x2+z/2) (z*y2+z/2)
done
with Not_found -> ()
exception Solution
let tour echiquier (x0,y0) =
try
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
let rec aux n (x,y) =
echiquier.(x).(y) <- Some n;
clear_graph ();
affiche_echiquier echiquier;
Unix.sleepf 0.1;
if n = w * h
then raise Solution;
List.iter
(aux (n+1))
(deplacements_valides echiquier (x,y));
echiquier.(x).(y) <- None
in aux 1 (x0,y0)
with Solution -> ()
let tour_ferme echiquier (x0,y0) =
try
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
let rec aux n (x,y) =
echiquier.(x).(y) <- Some n;
clear_graph ();
affiche_echiquier echiquier;
Unix.sleepf 0.2;
let ld = deplacements (x,y) in
if n = w * h && List.mem (x0,y0) ld
then raise Solution;
List.iter
(aux (n+1))
(deplacements_valides echiquier (x,y));
echiquier.(x).(y) <- None
in aux 1 (x0,y0)
with Solution -> ()
let dup e =
let w = Array.length e in
let h = Array.length e.(0) in
let e' = Array.make_matrix w h None in
for i = 0 to w - 1 do
for j = 0 to h - 1 do
e'.(i).(j) <- e.(i).(j)
done
done;
e'
let tous_tour echiquier (x0,y0) =
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
let rec aux n (x,y) =
echiquier.(x).(y) <- Some n;
let l = if n = w * h
then [ dup echiquier ]
else
List.fold_left (@) [] (List.map
(aux (n+1))
(deplacements_valides echiquier (x,y))) in
echiquier.(x).(y) <- None;
l
in aux 1 (x0,y0)
let tous_tour_ferme echiquier (x0,y0) =
let w = Array.length echiquier in
let h = Array.length echiquier.(0) in
let rec aux n (x,y) =
echiquier.(x).(y) <- Some n;
let ld = deplacements (x,y) in
let l = if n = w * h && List.mem (x0,y0) ld
then [ dup echiquier ]
else
List.fold_left (@) [] (List.map
(aux (n+1))
(deplacements_valides echiquier (x,y))) in
echiquier.(x).(y) <- None;
l
in aux 1 (x0,y0)
let _ =
let w = int_of_string Sys.argv.(1) in
let h = int_of_string Sys.argv.(2) in
let echiquier = Array.make_matrix w h None in
(*
let solutions = tous_tour_ferme echiquier (0,0) in
List.iter (fun e -> print_echiquier e; print_newline()) solutions;
Printf.printf "%d\n" (List.length solutions)
*)
open_graph " ";
tour echiquier (0,0);
(*
affiche_echiquier echiquier;
*)
wait_next_event [Key_pressed]
(*
print_echiquier echiquier
*)
Deux propositions d'extensions :
- énumérer tous les tours pour un petit nombre de cases ;
- trouver des parcours qui forment sur l'échiquier des carrés magiques ou pseudo-magiques. Rappel : un carré est pseudo-magique si la somme des valeurs sur les lignes et les colonnes est la même, un carré est magique s'il est pseudo-magique et a la même somme sur les diagonales.
Façon Python avec une liste :
for x in l do
f x
done
en OCaml :
List.iter f l
List.iter print_int [1;2;3];
print_newline(); flush stdout
123
Sinon on peut la convertir en tableau si elle n'est pas trop grande (ici \(\le 8\) déplacements) :
let a = Array.of_list l in
for i = 0 to Array.length a - 1 do
(* Ici on a a.(i) c'est le ieme de l *)
done