dimanche 3 juillet 2016

Résolution du Binero / Takuzu : les Joies du Backtracking

Coucou !

Les vacances approchent, et avec elles arrivent les livrets de jeux de vacances (youpi~).

Comme beaucoup de gens, mes parents achètent souvent des Sudoku à cette période de l'année... Mais aujourd'hui, ils sont revenus avec quelque chose de nouveau : des grilles de Binero (ou Binairo, ou Takuzu) !


Les règles du Binero / Takuzu


Le jeu se déroule sur une grille carrée avec un nombre pair de cases sur chaque ligne et chaque colonne.
Le but du jeu est de placer un 0 ou un 1 dans chaque case de telle sorte que chaque ligne et chaque colonne possède un nombre égal de 0 et de 1. Il est cependant nécessaire de respecter deux contraintes :
  • Il ne peut pas y avoir plus de deux 0 ou deux 1 consécutifs
  • Les lignes ou colonnes identiques sont interdites
Sur un petit exemple, la grille 4x4 suivante :


...se résout ainsi :


Les terrains classiques vont des tailles 4x4 à 14x14.

Sur des petites grilles le jeu est vraiment très simple. À un niveau plus avancé, la difficulté réside surtout dans le fait qu'il y ait parfois un choix arbitraire à effectuer sur une case pour arriver à la solution, ou à une contradiction qui permet d'en déduire que le choix effectué n'était pas le bon.

A mon humble avis, malgré son aspect épuré, le jeu possède un rapport de complexité / profondeur beaucoup moins intéressant que le Sudoku. Le Sudoku offre en effet une variété de techniques de résolution bien plus vaste que le Binaro, ce dernier m'a d'ailleurs lassé au bout de quelques grilles à peine.

En bref, j'ai trouvé plus intéressant de m'attaquer au problème avec un ordinateur qu'avec du papier et un crayon.


Résolution par Backtracking


Je décide de construire un algorithme en Python 3 capable de résoudre n'importe quelle grille de Binaro.

J'utiliserai une méthode de résolution par backtracking, ou "retour sur trace". Il s'agit d'une méthode d'essai-erreur où l'on fait des décisions arbitraires jusqu'à arriver à une impossibilité : on revient alors sur ces décisions passées pour sortir de l'impasse, et ce jusqu'à la résolution complète du puzzle.

Si vous n'avez jamais entendu parler de récursivité, les explications suivantes risquent de vous passer totalement au dessus de la tête...
Accrochez-vous !

Nous représenterons une grille de Binaro par un tableau à deux dimensions. Les "2" représentent les cases de valeur inconnue.

On s'intéresse par exemple à la grille 14x14 :
grille = [[2,2,2,2,2,1,1,2,2,2,1,2,2,0],
          [2,2,1,1,2,2,1,2,0,2,2,2,1,0],
          [2,2,2,1,2,2,2,2,2,0,2,2,1,2],
          [2,2,0,2,2,0,2,2,2,2,2,0,2,2],
          [2,2,2,2,2,2,2,2,2,2,2,2,2,2],
          [2,0,2,2,2,0,0,2,2,2,0,2,2,2],
          [2,2,2,2,2,0,0,2,2,2,2,2,2,0],
          [2,2,0,2,2,2,2,2,2,2,2,0,2,2],
          [2,2,0,0,2,2,2,2,0,2,2,2,2,2],
          [2,2,2,2,2,2,2,2,2,1,2,2,2,2],
          [2,2,2,2,2,2,2,2,2,2,2,2,1,2],
          [0,0,2,2,0,2,2,1,1,2,2,2,1,2],
          [0,2,2,1,2,2,0,2,1,2,2,0,2,2],
          [2,2,2,2,0,0,2,2,2,2,2,2,1,2]]
On va profiter des effets de bords de Python pour ne manipuler qu'un seul tableau tout le long des appels récursifs de la fonction principale.

def solve(grid,i = 0):
  
dim = len(grid)
  if i == dim*dim:
    return True if checkIdenticalLines(grid) else False
  x = i % dim
  y = i//dim
  if grid[y][x] != 2:
    return solve(grid, i + 1)
  for j in range(2):
    row = grid[y]
    column = [row[x] for row in grid]
    if checkLine(row, x, j) and checkLine(column, y, j):
      grid[y][x] = j
      if solve(grid, i + 1):
        return True
  grid[y][x] = 2
  return False


