Contrôle Classant -- Informatique 431

Claire Kenyon   Jean-Jacques Lévy 1
Ecole polytechnique, 30 juin 2004






On attachera une grande importance à la concision, à la clarté, et à la précision de la rédaction. Lorsqu'une question porte sur une complexité en temps ou en espace, seul l'ordre de grandeur en fonction du paramètre précisé est demandé. En Java, la concaténation u+v de deux chaînes de caractères u et v alloue un espace mémoire dont la taille est la somme des longueurs de u et de v.



Un polyomino P est une union de carrés élémentaires du plan, où le carré (i,j) est l'ensemble Ci,j de points défini par Ci,j = { (x,y) | i £ x £ i+1,  j £ y £ j+1 }. Un polyomino est dit 4-connexe si, pour toute paire de carrés C et C' de P, on peut relier C à C' par une suite de carrés C=C0, C1, C2, ...Cl= C' telle que, pour tout k (0 £ k < l), le carré Ck+1 soit le voisin Nord, Sud, Est ou Ouest du carré Ck. Un polyomino est 8-connexe si, pour toute paire de carrés C et C' de P, on peut relier C à C' par une suite de carrés C=C0, C1, C2, ...Cl= C' telle que, pour tout k (0 £ k < l), le carré Ck+1 soit le voisin Nord, Sud, Est, Ouest, Nord-Ouest, Sud-Ouest, Nord-Est ou Sud-Est du carré Ck. Le complémentaire d'un polyomino P est l'union des carrés élémentaires Ci,j non contenus dans P.

Un polyomino est simplement connexe, ou encore sans trou, s'il est 4-connexe et si son complémentaire dans le plan est 8-connexe. Dans le problème, nous ne considérerons que des polyominos de surface finie non nulle. On identifie les polyominos à translation près; par exemple il n'existe qu'un seul polyomino simplement connexe de surface 1 et que deux polyominos simplement connexes de surface 2.


Figure 1: Trois polyominos simplement connexes


Un polyomino P simplement connexe peut être codé par un mot de contour, obtenu à partir de tout point de départ à coordonnées entières sur la frontière de P, en parcourant la frontière de P dans le sens direct (sens contraire du mouvement des aiguilles d'une montre); pour chaque morceau de contour de longueur 1 parcouru, on écrira g pour gauche, d pour droite, h pour haut et b pour bas. On obtient donc un mot sur l'alphabet {g, d, h, b}. Ce codage n'est pas unique: il dépend du choix du point de départ.

Un mot de contour est canonique si son point de départ est le point de la frontière de P minimum dans l'ordre lexicographique <l ex (L'ordre lexicographique est défini par (x,y) <l ex (x',y') si et seulement si x < x' ou x = x' et y < y').

Si on pose an=aa ... a pour le mot formé de n lettres a (n ³ 0), deux contours possibles pour le polyomino de la figure ??(a), à partir des deux points marqués sur la frontière, sont d4hdhg3bghg3bg2hg3bdbd5hdb et dbd5hdbd4hdhg3bghg3bg2hg3b. Le deuxième est canonique.

Partie I. Contours

Question 1 Donner le mot de contour canonique du polyomino de la figure ??(b).

Question 2 Montrer que si u est le mot d'un contour d'un polyomino simplement connexe, il contient autant de g que de d, et autant de h que de b. Cette condition est-elle suffisante? Pourquoi?

Les mots de contour sont représentés par la classe Contour qui définit les quatre constantes D, H, G, B représentant les directions d, h, g, b et un tableau v d'entiers contenant le mot de contour.




  final static int D = 0, H = 1, G = 2, B = 3;
  int[ ] v;
  Contour (int n) { v = new int[n]; }
}

Question 3 Ecrire la fonction testXY(m) qui teste si le mot de contour m contient autant de g que de d et autant de h que de b. Quelle est sa complexité en temps par rapport à la longueur de m?

