TL;DR
- Le Problème: Optimisation de la complexité temporelle et spatiale des structures de données.
- L'Approche: Problème style CTCI 6.9 pour débutants: 100 casiers fermés, 100 personnes basculent chaque i-ème porte. Les ouverts sont les carrés parfaits (1, 4, 9, ..., 100) car seuls les carrés ont un nombre impair de facteurs. Simulation Java optionnelle.
- Complexité: Compromis optimal entre temps et mémoire avec gestion des cas limites.
Il y a 100 casiers dans un couloir, tous fermés. 100 personnes passent. La personne i bascule chaque casier multiple de i: la personne 1 touche tout, la 2 touche 2, 4, 6, ..., la 100 ne touche que le 100. Quand tout le monde a fini, quels casiers sont ouverts ?
Tu peux simuler tout le couloir avec des boucles. Ça marche, et en entretien on peut te demander le code. La vraie réponse est plus nette: seuls les casiers carrés parfaits restent ouverts (1, 4, 9, 16, 25, 36, 49, 64, 81, 100). Chaque casier commence fermé et bascule une fois par diviseur. Seuls les carrés ont un nombre impair de diviseurs, donc seuls eux finissent ouverts.
Ce billet est un enseignement original pour débutants, avec Java optionnel pour simuler les bascules. Même famille de puzzles d'entretien, pas une copie de livre. Fait partie de la série CTCI en Java. Chapitre 6, maths et logique, problème 6.9.
1. Analogie du quotidien
Imagine un couloir d'école avec 100 casiers en métal. Chaque porte commence fermée.
Une file d'élèves passe. L'élève 1 ouvre ou ferme chaque porte (donc toutes s'ouvrent). L'élève 2 touche une porte sur deux (en ferme la moitié). L'élève 3 touche une sur trois, et ainsi de suite jusqu'à l'élève 100, qui ne touche que le casier 100.
Pas besoin de suivre chaque élève. Change de question: combien de fois le casier k est-il touché ? Une fois pour chaque nombre qui divise k. Le 12 est touché par 1, 2, 3, 4, 6 et 12: six fois. Six est pair, il finit fermé (départ fermé, un nombre pair de bascules le laisse fermé). Le 16 est touché par 1, 2, 4, 8 et 16: cinq fois. Impair, il finit ouvert.
Les comptages impairs n'apparaissent que lorsqu'un facteur se "paire avec lui-même": les carrés parfaits.
2. Énoncé en mots simples
Mise en place:
- 100 casiers, numérotés de 1 à 100.
- Tous commencent fermés.
- 100 personnes, numérotées de 1 à 100.
- La personne
ibascule les casiersi, 2i, 3i, ...(chaque multiple deiau plus égal à 100). - Basculer: fermé devient ouvert, ouvert devient fermé.
Objectif: après la personne 100, lister (ou compter) les casiers ouverts.
Hypothèses à énoncer en entretien:
- Casiers et personnes sont 1-based, 1..100.
- Exactement un passage par personne, dans l'ordre (l'ordre ne change pas l'état final; chaque casier bascule une fois par diviseur).
- Aucune autre opération entre les passages.
Forme de signature si tu codes un simulateur:
// returns true if locker is open after the full process (1-based indices in comments)
boolean[] openLockers(int n);
Ou juste renvoyer les indices ouverts:
// simulate n lockers / n people; return list of open locker numbers (1-based)
List<Integer> openAfterProcess(int n);
Petit aperçu numérique (n = 10):
| Casier | Diviseurs (qui bascule) | Compte | Final (départ fermé) |
|---|---|---|---|
| 1 | 1 | 1 impair | ouvert |
| 2 | 1, 2 | 2 pair | fermé |
| 3 | 1, 3 | 2 pair | fermé |
| 4 | 1, 2, 4 | 3 impair | ouvert |
| 5 | 1, 5 | 2 pair | fermé |
| 6 | 1, 2, 3, 6 | 4 pair | fermé |
| 7 | 1, 7 | 2 pair | fermé |
| 8 | 1, 2, 4, 8 | 4 pair | fermé |
| 9 | 1, 3, 9 | 3 impair | ouvert |
| 10 | 1, 2, 5, 10 | 4 pair | fermé |
Ouverts pour n = 10: 1, 4, 9. Pour n = 100: 1, 4, 9, ..., 100 (dix portes).
3. Réfléchis d'abord
Force brute d'abord
Deux boucles imbriquées:
lockers[1..n] = closed
for person p = 1..n:
for locker k = p, 2p, 3p, ... <= n:
toggle lockers[k]
C'est O(n²) dans une forme naïve; en réalité environ O(n log n) bascules au total car la personne p touche n/p portes. Suffisant pour n = 100. On veut quand même le pourquoi.
Qui bascule le casier k ?
La personne p touche le casier k seulement si p divise k. Donc le casier k bascule une fois par diviseur positif de k.
Départ fermé:
- Nombre pair de bascules → fermé
- Nombre impair de bascules → ouvert
Les ouverts sont exactement ceux qui ont un nombre impair de diviseurs.
Quand le nombre de diviseurs est-il impair ?
Les diviseurs vont souvent par paires: si d divise k, k/d aussi, et d ≠ k/d sauf si d² = k.
Exemple pour 12:
1 × 12
2 × 6
3 × 4
Six diviseurs distincts, trois paires.
Exemple pour 16:
1 × 16
2 × 8
4 × 4 // sqrt pairs with itself
Diviseurs: 1, 2, 4, 8, 16. Cinq valeurs. Le facteur du milieu ne compte qu'une fois.
Seuls les carrés parfaits ont un diviseur qui est la racine carrée "appariée à elle-même", donc seuls eux ont un compte impair.
Donc les casiers ouverts sont:
1², 2², 3², ..., floor(sqrt(n))²
Pour n = 100: 1, 4, 9, 16, 25, 36, 49, 64, 81, 100. Compte: 10.
Forme fermée pour le compte
Le nombre de casiers ouverts pour un n général est floor(sqrt(n)). Plus besoin de simuler une fois le théorème en poche.
Pourquoi c'est "maths et logique", pas de la trivia de code
N'importe qui écrit la double boucle. Le gain en entretien, c'est de relier parité des bascules à parité des diviseurs à carrés parfaits. Dis cette chaîne à voix haute avant de toucher le clavier.
Variantes qu'on ramène souvent
- Départ ouvert au lieu de fermé: inverse l'état final (ou redéfinis "ouvert"). Toujours préciser l'état initial.
- n n'est pas 100: même règle; ouverts = carrés jusqu'à n.
- Seulement le compte, pas la liste: la réponse est
floor(sqrt(n)). - "Quelles personnes laissent un casier ouvert ?" Toujours les indices carrés; les personnes ne "possèdent" pas l'état final, les bascules oui.
4. Solution Java (simulation)
Le raisonnement suffit. Le code prouve l'affirmation pour n = 100 et n général.
Simulation complète
import java.util.ArrayList;
import java.util.List;
/** Simulate n lockers / n people. Returns 1-based open locker numbers. */
static List<Integer> openAfterProcess(int n) {
boolean[] open = new boolean[n + 1]; // index 0 unused; false = closed
for (int person = 1; person <= n; person++) {
for (int locker = person; locker <= n; locker += person) {
open[locker] = !open[locker];
}
}
List<Integer> result = new ArrayList<>();
for (int k = 1; k <= n; k++) {
if (open[k]) {
result.add(k);
}
}
return result;
}
Réponse purement mathématique (à dire en premier)
/** Open lockers are perfect squares: 1, 4, 9, ..., floor(sqrt(n))^2. */
static List<Integer> openBySquares(int n) {
List<Integer> result = new ArrayList<>();
for (int i = 1; i * i <= n; i++) {
result.add(i * i);
}
return result;
}
Auto-vérification contre la simulation
static void verify(int n) {
List<Integer> sim = openAfterProcess(n);
List<Integer> math = openBySquares(n);
if (!sim.equals(math)) {
throw new AssertionError("mismatch for n=" + n + " sim=" + sim + " math=" + math);
}
System.out.println("ok n=" + n + " open=" + math + " count=" + math.size());
}
// verify(10); // [1, 4, 9]
// verify(100); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Compte sans liste
static int countOpen(int n) {
return (int) Math.floor(Math.sqrt(n));
// or integer loop: int c = 0; for (int i = 1; i * i <= n; i++) c++; return c;
}
Pour n = 100, floor(sqrt(100)) = 10.
Nombres travaillés pour le casier 36
Diviseurs de 36: 1, 2, 3, 4, 6, 9, 12, 18, 36. Ce sont 9 (impair).
start closed
after 1: open
after 2: closed
after 3: open
after 4: closed
after 6: open
after 9: closed
after 12: open
after 18: closed
after 36: open
Finit ouvert. 36 = 6².
Casier 50: diviseurs 1, 2, 5, 10, 25, 50. Six fois, pair, finit fermé.
5. Tableau de complexité
| Approche | Temps | Espace extra | Notes |
|---|---|---|---|
| Simulation double boucle | O(n log n) bascules | O(n) pour le tableau booléen | clair, bien en round de code |
Lister les carrés i*i <= n |
O(sqrt(n)) | O(sqrt(n)) pour la liste | optimal une fois l'idée connue |
Compte seul floor(sqrt(n)) |
O(1) avec Math.sqrt, ou O(sqrt(n)) en entiers |
O(1) | mieux s'ils demandent seulement "combien" |
| Factoriser chaque k et compter les diviseurs | O(n sqrt(n)) naïf | O(1) hors sortie | correct mais plus lent; enseigne la vue diviseurs |
Pour n = 100 tout est instantané. Pour un n énorme, préfère la liste de carrés ou le compte floor-sqrt.
6. Cas limites et erreurs fréquentes
Les interviewers piquent ici:
- n = 1: seul le casier 1, la personne 1 l'ouvre. Ouverts:
[1]. - n = 0 ou négatif: vide; rejette dans le code.
- Tableaux off-by-one: en Java ils sont 0-based; laisse l'index 0 inutilisé ou mappe avec soin.
- Départ ouvert: inverse la réponse. Confirme que l'énoncé dit fermés au départ.
- La personne i ne bascule-t-elle que le casier i ? Non: aussi les multiples. Certains oublient et ne touchent que
i. - sqrt flottant pour le compte:
Math.sqrtsuffit jusqu'aux carrés entiers exacts en double vers 2^53; pour unlongénorme préfère une recherche binaire entière du floor sqrt, ou un cast prudent. - "Tous les casiers touchés par la personne 1 restent ouverts à la fin" faux; plus tard beaucoup se ferment.
Erreurs courantes:
- Simuler seulement la personne 1 et la 100 et deviner des motifs sans diviseurs.
- Dire que les premiers restent ouverts (non: un premier a exactement deux diviseurs, compte pair, fermé).
- Inclure des non-carrés qui "semblent spéciaux" (puissances de deux, etc.).
- Compter 0 comme casier carré alors que les casiers vont de 1 à n.
- O(n²) avec
for k=1..n if k % p == 0alors quefor locker = p; locker <= n; locker += pest plus propre et plus rapide.
Idée minimale de fumée:
verify(1);
verify(10);
verify(100);
System.out.println(countOpen(100)); // 10
System.out.println(openBySquares(100));
// [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
7. Récap à expliquer à un ami
Cent casiers, tous fermés. Cent personnes. La personne i bascule chaque i-ème porte.
- Le casier k bascule une fois pour chaque diviseur de k.
- Départ fermé: bascules impaires → ouvert, paires → fermé.
- Les diviseurs se mettent par paires, sauf quand k est un carré parfait (la racine ne compte qu'une fois).
- Donc les ouverts sont 1, 4, 9, ..., 100. Il y en a 10 (
floor(sqrt(100))). - Le code peut simuler avec un tableau booléen, ou émettre
i*itant quei*i <= n.
Si tu peux dire "nombre impair de facteurs, seulement les carrés" sans dessiner toute la table, le problème 6.9 est à toi. Le chapitre 6 récompense ce style: un invariant bat un tas de détails de simulation.
Série
- Guide: Guide de la série CTCI
- Précédent: The Egg Drop Problem
- Suivant: Poison