Le paramètre i (en rouge) va correspondre au numéro de la case traitée, et les paramètres x et y à ses coordonnées (en vert et bleu) sur la grille :

par exemple dans le cas d'une grille 3x3, la case i = 1 est associée à x = 1 et y = 0

La fonction principale fait appel à deux fonctions annexes :
  • checkIdenticalLines qui détermine si la grille finale possède des répétitions de lignes ou de colonnes.
  • checkLine qui détermine si une ligne est juste ou non. Une ligne est correcte si :
    • Son nombre de cases indéterminées est inférieur ou égal à la valeur absolue de la différence entre son nombre de 0 et son nombre de 1.
    • On n'y trouve pas plus de deux 0 ou deux 1 consécutifs.

La fonction principale se décompose ainsi :
  • On commence par l'appeler avec une valeur par défaut de i = 0.
  • La variable dim prend la valeur de la dimension de la grille, par exemple si c'est une grille de 14x14, alors dim = 14.
  • Si i = dim * dim, c'est qu'on a traité toutes les cases de la grille
  • Dans ce cas si toutes les lignes et les colonnes sont différentes alors la grille est résolue, on renvoie True. Sinon la solution est fausse et on renvoie False. Cette propriété est déterminée grâce à checkIdenticalLines.
Dans tous les autres cas :
  • On détermine les coordonnées x et y de la case i traitée.
  • Si la case à laquelle on s'intéresse fait partie des données du problème (ie que sa valeur est différente de 2), alors on l'ignore et on s'intéresse à la case suivante en appelant la fonction récursivement avec une valeur de i incrémentée.
  • Dans le cas contraire la case est indéterminée. On essaye alors de lui donner une valeur 0 puis 1. Si l'une des hypothèses n'entraîne pas de problème sur la ligne et la colonne de la case traitée (test effectué à l'aide de checkLine), alors on l'accepte et on s'intéresse à la case suivante par un appel récursif de la fonction avec une valeur de i incrémentée. Si celle-ci est correcte (ie que l'appel récursif de la fonction renvoie True) c'est que la solution a été trouvée en amont et on peut donc renvoyer True aussi.
  • Si on arrive à un blocage, la case traitée est toujours indéterminée, et on renvoie False car nos hypothèses nous ont menées à une solution qui n'est pas la bonne.

Alors oui, la première fois qu'on lit ce genre d'algorithme, ça semble miraculeux que le machin fonctionne, et pourtant !

In [1]: solve(grille)
Out[1]: True


True est renvoyé, c'est donc que la solution a été trouvée !
Et grâce aux effets de bord, il suffit de rappeler la variable "grille" pour voir la solution !

In [2]: grille
Out[2]:
[[0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0],
 [0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0],
 [1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1],
 [1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1],
 [0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0],
 [1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1],
 [0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0],
 [1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1],
 [1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1],
 [0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0],
 [1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0],
 [0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1],
 [0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1],
 [1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0]]


Si vous n'avez pas tout suivi, prenez le temps de relire les explications et d'en chercher des complémentaires sur le backtracking... Cette méthode permet de résoudre de nombreux autres puzzles !

Je ne vais pas m'attarder sur les fonctions annexes évoquée plus haut, ce n'est pas le cœur de l'article. Sachez juste qu'elles ne possèdent pas d'effet de bord, elles.

Les voici :


def checkLine(line, k, value):
  zero, one, notFound = 0, 0, 0
  past = 2
  adj, m_adj = 1, 1
  line_c = line[::]
  line_c[k] = value
  for e in line_c:
    if e == 0:
      zero += 1
    elif e == 1:
      one += 1
    else:
      notFound += 1
    if past == e and past < 2:
      adj += 1
      if adj > m_adj:
        m_adj = adj
    else:
      adj = 1
    past = e
  return (abs(zero-one) <= notFound and m_adj < 3)


def checkIdenticalLines(g):
  return checkIdenticalColumns(grid) \
and checkIdenticalRows(grid)

def checkIdenticalRows(rows):
  i = 0
  l = len(rows)
  while i < l and rows[i] not in rows[:i]+rows[i+1:]:
    i += 1
  return i == l


def checkIdenticalColumns(grid):
  
columns = \

[[row[x] for row in grid] for x in range(len(grid))]
  i = 0
  l = len(columns)
  while i < l and columns[i] not in columns[:i]+columns[i+1:]:
    i += 1
  return i == l