Question 4 Ecrire la fonction egal(m, m') qui teste si les mots m et m' sont des mots de contours d'un même polyomino. Quelle est sa complexité en temps par rapport aux longueurs de m et de m' ?



On s'intéresse à présent aux polyominos simplement connexes de hauteur 1 ou 2, c'est-à-dire aux polyominos simplement connexes contenus dans la bande semi-infinie de hauteur deux définie par A = {(x,y) | 0 £ y £ 2,  x ³ 0 } (voir exemple de la figure ??(a)). Soit L l'ensemble des mots de contours canoniques de tels polyominos.

Question 5 Dessiner tous les polyominos simplement connexes de surface au plus 3 dont le mot de contour canonique est dans L.

Question 6 Donner une grammaire algébrique (context-free) pour engendrer L.

Question 7 Donner une version non ambigue de cette grammaire, sans en démontrer formellement sa non-ambiguité.

Question 8 En déduire une fonction imprimerL(n) qui imprime tous les mots de contours canoniques des polyominos de hauteur 1 ou 2 de longueur inférieure ou égale à n avec une complexité O(k n2) en espace mémoire où k est le nombre de solutions. Cette fonction imprime-t-elle plusieurs fois le même mot de contour?

Question 9 Donner des indications pour écrire la fonction imprimerL(n) avec une complexité O(k n) en espace mémoire où k est le nombre de solutions.

Partie II. Génération aléatoire

L'objectif de cette partie est de construire des polyominos simplement connexes dont le complémentaire est 4-connexe. Pour cela, on utilise un algorithme probabiliste dont l'idée générale est la suivante. On part d'un point quelconque du plan; on fait un chemin aléatoire dont chaque pas est choisi parmi les quatre directions possibles prises dans l'ensemble {d, h, g, b} de manière aléatoire uniforme (en excluant la direction opposée à la dernière direction choisie). Dès que le chemin u ainsi construit crée un cycle, si ce cycle se termine sur le point de départ initial de u, il définit un polyomino simplement connexe (u correspond au parcours de la frontière du polyomino, dans le sens direct ou inverse) et on a fini; sinon, u se décompose en un chemin v non vide suivi par un cycle, et on élimine le cycle avant de continuer la marche aléatoire à partir de v. Si cet algorithme termine, on veille à retourner le contour dans le sens direct. Dans cette construction, à tout moment, le chemin u construit ne peut posséder qu'au plus un cycle.

Comme la longueur maximale des contours de polyominos est inconnue dans cette partie, on représente à présent ces contours par des objets de la classe ContourE, c'est-à-dire par des listes de déplacements d, h, g, b. (Dans cette représentation, l'image miroir d'un contour est aussi représentable par un objet de la classe ContourE)

.59@percent
  final static int D = 0, H = 1, G = 2, B = 3;
  Liste ch;
}

class Liste {
  int val; Liste suivant;
  Liste (int x, Liste a) { val = x; suivant = a; }
.40@percent
    Liste r = null;
    while (a != null) {
      r = new Liste (a.val, r);
      a = a.suivant;
    }
    return r;
} } }

Question 10 Soit P un polyomino simplement connexe. On considère la valeur obtenue en faisant le tour de P dans le sens direct et en comptant +1 pour chaque virage d'un quart de tour vers la gauche et -1 pour chaque virage d'un quart de tour vers la droite. Montrer, par récurrence sur la surface de P, que la valeur totale lorsqu'on fait le tour de P une fois est exactement égale à 4.

Question 11 Soit u un chemin qui définit un polyomino simplement connexe. Écrire une fonction estEnOrdreDirect(u) qui vérifie si u est un parcours de la frontière dans le sens direct. Quelle est la complexité en temps de cette fonction?





En Java, la classe Random possède la méthode nextInt(n) pour tout objet de sa classe qui retourne un nombre entier aléatoire entre 0 (inclus) et n (exclus). Ainsi pour générer un nombre dans {0,1,2,3}, on écrit:


  ...
  int d = rand.nextInt(4);     // pour avoir un nouveau nombre aléatoire
  ...

Question 12 Écrire une fonction genererUnContour() qui génère un contour aléatoire d'un polyomino simplement connexe, selon l'algorithme indiqué au début de cette partie.

(Remarque: il est possible que le programme ne termine pas si le chemin ne revient pas à son point de départ. Cependant, on peut démontrer que la probabilité d'un tel événement est nulle.)

Partie III. Connexité simple


Soit N un entier fixé (N > 0). On souhaite décider si la région du plan A = {(x,y) | 0 £ x, y £ N } contient un seul polyomino simplement connexe, comme sur la figure ??.


Figure 2: Région


On représente le plan par une matrice de booléens noir telle que noir[i,j] = true si et seulement si la région {(x,y) | i £ x £ i+1, j £ y £ j+1 } est contenue dans un polyomino. On suppose, par ailleurs, que la classe Paire suivante permet de manipuler des paires d'entiers.
.45@percent
  boolean[ ][ ] noir;
  Region (int n) {
    noir = new boolean[n][n];
} }
  .45@percent
  int x, y;
  Paire (int x0, int y0) {
    x = x0; y = y0;
} }

