TL;DR
- Le Problème: Optimisation de la complexité temporelle et spatiale des structures de données.
- L'Approche: Problème style CTCI 7.10 pour débutants: conçois un Minesweeper texte avec Cell et Board, placement aléatoire des bombes, comptes de voisins, règles de clic et flood fill à l'ouverture d'un zéro.
- Complexité: Compromis optimal entre temps et mémoire avec gestion des cas limites.
Conçois un Minesweeper en texte. Un plateau N x N cache B bombes. Les autres cases portent un nombre (combien de bombes parmi les huit voisines) ou zéro (vide). Clic sur une bombe et tu perds. Clic sur un nombre et seule cette case s'ouvre. Clic sur un zéro et le plateau s'étend: ce vide, chaque vide connecté, et l'anneau de nombres autour de cette région s'ouvrent. Pose un drapeau sur les cases que tu crois bombes pour ne pas cliquer par erreur. Tu gagnes quand toutes les cases non-bombe sont exposées.
Ce billet est un enseignement original pour débutants en Java. Même famille de conception orientée objet de jeux en entretien, pas une copie de livre. Fait partie de la série CTCI en Java. Chapitre 7, conception orientée objet: Board, Cell, placer les bombes, clic avec flood fill sur les zéros.
1. Analogie du quotidien
Imagine une grille de papier couverte de post-it. Sous certains, une punaise (une bombe). Sous les autres, un petit compte de punaises proches, ou rien.
Tu enlèves un post-it:
- Punaise dessous: partie terminée.
- Un nombre: seul ce post-it part.
- Vide: tu continues d'enlever chaque post-it vide qui touche celui-ci, et tu enlèves aussi les post-it numérotés au bord de cette zone vide. Tu t'arrêtes aux nombres; tu ne traverses pas un nombre vers le vide suivant sauf si ce vide était déjà relié par des zéros.
Cette expansion du vide est un flood fill. Le plateau est une grille 2D de cellules. L'objet jeu suit si tu joues encore, si tu as déjà perdu, ou déjà gagné.
2. Problème en mots simples
But: classes et méthodes pour un Minesweeper texte jouable.
Pièces centrales:
| Pièce | Rôle |
|---|---|
Cell |
une case: bombe ou non, compte voisin, exposée?, drapeau? |
Board |
grille de cellules; place les bombes; calcule les nombres; flip / flood fill |
Game (optionnel mais propre) |
état, clic/drapeau utilisateur, win/lose, affichage |
Règles:
- La grille est
N x Navec exactementBbombes (ourows x colssi tu généralises). - Le nombre d'une case non-bombe est le nombre de voisines bombes parmi 8 au plus.
- Clic gauche (découvrir):
- Bombe → perte (on l'expose; la partie finit).
- Nombre > 0 → n'expose que cette case.
- Nombre 0 → flood fill des zéros et inclut les nombres du bord.
- Clic droit (drapeau): bascule un drapeau sur une case cachée. Les cases à drapeau ne se découvrent pas au clic.
- Victoire quand chaque case non-bombe est exposée. Les drapeaux n'ont pas besoin de coller aux bombes pour gagner (règle classique: seules les cases sûres doivent être ouvertes).
À clarifier en entretien:
- Le premier clic peut-il être garanti sûr? (Bonne règle produit; pas exigée par l'énoncé classique.)
- Placement des bombes: cases aléatoires distinctes, ou liste fixe pour les tests?
- Que affiche l'UI texte pour caché, drapeau, bombe, vide, nombre?
- Clics hors limites: ignorer ou lever une exception?
Forme des signatures:
class Cell { /* bomb, number, exposed, flagged */ }
class Board {
Board(int n, int bombCount);
void placeBombs(/* random or seed */);
void setNumbers();
// returns true if the click was safe (not a bomb), false if bomb hit
boolean flipCell(int r, int c);
void toggleFlag(int r, int c);
boolean allNonBombsExposed();
// optional: print for debugging
void print(boolean revealAll);
}
3. Réfléchir d'abord
Les classes d'abord, les pixels ensuite
Les interviewers veulent de la structure plus qu'une UI soignée.
Cell: données d'une case. Logique légère (getters/setters, peut-êtreisBlank()).Board: possède le tableau 2D, placement des bombes, comptes de voisins, uncover + flood fill.Game: boucle, saisie, messages win/lose. Fine couche surBoard.
Tu peux fusionner Game dans Board pour un entretien court. Préfère la séparation si le temps le permet.
Place les bombes, puis compte les voisins
L'ordre compte:
- Crée toutes les cellules non-bombe, nombre 0, cachées, sans drapeau.
- Place
Bbombes sur des cases aléatoires distinctes. - Pour chaque case non-bombe, compte les voisines bombes dans les 8 directions et stocke ce compte.
Si tu comptes d'abord et places les bombes ensuite, chaque nombre est faux.
directions (dr, dc):
(-1,-1) (-1,0) (-1,1)
( 0,-1) ( 0,1)
( 1,-1) ( 1,0) ( 1,1)
Ignore les coordonnées hors plateau. Un coin a 3 voisins, un bord 5, une case intérieure 8.
Règles de clic et flood fill
flip(r, c):
if out of bounds or already exposed or flagged: return (no-op / still safe)
if cell is bomb: expose it; return false (lose)
// safe cell
flood from (r, c) using BFS or DFS
return true
Flood fill (esquisse BFS):
- Démarre une file avec
(r, c). - Tant que la file n'est pas vide, prends une cellule:
- Si déjà exposée, saute.
- Expose-la.
- Si son nombre est strictement positif, n'étends pas plus loin depuis cette case (c'est le bord de la région vide).
- Si son nombre est 0, enfile les 8 voisines dans les bornes encore cachées, sans drapeau (et non bombes; les zéros ne sont pas sur des bombes si les nombres sont corrects).
Cela colle au Minesweeper classique: les nombres du bord s'ouvrent, mais le flood ne creuse pas à travers eux vers des régions de zéros déconnectées.
Pourquoi ne pas ouvrir seulement le zéro cliqué?
Si tu n'ouvres qu'un vide, le jeu paraît cassé. Les joueurs attendent la cascade. Les interviewers veulent aussi entendre "BFS/DFS flood fill" comme algorithme des vides connectés.
Drapeaux
Les drapeaux sont un verrou d'UI:
- Bascule sur une case cachée.
- Une case à drapeau ignore les clics de découverte.
- Le drapeau ne change ni le plan des bombes ni les nombres voisins.
- Il faut enlever le drapeau avant de pouvoir ouvrir la case.
Condition de victoire
Après un flip réussi:
- Compte les cases non-bombe exposées, ou suis
remaining = N*N - Bet décrémente à chaque exposition. - Quand remaining atteint 0, l'état devient WON.
- Toucher une bombe met LOST tout de suite.
Un compteur donne des checks de victoire en O(1). Scanner la grille après chaque clic est O(N²) et convient aux petits plateaux.
Ce qu'il ne faut pas surconstruire
- Pas besoin d'un framework graphique complet.
- Pas besoin de multijoueur ni de timers sauf demande.
- Des hiérarchies "NumberCell vs BombCell" paient rarement; un simple
boolean isBombplusint adjacentsuffit.
4. Solution Java
Cell
class Cell {
private final int row;
private final int col;
private boolean bomb;
private boolean exposed;
private boolean flagged;
private int adjacentBombs; // 0..8 for non-bombs; unused or 0 for bombs
Cell(int row, int col) {
this.row = row;
this.col = col;
}
int getRow() { return row; }
int getCol() { return col; }
boolean isBomb() { return bomb; }
void setBomb(boolean bomb) { this.bomb = bomb; }
boolean isExposed() { return exposed; }
void setExposed(boolean exposed) { this.exposed = exposed; }
boolean isFlagged() { return flagged; }
void setFlagged(boolean flagged) { this.flagged = flagged; }
int getAdjacentBombs() { return adjacentBombs; }
void setAdjacentBombs(int n) { this.adjacentBombs = n; }
boolean isBlank() {
return !bomb && adjacentBombs == 0;
}
}
Board: construire, placer les bombes, fixer les nombres
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
class Board {
private static final int[][] DIRS = {
{-1, -1}, {-1, 0}, {-1, 1},
{0, -1}, {0, 1},
{1, -1}, {1, 0}, {1, 1}
};
private final int n;
private final int bombCount;
private final Cell[][] grid;
private int unexposedSafe; // non-bomb cells still hidden
private boolean exploded;
Board(int n, int bombCount, long seed) {
if (n <= 0) {
throw new IllegalArgumentException("n must be positive");
}
if (bombCount < 0 || bombCount > n * n) {
throw new IllegalArgumentException("invalid bombCount");
}
this.n = n;
this.bombCount = bombCount;
this.grid = new Cell[n][n];
for (int r = 0; r < n; r++) {
for (int c = 0; c < n; c++) {
grid[r][c] = new Cell(r, c);
}
}
placeBombs(seed);
setNumbers();
this.unexposedSafe = n * n - bombCount;
this.exploded = false;
}
private void placeBombs(long seed) {
Random rng = new Random(seed);
Set<Integer> used = new HashSet<>();
while (used.size() < bombCount) {
int idx = rng.nextInt(n * n);
if (!used.add(idx)) {
continue;
}
int r = idx / n;
int c = idx % n;
grid[r][c].setBomb(true);
}
}
private void setNumbers() {
for (int r = 0; r < n; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c].isBomb()) {
continue;
}
int count = 0;
for (int[] d : DIRS) {
int nr = r + d[0];
int nc = c + d[1];
if (inBounds(nr, nc) && grid[nr][nc].isBomb()) {
count++;
}
}
grid[r][c].setAdjacentBombs(count);
}
}
}
private boolean inBounds(int r, int c) {
return r >= 0 && r < n && c >= 0 && c < n;
}
Cell getCell(int r, int c) {
return grid[r][c];
}
boolean hasExploded() {
return exploded;
}
boolean isWon() {
return !exploded && unexposedSafe == 0;
}
}
Board: drapeau et clic avec flood fill
/** Toggle flag on a hidden cell. No-op if already exposed. */
void toggleFlag(int r, int c) {
if (!inBounds(r, c) || exploded || isWon()) {
return;
}
Cell cell = grid[r][c];
if (cell.isExposed()) {
return;
}
cell.setFlagged(!cell.isFlagged());
}
/**
* Uncover cell (r, c). Returns false if a bomb was hit.
* Returns true if the click was safe or ignored (flagged / already open).
*/
boolean flipCell(int r, int c) {
if (!inBounds(r, c) || exploded || isWon()) {
return !exploded;
}
Cell start = grid[r][c];
if (start.isExposed() || start.isFlagged()) {
return true;
}
if (start.isBomb()) {
start.setExposed(true);
exploded = true;
return false;
}
// BFS flood fill for zeros; expose bordering numbers
Queue<Cell> q = new ArrayDeque<>();
q.add(start);
while (!q.isEmpty()) {
Cell cur = q.poll();
if (cur.isExposed() || cur.isFlagged() || cur.isBomb()) {
continue;
}
cur.setExposed(true);
unexposedSafe--;
if (cur.getAdjacentBombs() > 0) {
// number cell: stop expanding through it
continue;
}
// blank (zero): expand to all neighbors
int cr = cur.getRow();
int cc = cur.getCol();
for (int[] d : DIRS) {
int nr = cr + d[0];
int nc = cc + d[1];
if (!inBounds(nr, nc)) {
continue;
}
Cell next = grid[nr][nc];
if (!next.isExposed() && !next.isFlagged() && !next.isBomb()) {
q.add(next);
}
}
}
return true;
}
Affichage texte (aide optionnelle)
/**
* Text view. If revealAll is true, show bombs and numbers regardless of exposed.
* Hidden: '.', flagged: 'F', bomb: '*', blank: ' ', number: digit char.
*/
void print(boolean revealAll) {
for (int r = 0; r < n; r++) {
StringBuilder line = new StringBuilder();
for (int c = 0; c < n; c++) {
Cell cell = grid[r][c];
char ch;
if (!revealAll && cell.isFlagged() && !cell.isExposed()) {
ch = 'F';
} else if (!revealAll && !cell.isExposed()) {
ch = '.';
} else if (cell.isBomb()) {
ch = '*';
} else if (cell.getAdjacentBombs() == 0) {
ch = ' ';
} else {
ch = (char) ('0' + cell.getAdjacentBombs());
}
line.append(ch).append(' ');
}
System.out.println(line.toString().trim());
}
}
Petit parcours
Suppose un plateau 3x3 avec une bombe en (1,1). Les nombres autour sont tous 1. Le centre est *. Coins et bords qui touchent la bombe montrent 1. Il n'y a pas de région de zéros, donc chaque clic sûr ouvre exactement une case. Huit clics sûrs gagnent; clic au centre perd.
Maintenant un plateau 5x5 avec des bombes seulement près des coins, de sorte que le milieu est une mer de zéros. Clic au centre:
- Le centre est 0 → enfile les voisins.
- Chaque 0 voisin s'expose et s'étend.
- Les cases numérotées au bord de ce lac de zéros s'exposent une fois et arrêtent le flood.
- Un clic peut ouvrir des dizaines de cases. C'est la fonctionnalité que les interviewers veulent bien implémentée.
Tests déterministes sans UI complète
// fixed seed so bomb layout is stable in unit tests
Board board = new Board(5, 3, 42L);
assert board.flipCell(2, 2); // hope safe; adjust seed if needed
board.toggleFlag(0, 0);
assert !board.hasExploded();
// after enough safe flips:
// assert board.isWon();
Pour une démo d'entretien, fixe les positions de bombes au lieu du hasard si tu veux un scénario stable.
// alternative for demos: placeBombsFromList(List of [r,c])
5. Table de complexité
| Opération | Temps | Notes d'espace |
|---|---|---|
| Construire la grille | O(N²) | N² cellules |
| Placer B bombes | O(B) espéré avec set; pire O(N²) de retries si dense | set d'indices |
| setNumbers | O(N²) | 8 checks de voisin par case |
| flipCell sur un nombre | O(1) | expose une case |
| flipCell sur une grande région de zéros | O(K) pour K cases ouvertes (jusqu'à O(N²)) | file BFS O(K) |
| toggleFlag | O(1) | |
| Victoire avec compteur | O(1) | unexposedSafe |
| Victoire par parcours complet | O(N²) | sans champ extra |
Pour les tailles d'entretien (N entre 8 et 30), tout ce qui précède convient. Donne la borne du flood fill comme "travail proportionnel aux cases révélées."
6. Cas limites et erreurs fréquentes
Les interviewers touchent ceux-ci:
- N = 1, B = 1: la seule case est une bombe. Le premier clic perd. Gagner est impossible sans règle spéciale.
- N = 1, B = 0: un vide. Un clic et tu gagnes.
- B = 0: tout le plateau est des zéros. Un clic flood-fill tout et gagne.
- B = N²: chaque case est une bombe. Aucun clic sûr.
- Clic hors limites: ignore ou lève; fixe le contrat.
- Clic sur case à drapeau: pas de découverte (il faut d'abord enlever le drapeau).
- Clic déjà exposé: no-op, toujours sûr.
- Double exposition en BFS: protège avec
isExposedpour ne pas décrémenterunexposedSafedeux fois. - Flood à travers les nombres: faux. Les nombres s'ouvrent mais n'enfilent pas leurs voisins.
- Flood vers les bombes: n'enfile jamais de cellules bombe. Vérifie quand même
!isBomb()par sécurité.
Erreurs fréquentes:
- Compter les voisins avant de placer les bombes.
- Seulement 4 directions au lieu de 8 (Minesweeper utilise les diagonales).
- N'ouvrir que le zéro cliqué sans flood fill.
- S'étendre au-delà des nombres vers des régions non liées.
- Oublier que les drapeaux bloquent les clics.
- Gagner quand toutes les bombes sont flaguées au lieu de quand toutes les non-bombes sont ouvertes (la victoire classique est l'exposition des cases sûres).
- Décalage d'un sur
unexposedSafeen revisistant une case dans la file. - Afficher les bombes au joueur pendant la partie (vue debug vs vue joueur).
Idée minimale de smoke:
Board empty = new Board(3, 0, 1L);
assert empty.flipCell(1, 1);
assert empty.isWon(); // all zeros opened in one flood
Board one = new Board(1, 1, 1L);
assert !one.flipCell(0, 0);
assert one.hasExploded();
7. Résumé à expliquer à un ami
Minesweeper comme problème d'OOD, c'est trois idées:
Cellporte bombe, nombre, exposé, drapeau pour une case.BoardplaceBbombes, puis écrit les comptes de voisins sur 8 directions.- Clic soit perd sur une bombe, soit ouvre un nombre, soit fait un flood fill BFS/DFS d'une région de zéros et de son bord numéroté. Les drapeaux ne bloquent que les clics accidentels. Tu gagnes quand chaque case non-bombe est exposée.
Si tu peux dessiner un petit plateau, placer des bombes, remplir les nombres et dérouler un flood fill à la main, tu maîtrises le problème 7.10. Les interviewers veulent que tes classes collent aux noms du jeu et que l'expansion des zéros soit une vraie recherche sur la grille, pas un vague "ouvrir les cases proches."
Série
- Guide: Guide de la série CTCI
- Précédent: Circular Array
- Suivant: File System