Notez bien que ces deux dernières fonctions sont sans doute optimisables, je ne garantie pas que le slicing soit adapté dans ce contexte.

Sur ce je vous dis à une prochaine fois !



vendredi 17 juin 2016

Obfuscation en Caml Light : Palindrome mon Amour

Salut !

Lors du précédent article, on a abordé l'obfuscation en Caml au travers de quelques astuces propres au langage, mais sans vraiment construire de programme obfusqué.

Aujourd'hui on va réparer ça.


Genèse


L'autre jour, en lisant un peu les résultats de l'IOCCC (cf article précédent), un code source de l'édition 1987 a fortement attiré mon attention. Il s'agit du programme d'un.e certain.e "westley", arrivé.e 7ème cette année là :


Toutes les lignes de son programme sont des palindromes !

La source de westley est ici : http://www.ioccc.org/1987/westley.c
Cependant chez moi le lien est mort depuis quelques jours. En cas de problème -> WayBackMachine.

Honnêtement j'ai beau adorer l'idée, ce programme là - une fois déobfusqué - est un peu ridicule : il ne fait qu'afficher une chaîne de caractère palindromique. Ceci doit expliquer sa place relativement basse dans le classement.

Reprendre l'idée à mon compte en Caml m'a paru alors plus constructif que de travailler sérieusement mon DM d'informatique.


RionsnoiR


Pour cet exercice il m'a d'abord fallu décider de ce qu'allait faire mon programme. J'ai opté pour une simple détection de palindrome : la fonction prend en argument une chaîne de caractères et renvoie true si celle-ci est un palindrome, false dans le cas contraire.

C'est beaucoup plus compliqué que l'algorithme de westley, mais j'aime bien les challenges alors ¯\_(ツ)_/¯

J'ai d'abord rédigé l'algorithme "en clair" :

let palindrome s =
let rec palin i j = (j-i) < 1 || (s.[i] = s.[j]) && palin (i+1) (j-1)
in palin 0 ((string_length s)-1);;

J'ai opté pour une version récursive dans un intérêt de compacité. Plus la version de départ a une syntaxe légère, plus la version palindromique se fait facilement...
...
Enfin, théoriquement ça semble logique.

Finalement, après ~une semaine~ de travail irrégulier, je mets un terme à la rédaction de mon programme :




let pop tel
_ _
radar = radar
;;
let sa as tel
=
0
;;
let erehw radar = radar where tel
=
0
;;let ni radar = let tel = radar in tel;;

let rionsnoir tel
=
let rec cer tel
kayak _ rotor = rotor - kayak
< 1 || 1 >
8
||
(
nth_char (let ni radar = radar in tel) rahc_htn
where rahc_htn as pop = pop sa nth_char erehw
kayak
)=(
nth_char (let ni radar = radar in tel) rahc_htn
where rahc_htn as pop = pop sa nth_char erehw
rotor
)
&&
(let rec ni radar = radar in cer tel)
(2/3+kayak+3/2)
(1-rotor) (rotor-1)
;in ni;
(let rec ni radar = radar in cer tel)
0
(1-(let htgnel_gnirts ni radar = radar in tel;1)) ((1;let ni radar = radar in string_length tel)-1)
;;




Je ne suis pas peu fier du résultat. Malgré une petite imprécision à la 18ème ligne (j'ai triché), toutes les autres sont bien des palindromes. La fonction principale est "rionsnoir", les autres ne font que la servir. Bon courage pour la lecture du machin.

Pouf pouf.

Voilà qui clôture pour un certain temps mon exploration des possibilités d'obfuscation en Caml. Je n'ai trouvé personne d'autre sur internet à s'être aventuré sur ce terrain, et je suis plutôt content du maigre travail que j'y ai accompli.




dimanche 5 juin 2016

Obfuscation en Caml Light : Deux Astuces

Aujourd'hui je vais vous montrer deux astuces qu'il est possible d'exploiter en Caml Light afin d'obfusquer vos programmes. Cet article s'adresse aussi bien aux néophytes qu'aux habitué.es du langage : les syntaxes présentées sont simples, et souvent méconnues - puisque dépréciées pour écrire du beau code.

Si vous êtes en prépa, vous n'êtes pas sans savoir que Caml Light est maintenant le seul langage officiellement au programme de l'option info. Aussi, n'hésitez pas à abuser de ces astuces pour diviser vos notes par deux.

:)