Question 13 Écrire une fonction trouverUnPoint(r) qui renvoie la paire de coordonnées d'un point d'un polyomino contenu dans la région r. Cette fonction retourne null si ce point n'existe pas. Quelle est la complexité en temps de cette fonction?

Question 14 Écrire une fonction contientUnPolyomino(r) qui retourne la valeur vraie si la région r contient un seul polyomino 4-connexe de surface maximale, et que ce polyomino soit simplement connexe. Quelle est la complexité en temps de cette fonction? (Expliquez votre algorithme en termes simples de structures de données)



Une région peut A = {(x,y) | 0 £ x, y £ N } peut à présent contenir plusieurs polyominos, certains sont simplement connexes, d'autres sont avec trous.

Question 15 Écrire une fonction compterPolyominos(r) qui compte le nombre de polyominos simplement connexes parmi les polyominos 4-connexes de surfaces maximales contenus dans la région r. (Expliquez à nouveau votre algorithme; on pourra procéder de l'extérieur vers l'intérieur)


Figure 3: Région avec 6 polyominos simplement connexes de surfaces maximales (en couleur foncée)


1 Corrigé

Question 1 db2d6h5ghggbdbggbbdhdbbgggh4dhggbbgbbdbb

Question 2 Si (x,y) sont les coordonnées d'un point du contour, on revient au même point après un tour complet. Donc la somme totale des déplacements en X et Y est nulle.

Cette condition n'est pas suffisante. Contrexemples: dg, hdbg, dhhdbggb.

Question 3 Solution en O(n) où n est la longueur de m.
    int dx = 0, dy = 0;
    for (int i = 0; i < m.v.length; ++i)
      switch (m.v[i]) {
      case D: ++dx; break;
      case H: ++dy; break;
      case G: --dx; break;
      case B: --dy; break;
      }
    return dx == 0 && dy == 0;
  }

Question 4 Solution en O(n2) où n est la longueur de m et de m1.
    int n = m.v.length, n1 = m1.v.length;
    boolean res = false;
    if (n == n1)
      for (int i = 0; !res && i < n; ++i) {
        res = true;
 for (int j = 0; res && j < n; ++j) {
   res = m1.v[j] == m.v[(i+j)  % n];
      } }
    return res;
  }

Question 5 

Question 6 On décompose les polyominos selon la largeur les mots de L. Ce qui donne
L ® d  L1  gb | d  L2  gbb | d  L3  gb
L1 ® h | d  L1  g | d  L2  gb
L2 ® hh | d  L1  gh | hd  L3  g | d  L2  g
L3 ® h | d  L3  g | bd  L2  g

Question 7 Dans la décomposition précédente, on peut avoir deux fois les polyominos de hauteur 1. Il faut donc ne les faire apparaitre que dans une seule décomposition:
L ® d  L1  gb | d  L2  gbb | d  L'3  gb
L1 ® h | d  L1  g | d  L2  gb
L2 ® hh | d  L1  gh | hd  L3  g | d  L2  g
L3 ® h | d  L3  g | bd  L2  g
L'3 ®      d  L'3  g | bd  L2  g

Question 8 On suit la définition de la grammaire précédente; il n'y a qu'une seule occurrence de chaque contour, sinon la grammaire serait ambigue. Ce programme prend une place en O(k n2) puisque chaque concaténation crée un nouvel espace mémoire. En fait seul O(n2) n'est nécessaire à tout moment.
    if (n >= 4) iL1 ("d", n-3, "gb");
    if (n >= 6) iL2 ("d", n-4, "gbb");
    if (n >= 8) iL3bis ("d", n-3, "gb");
  }

  static void iL1 (String s, int n, String t) {
    if (n >= 1) iSol (s+"h"+t);
    if (n >= 3) iL1 (s+"d", n-2, "g"+t);
    if (n >= 5) iL2 (s+"d", n-3, "gb"+t);
  }

  static void iL3 (String s, int n, String t) {
    if (n >= 1) iSol (s+"h"+t);
    if (n >= 3) iL3 (s+"d", n-2, "g"+t);
    if (n >= 5) iL2 (s+"bd", n-3, "g"+t);
  }

  static void iL2 (String s, int n, String t) {
    if (n >= 2) iSol (s+"hh"+t);
    if (n >= 4) iL2 (s+"d", n-2, "g"+t);
    if (n >= 4) iL1 (s+"d", n-3, "gh"+t);
    if (n >= 4) iL3 (s+"hd", n-3, "g"+t);
  }

  static void iL3bis (String s, int n, String t) {
    if (n >= 9) iL3bis (s+"d", n-2, "g"+t);
    if (n >= 7) iL2 (s+"bd", n-3, "g"+t);
  }

  static void iSol (String s) { System.out.println (s); }

