• Aucun résultat trouvé

E452. Qui se répète perd

N/A
N/A
Protected

Academic year: 2022

Partager "E452. Qui se répète perd"

Copied!
6
0
0

Texte intégral

(1)

E452. Qui se répète perd

Diophante fixe un entier naturel n ≥ 2. Zig et Puce partent d'une ligne vide, le premier joueur écrit

"0" ou "1" puis chacun à son tour ajoute "0" ou "1" à la fin de la séquence de "0" et de "1"

précédemment écrite. Un joueur perd si le chiffre qu'il ajoute fait apparaître un bloc de n chiffres consécutifs qui se répète pour la deuxième fois. Les deux blocs qui se répètent peuvent se

chevaucher Par exemple:

- pour n = 3, à partir de la séquence 0011100 le second joueur perd en écrivant "1" car le bloc de 3 chiffres "001" se répète dans la séquence 00111001.

- pour n = 5, à partir de la séquence 101010 le premier joueur perd en écrivant "1" car le bloc de 5 chiffres "10101" se répète dans la séquence 1010101.

Q1 Démontrer que quel que soit n ≥ 2, la partie se termine toujours en un nombre fini de tours.

Q2 n = 3 et Puce commence la partie. Qui est vainqueur?

Q3 n = 4 et Zig commence la partie. Qui est vainqueur?

Q4 n = 5 et Zig commence la partie. Qui est vainqueur?

Pour les plus courageux: peut-on déterminer qui a une stratégie gagnante en fonction de n?

Solution proposée par Claudio Baiocchi

Dans une chaîne de 0/1 de longueur on a sous-chaines de longueur (si les sous- chaîne peuvent se chevaucher). Les chaîne distinctes de longueur étant , on aura

nécessairement une répétition dès que .

Naturellement le joueur qui est destiné à perdre et celui qui peut gagner vont suivre des stratégies opposées : le futur perdant cherchera de renvoyer la fin construisant un mot le mot le plus long possible ; l’autre, au contraire, cherchera de gagner avec le mot le plus court possible. En appelant optimale une stratégie qui respecte ces deux buts, les chaînes suivantes (qu’on a obtenues grâce à un programme Pascal détaillé et commenté ci-dessous) sont toutes optimales ; les points

d’interrogation qui terminent chaque chaîne expriment que tout choix ultérieur va engendrer une répétition :

Pour : une suite gagnante est 0010?

Pour : une suite gagnante est 01011001?

Pour : une suite gagnante est 001000110111001?

Pour : une suite gagnante est 010101100110100101?

On désigne par n°1 le joueur qui commence la partie et par n°2 l'autre joueur.

Pour ce qui concerne les questions Q₂,Q₃ et Q₄, les résultats des cas et suggèrent que, pour impair, le joueur n°2 vaincra en écrivant un chiffre distinct de celui choisi par le joueur n°1; mais l’optimalité de la stratégie n’est pas évidente, et on n’a pas d’indication pour ce qui concerne le joueur n°1.

Un programme-arbitre

Ce type de jeux, ainsi que bien d’autres, rentre dans le schéma caractérisé par les règles suivantes:

1) Il n'existe pas de match nul ; toute partie se termine après un nombre fini de coups.

2) La partie est finie lorsqu’il n’y a plus de coups admissibles.

3) Chaque joueur, à son tour, est placé devant l'alternative:

(2)

a) si la partie n'est pas finie, le joueur exécute un coup, à son choix, parmi les coups admis et il consigne à l'adversaire la situation résultante,

b) sinon, il déclare sa défaite.

Un défi entre deux humains peut être "fastidieux" à analyser manuellement : savoir si un caractère engendre une sous-chaine déjà présente devient de plus en plus compliqué lorsque croit.

Faisant usage du type « string », présent dans presque tous les langages de programmation, on peut aisément construire un programme-arbitre qui gère le jeu en empêchant en particulier les coups interdits et qui, créant au fur et à mesure une liste des coups admis dans la situation actuelle, peut déclarer la fin du jeu.