C'est quoi "l'Obfuscation" ?

L'obfuscation d'un code source est le procédé qui consiste à le rendre difficilement compréhensible par un humain, tout en le laissant parfaitement interprétable par la machine. Parfois afin de protéger son programme de la rétro-ingénierie ; parfois par simple intérêt du programmeur pour l'aspect créatif de la pratique. Le concours international annuel d'obfuscation en C (l'IOCCC) permet d'avoir un aperçu du niveau d'entropie atteint aujourd'hui...

Ici le programme d'Endoh1, arrivé en 4ème position de l'IOCCC 2016


Aujourd'hui on va en faire pour le fun et à un degré de complexité moindre : je vais simplement vous présenter deux astuces, ça sera à vous de jouer avec et de le combiner dans tous les sens !

En Caml Light

Astuce 1 : Des Collisions de Caractère

Le Caml est un langage français ! C'est l'un des rares à accepter les accents dans le nom des variables. Pour l'interpréteur la variable "élément" est donc différente de la variable "element".

Cependant le langage accepte également des caractères spéciaux qui sont curieusement remplacés par d'autres lors de la compilation ! Exactement comme si vous initialisiez une variable "élément", et qu'elle soit accessible par "element" !

En pratique les lignes suivantes...
let ™ = 1 in T;;
let š = 2 in s;;
let Š = 3 in S;;
let œ = 4 in o;;
let Œ = 5 in O;;
let ƒ = 6 in f;;
...sont interprétées ainsi :
#let ™ = 1 in T;;
- : int = 1
#let š = 2 in s;;
- : int = 2
#let Š = 3 in S;;
- : int = 3
#let œ = 4 in o;;
- : int = 4
#let Œ = 5 in O;;
- : int = 5
#let ƒ = 6 in f;;
- : int = 6
Il est à noter que ces remplacements peuvent également se faire sur des mots réservés. Ainsi, "for" peut tout à fait s'écrire "ƒœr", sans altérer l’exécution normale du programme.

De la même manière, les points (utiles par exemple pour les floats ou les tableaux) peuvent être remplacés par les caractères "•" et "…" sans que cela ne pose de problème à l'interpréteur.

Ici la ligne...
let a = [|8•;7…6|] in a•(0) +• a…(0) +… 9•1;;
...s’exécute parfaitement :
#let a = [|8•;7…6|] in a•(0) +• a…(0) +… 9•1;;
- : float = 25.1
Et la fonction...
let ™Œ™Œ =
let œo = ref 1• in
ƒœr Œ = 1 tœ 9 dœ
oœ := (fun s -> float_of_int(O)*…š)(!œœ);
dœne;
!oo;;
...s'appelle de la manière suivante :
#TOTO;;
- : float = 362880.0
C'est moche ? Alors nous sommes sur la bonne voie.

Astuce 2 : Des Instructions avant une Condition

Ici ça s'explique en deux mots : pour chaque structure syntaxique impliquant une condition, on peut précéder cette condition d'instructions qui seront exécutées quoi qu'il arrive.

Partons d'un exemple : normalement, un if s'écrit :
[instruction1]; if [condition] then [instruction2];
Et bien  il est possible de condenser cette syntaxe sous la forme :
if [instruction1];[condition] then [instruction2];
...
Sur une série d'exemple ça donne :

Pour le if :
#let a = ref 0 in if incr a; !a = 1 then print_string("oui");;
oui- : unit = ()
Pour le for (ici j'ai un peu joué en utilisant un int et une référence qui portent le même nom) :
#let j = ref 9 in for j = 0 to print_string("deb");incr j; !j do print_int(j) done; !j;;
deb012345678910- : int = 10
Pour le when :
let rec foo x =
let j = ref 1 in match x with
| y when (incr j; !j = y) -> y
| y -> print_int(y - !j); foo (y - 1);; 
#foo 12;;
10987654321- : int = 2
Sachez qu'on peut en tirer un équivalent de la boucle do... while en Caml.

Et oui ! Le do...while que - personne - n'utilise et absent dans la moitié des langages (dont le Caml) peut être implémenté par une syntaxe de la forme :
while [instructions];[condition] do () done;
 Honnêtement, j'ai un faible pour cette structure.
#let a = ref 0 in while incr a; !a < 10 do () done; !a;;- : int = 10

Embêtez vos profs d'info, et ne respectez plus jamais aucune indentation...


Bon code !