Question 9 Pour réduire la place mémoire, il suffit de passer un contour de taille n qu'on remplit graduellement pour le préfixe s et le suffixe t dans les fonctions précédents. Ainsi le programme suivant occuperait une taille 0(k n) (en fait O(n) mémoire nécessaire à tout moment):
    Contour m = new Contour (n);
    if (n >= 4) {m.v[0] = D; m.v[n-2] = G; m.v[n-1] = B; iL1(m, 1, n-2);}
    if (n >= 6) {m.v[0] = D; m.v[n-3] = G; m.v[n-2] = m.v[n-1] = B; iL2(m, 1, n-3);}
    if (n >= 8) {m.v[0] = D; m.v[n-2] = G; m.v[n-1] = B; iL3bis(m, 1, n-2);}
  }

  static void impL1 (Contour m, int i, int j) {
    if (j - i >= 1) {m.v[i] = H; impSol (m, i+1, j);}
    if (j - i >= 3) {m.v[i] = D; m.v[j-1] = G; impL1 (m, i+1, j-1);}
    if (j - i >= 5) {m.v[i] = D; m.v[j-2] = G; m.v[j-1] = B; impL2 (m, i+1, j-2);}
  }
  ...

Question 10 Vrai si P a une surface 1. Soit Pn+1 de surface n+1 (n > 1) qui a été obtenu à partir d'un Pn en rajoutant un carré élémentaire. On raisonne sur le nombre de cotés communs entre Pn et Pn+1 et à chaque fois on indique le changement dans le contour et on vérifie l'invariance de la somme:

Cas avec 1 coté commun:
hhh devient hdhgh. Alors on a bien 0 = -1 + 1 + 1 - 1
dhh devient ddhgh. Alors on a bien 1 = 0 + 1 + 1 - 1
hhg devient hdhgg. Alors on a bien 1 = -1 + 1 + 1 + 0
dhg devient ddhgg. Alors on a bien 2 = 0 + 1 + 1 + 0

Cas avec 2 cotés communs:
hghg devient hhgg. Alors on a bien 1 = 0 + 1 + 0
hghh devient hhgh. Alors on a bien 0 = 0 + 1 + -1
gghg devient ghgg. Alors on a bien 0 = 0 + 1 + -1
gghh devient ghgh. Alors on a bien -1 = -1 + 1 + -1

Cas avec 3 cotés communs:
hghdh devient hhh. Alors on a bien 1 + -1 + -1 + 1 = 0
gghdh devient ghh. Alors on a bien 0 + -1 + -1 + 1 = -1
gghdd devient ghd. Alors on a bien 0 + -1 + -1 + 0 = -1+ -1
hghdd devient hhd. Alors on a bien 1 + -1 + -1 + 0 = 0 + -1
Question 11 La fonction précédente prend un temps O(n) où n est la longueur de u.
    int s = 0;
    for (Liste a = u.ch; a != null; a = a.suivant)
      if (a.suivant != null) {
 int delta = a.suivant.val - a.val;
 if (delta == -3) delta = 1;
 else if (delta == 3) delta = -1;
 s = s + delta;
      }
    return 3 <= s && s <= 5;
  }

Question 12 
    Random rand = new Random();
    Liste a = null, b;
    do {
      int d; do { d = rand.nextInt(4); }
      while (a != null && Math.abs (d - a.val) == 2);
      a = new Liste(d, a);
      b = a; a = eliminerCycle(a);
    } while (a != null);
    return mettreEnOrdreDirect(new ContourE(b));
  }

  static Liste eliminerCycle (Liste a) {
    int[ ] dx = {1, 0, -1, 0}, dy = {0, 1, 0, -1};
    int x = 0, y = 0; Liste b = a;
    do {
      x = x + dx[b.val]; y = y + dy[b.val];
      b = b.suivant;
    } while (b != null && (x != 0 || y != 0)) ;
    if (x == 0 && y == 0) a = b;
    return a;
  }

  static ContourE mettreEnOrdreDirect (ContourE u) {
    if (! estEnOrdreDirect(u))
      u.ch = Liste.miroir (u.ch);
    return u;
  }