Dans le programme suivant, écrit en Dev-Pascal, on a fixé ; et on a rajouté la possibilité de jouer avec plus de symboles (il suffit de changer la valeur de la variable last ; par exemple avec last=’2’ on admettrait les trois symboles . Pour des raisons techniques on a préféré travailler à la façon des arabes : le caractère c à rajouter est rattaché à la gauche (au lieu qu’à la droite) de la situation actuelle now : la motivation étant que, en Pascal, isoler le bloc de gauche est plus simple que isoler le bloc de gauche ; c’est la fonction rep qui s’occupe de cela mais naturellement, pour la visualisation, on utilise la fonction reverse. La fonction fini donne un résultat booléen et, grâce au fait qu’on a déclaré VAR son paramètre permis, elle modifie aussi la variable ss de la routine coup qui fournit la liste des choix permis à partir du contenu de la situation actuelle now.

uses crt;

(* E452. Qui se répète perd *)

const noms:array[false..true]of string=('Puce',' Zig');

color:array[false..true]of byte=(2,15);

var joueur:boolean; last:char; n:byte; now:string;

procedure init;var c:char; p:byte;

begin

clrscr; last:='2'; n:=3; now:='';

writeln(noms[true],' contre ',noms[false]);

write(noms[true],' joue en premier ? (o/n) ');

repeat c:=readkey; p:=pos(upcase(c),'N,OSY') until p>0;

writeln(c); joueur:=p>1 end;

function rep(c:char; s:string):boolean;

begin

if length(s)<n then rep:=false else rep:=pos(copy(c+s,1,n),s)>0 end;

function reverse(s:string):string; var t:string; x:byte;

begin

t:=''; for x:=1 to length(s) do t:=s[x]+t; reverse:=t end;

function fini(s:string; var permis:string):boolean; var c:char;

begin

permis:=''; for c:='0' to last do

(3)

if not rep(c,s) then permis:=permis+c;

fini:=permis='' end;

procedure coup(j:boolean); var c:char; ss:string;

begin

textcolor(color[j]);write(noms[j],' doit ');

if now='' then write('commencer; choix ? ') else

write('rajouter un caract',#138,'re ',#131,' ',reverse(now));

if fini(now,ss) then begin

writeln(' mais tout symbole est interdit! '); now:='?' end else begin

repeat c:=readkey until pos(c,ss)>0;

writeln(c);now:=c+now end end;

begin init; repeat

coup(joueur); joueur:=not joueur until now='?'; write('Termin',#130,' : ');

writeln(noms[joueur],' a perdu !');

readln end.

Comment "soudoyer" le programme-arbitre

On remarquera que, en accord avec les règles du jeu (qui parlent uniquement de coups perdants) notre arbitre, grâce à la function rep, sait uniquement quels sont les coups perdants et, en empêchant de jouer un coup perdant, le programme-arbitre interdit aux joueurs de se suicider. Et si on voulait

"soudoyer" l’arbitre ?

En fait l’arbitre connait bien plus de choses que l'on pourrait croire de prime abod : la fonction rep sait distinguer les coups (perdants ou non-perdants) mais la fonction fini permet de distinguer les situations : on peut appeler perdante une situation à partir de laquelle tout coup est perdant ! A partir de là on peut définir coups gagnants et situations gagnantes, définissant gagnant tout coup dont l’exécution consigne à l’adversaire une situation perdante, et définissant gagnante toute

situation où l’on dispose d’au moins un coup gagnant. Naturellement implémenter ces idées dans le programme demande uniquement un cycle sur l’ensemble des coups, contrôlant le résultat de l’exécution de chaque coup admis.

Bien sûr, on peut continuer : un coup est perdant au niveau 2 s’il engendre une situation gagnante ; et une situation est perdante au niveau 2 si tous les coup admis sont perdants au niveau 2 ; un coup est gagnant au niveau 2 si il engendre une situation perdante au niveau 2 ; et une situation est gagnante au niveau 2 s’il existe un coup gagnant au niveau 2… En théorie rien n’empêche de continuer ; ainsi que dans le jeu d’échecs, où la force d’un joueur peut se mesurer en termes de nombre de coups et contrecoups qu’il sait prévoir, ici un bon programme est un programme qui sait rechercher des bons coups de niveau assez élevé. Mais comment réaliser tout cela ?

D’ici peu on construira une routine next qui permet de résoudre des problèmes un peu plus

difficiles ; l’idée de base étant que la routine next fera appel à soi-même pour passer de niveau ! Il s’agit de ce qu’on appelle fonction récursive.

(4)

Remarques pour qui n’a jamais vu des programmes récursifs

 Pour ceux qui ne connaissent pas le sujet : est récursive une fonction qui, au cours de son exécution, peut exécuter une copie de soi-même. Lors de cette exécution le programme doit sauver quelque part toutes les valeurs qu’il utilise, car la copie utilisera des variables de même nom ; à fin exécution on devra reconstruire toute valeur de départ. La zone de mémoire réservée à cette mémorisation prend le nom de pile (stack en anglais).

 Comme exemple on peut penser à la fonction puissance :

(absente dans Pascal) qui, pour , peut être définie par :

function power ( x :real ; n :byte ) : real ;

begin if n=1 then power := x else power := x*power(x,n-1) end ;

Des réalisations nettement plus performantes (et moins « couteuses » du point de vue des ressources employées) sont les constructions itératives ; par exemple :

function power ( x :real ; n :byte ) : real ; var y : real;

begin y:=1; for x:=1 to n do y:=y*x; power:=y end;

Malheureusement, pour notre programme de jeu, il n’existe pas de versions itératives.

 Seulement quelques logiciels admettent la récursion. Par exemple Basic ne l’admet pas ; le bon vieux Turbo-Pascal admet la récursion, mais d’un niveau très bas car sa pile est très petite. De son côté, Dev-Pascal réserve beaucoup d’espace mais manque d’une diagnostique efficiente : lorsque la pile est pleine il se plante…

De l’arbitre soudoyé à l’Oracle

En réalité les problèmes dont on est parti, qui demandent « Qui va gagner ? » demandent un peu plus que les problèmes du type « Que jouer maintenant ? » : au lieu que chercher les meilleurs coups, l’un après l’autre, il faut construire un développement complet du jeu qui soit optimal dans le sens précisé tout au début : le joueur qui est destiné à perdre et celui qui peut gagner vont suivre des stratégies opposées car le futur perdant cherchera de renvoyer la fin construisant un mot le mot le plus long possible ; l’autre, au contraire, cherchera de gagner avec le mot le plus court possible. La différence essentielle est que, au lieu de mémoriser le meilleur coup à partir d’une situation donnée, on doit mémoriser le développement tout entier qui décrit la partie optimale.

Par rapport au programme-arbitre, la routine init ne servira plus ; les fonctions rep, reverse et fini resteront telles quelles ; l’évaluation, à un niveau imprécisé, d’une situation now sera le maximum des valeurs v attribuées aux coups admis. La variable global opt est la chaine qui mémorisera le développement optimal du jeu : initialisée à ‘0’ (inutile de suivre aussi les développements qui commencent par ‘1’) elle est passée comme VAR à la routine next, qui mémorisera dans elle l’évolution optimale du jeu. Partant d’une valeur myv initialisé avec pessimisme à -1, la routine next évalue les coups permis, pour éventuellement porter myv à +1 lorsqu’on trouve un coup gagnant :

function next(now:string; var best:string):shortint;

var c:char; mybest,permis:string; v,myv:shortint;

begin

myv:=-1; mybest:='';

if fini(now, permis) then mybest:='?'+now else begin

for c:='0' to last do

(5)

if pos(c,permis)>0 then begin v:=-next(c+now,best);

Là il faut faire bien attention : pour croitre de niveau, la routine next demande à elle-même

l’évaluation de la situation modifiée ; naturellement il faut changer de signe le résultat car la routine donne valeur -1 aux situations perdantes (pour notre adversaire, donc gagnantes pour nous) et +1 aux situations gagnantes (pour l’adversaire, et perdantes pour nous). On remarquera aussi que le paramètre best (qui est global car il a été déclaré VAR) contient la meilleure poursuite du jeu ; donc, lorsqu’on met à jour la variable myv, il se peut qu’on doive mettre à jour aussi la variable mybest (copie locale de best) ; la mise à jour est faite toujours lorsqu’on passe d’une situation perdante à une gagnante ; tandis que, lorsque la situation reste telle quelle (perdante ou gagnante) on applique la stratégie naturelle : le gagnant cherche des solutions rapides, le perdant veut rallonger tant qu’il peut. La routine next continue donc sous la forme :

if v>myv then begin myv:=v; mybest:=best end else begin

if myv=1 then begin

if length(mybest)>length(best) then mybest:=best

end else if length(mybest)<length(best) then mybest:=best end end end;

best:=mybest; next:=myv end;

Pour mieux comprendre le résultat final la chaine best sera affichée par une routine qui écrit les choix des joueurs en couleur différents ; par exemple, dans l’usuel écran noir, en couleurs vert et blanc :

procedure print(best:string);var x,m:byte;

begin

reverse(best);

m:=2;for x:=1 to length(best) do begin textcolor(m); write(' ',best[x]); m:=17-m;

end; textcolor(15) end;

Il ne reste qu’à lancer le programme : begin

last := '1'; for n:=2 to 5 do begin

opt:='';write(#10#13'n = ',n,' ; qui joue en premier ');

if next('0',opt)=-1 then writeln('peut gagner') else writeln('n''a pas d''espoir');

write('meilleurs coups : ');

print(reverse(opt)); writeln end; write('Fini ! ');readln end.

Remarques finales Les réponses du programme dans chaque cas ( ) ont été pratiquement immédiates. En fait j’ai essayé aussi le cas de mais, après une nuit de travail, le programme

(6)

n’a sorti aucune réponse. Il est probable que l'analyse des longues chaines a engendré une pile trop pleine, et peut être une adaptation qui arrive à (établir et) couper les branches inutiles pourrait aider.

En fait, pour ce qui concerne l’exploration des arbres, on connait une technique (dite , ou en anglais Alpha–beta pruning) qui devrait permettre de réduire les temps d’élaboration car on coupe (au lieu qu’examiner en détail) branches et feuilles inutiles de l’arbre de jeu. Son

implémentation est toutefois assez délicate, er demanderait une fonction d’évaluation plus

sophistiquée : les valeurs fournies par la routine next ne sont pas suffisantes à décider l’inutilité de branches et feuilles…

Références

Documents relatifs

Compte tenu de l’importance de la prévention des événements cardiovasculaires dans cette population et des préoccupations récentes concernant certains effets indésirables

Dans l'analyse que nous allons faire des résultats de notre commerce extérieur, nous citerons les chiffres du commerce spécial et non les chiffres du commerce général, car le

Un joueur perd si le chiffre qu'il ajoute fait apparaître un bloc

Donc 100 est toujours gagnant pour le joueur qui hérite de cette position : en fonction de la stratégie gagnante à partir de 001 (il en existe toujours une puisque le nombre de

Un joueur perd si le chiffre qu'il ajoute fait apparaître un bloc de n chiffres consécutifs qui se répète pour la deuxième fois.. Les deux blocs qui se

-Relevez dans le texte ce qui démontre que Vautrin prend Rastignac par son point faible.. -Les exemples dont se sert Vautrin sont de l’ordre du concret et non pas

-Le niveau courant appelé aussi usuel, médian, standard, qui se caractérise par le respect des règles syntaxiques, morphologiques et phonétiques ainsi que par

Ma passion de la lecture est plus forte encore que vous ne l’aviez ( penser ) :voyez, dans le coin de ma chambre, cette pile de livres que j’ai ( lire ) en quelques semaines..