Question 13 La fonction suivante prend un temps O(N2).
    int n = r.noir[0].length;
    for (int i = 0; i < n; ++i)
      for (int j = 0; j < n; ++j)
 if (r.noir[i][j]) return new Paire (i, j);
    return null;
  }

Question 14 On considère le graphe dont noir est la matrice d'adjacence. On fait dfs sur l'extérieur toujours non vide (on rajoute virtuellement une bande d'unité un autour de la région); puis on va chercher un point dans le polyomino; on fait dfs sur le polyomino. Si enfin, on a visité tous les points de la région, la région contient bien un seul polyomino simplement connexe. Au total cette fonction est en O(N2).
    Paire p = trouverUnPoint(r);
    if (p == null) return false;
    else {
      int n = r.noir.length;
      boolean[ ][ ] vu = new boolean[n+2][n+2];
      int nNoirs = dfs (r, p.x, p.y, 0, vu);
      int nBlancs = dfs1 (r, -1, -1, 0, vu);
      return nNoirs + nBlancs == n * n + 4*(n + 1) ;
    }
  }

  static int dfs (Region r, int i, int j, int nc, boolean[ ][ ] vu) {
    int n = vu.length - 2;
    if (r.noir[i][j] && ! vu[i+1][j+1]) {
      vu[i+1][j+1] = true; ++nc;
      if (i > 0) nc = dfs (r, i-1, j, nc, vu);
      if (j > 0) nc = dfs (r, i, j-1, nc, vu);
      if (i < n-1) nc = dfs (r, i+1, j, nc, vu);
      if (j < n-1) nc = dfs (r, i, j+1, nc, vu);
    }
    return nc;
  }

  static int dfs1 (Region r, int i, int j, int nc, boolean[ ][ ] vuExt) {
    int n = vuExt.length - 2;
    if (-1 <= i && i <= n && -1 <= j && j <= n)
      if ((i == -1 || j == -1 || i == n || j == n ||
   !r.noir[i][j]) && ! vuExt[i+1][j+1]) {
 vuExt[i+1][j+1] = true; ++nc;
 for (int a = -1; a <= 1; ++a)
   for (int b = -1; b <= 1; ++b)
     nc = dfs1 (r, i+a, j+b, nc, vuExt);
      }
    return nc;
  }

Question 15 On considère à nouveau le graphe dont noir est la matrice d'adjacence. On explore les sommets du graphe par ordre lexicographique à partir du point (-1, -1) dans la bande extérieure blanche. Si le premier point non marqué est blanc, on marque sa composante 8-connexe (non vide); si c'est un point noir, on marque sa composante 4-connexe, et si celle-ci ne touche pas de sommet blanc non marqué, c'est un nouveau polyomino simplement connexe; et on recommence sur le premier non marqué.
    int n = r.noir.length;
    boolean[ ][ ] vu = new boolean[n+2][n+2];
    int nPols = 0;
    for (int i = -1; i <= n; ++i)
      for (int j = -1; j <= n; ++j)
 if (!vu[i+1][j+1]) {
   if (i == -1 || j == -1 || i == n || j == n || !r.noir[i][j])
     dfsBlanc (r, i, j, vu);
   else
     if (estSC (r, i, j, vu))
       ++nPols;
 }
    return nPols;
  }

  static boolean estSC (Region r, int i, int j, boolean[ ][ ] vu) {
    int n = vu.length - 2;
    boolean res = vu[i+1][j+1] || r.noir[i][j];
    if (r.noir[i][j] && ! vu[i+1][j+1]) {
      vu[i+1][j+1] = true;
      if (i > 0) res = estSC (r, i-1, j, vu) && res;
      if (j > 0) res = estSC (r, i, j-1, vu) && res;
      if (i < n-1) res = estSC (r, i+1, j, vu) && res;
      if (j < n-1) res = estSC (r, i, j+1, vu) && res;
    }
    return res;
  }

  static void dfsBlanc (Region r, int i, int j, boolean[ ][ ] vu) {
    int n = vu.length - 2;
    if (-1 <= i && i <= n && -1 <= j && j <= n)
      if ((i == -1 || j == -1 || i == n || j == n || !r.noir[i][j])
         && ! vu[i+1][j+1]) {
 vu[i+1][j+1] = true;
 for (int a = -1; a <= 1; ++a)
   for (int b = -1; b <= 1; ++b)
     dfsBlanc (r, i+a, j+b, vu);
  }  }


1

avec les participations de Luc Maranget, Didier Rémy et Abhishek Thakur

This document was translated from LATEX by HEVEA.