Générateur de Sudoku en JavaScript : des permutations aux bijections factorielles
Les développeurs de jeux et d'applications sont souvent confrontés au défi de générer des grilles de Sudoku uniques. Au lieu de s'attaquer au problème NP-difficile du backtracking, une approche plus efficace utilise des transformations géométriques sur une grille de base valide. L'algorithme a évolué de simples mélanges d'array vers une bijection mathématique capable de produire 609 492 049 920 configurations uniques à partir d'un seul modèle.
La grille de base contient chaque chiffre de 1 à 9 exactement une fois par ligne, colonne et bloc 3×3. Les transformations valides préservent cet invariant :
- Permutation des lignes à l'intérieur d'un bloc 3×3
- Permutation des colonnes à l'intérieur d'un bloc 3×3
- Permutation des blocs de lignes (3×9)
- Permutation des blocs de colonnes (9×3)
- Permutation globale des chiffres
Première itération : Mélange avec grain
La méthode initiale utilisait un grain hexadécimal de 18 caractères pour générer des bits déterminant la séquence d'échanges. L'algorithme Mulberry32 fournissait un PRNG déterministe.
const BASE_GRID = [
[5,3,4, 6,7,8, 9,1,2],
[6,7,2, 1,9,5, 3,4,8],
[1,9,8, 3,4,2, 5,6,7],
[8,5,9, 7,6,1, 4,2,3],
[4,2,6, 8,5,3, 7,9,1],
[7,1,3, 9,2,4, 8,5,6],
[9,6,1, 5,3,7, 2,8,4],
[2,8,7, 4,1,9, 6,3,5],
[3,4,5, 2,8,6, 1,7,9]
];
const Utils = {
seededRandom: (seed) => {
return () => {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
},
hexToBits: (hex) => {
return [...hex].flatMap(char => {
const v = parseInt(char, 16);
return [(v >> 3) & 1, (v >> 2) & 1, (v >> 1) & 1, v & 1];
});
},
transforms: {
swapRows: (g, r1, r2) => { [g[r1], g[r2]] = [g[r2], g[r1]]; },
swapCols: (g, c1, c2) => {
for (let r = 0; r < 9; r++) [g[r][c1], g[r][c2]] = [g[r][c2], g[r][c1]];
},
swapRowBlocks: (g, b1, b2) => {
for (let i = 0; i < 3; i++) Utils.transforms.swapRows(g, b1 * 3 + i, b2 * 3 + i);
},
swapColBlocks: (g, b1, b2) => {
for (let i = 0; i < 3; i++) Utils.transforms.swapCols(g, b1 * 3 + i, b2 * 3 + i);
}
}
};
Un tableau de 24 opérations d'échange était appliqué en trois passes, utilisant un masque de bits dérivé du grain. Problème : des motifs fixes de chiffres dans les blocs entraînaient une répétition visuelle.
Deuxième itération : Échanges globaux de chiffres
L’ajout de 8 transformations swapDigits(d1, d2) a éliminé ces motifs. La fonction parcourt toute la grille, échangeant toutes les occurrences d’un chiffre par un autre :
swapDigits: (g, d1, d2) => {
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
if (g[r][c] === d1) g[r][c] = d2;
else if (g[r][c] === d2) g[r][c] = d1;
}
}
}
Trois passes avec des décalages différents (0, 13, 29) modulo la longueur du tableau d’opérations ont assuré un comportement chaotique. Les opérations non commutatives garantissaient une diversité maximale.
Troisième itération : Bijection factorielle
La version finale a abandonné les mutations d’array au profit d’un calcul direct. Nombre total de transformations uniques :
- Permutation des 9 chiffres : 9! = 362 880
- Permutation des 3 blocs de lignes : 3! = 6
- Permutation des 3 blocs de colonnes : 3! = 6
- Permutations des lignes à l’intérieur de 3 blocs : (3!)^3 = 216
- Permutations des colonnes à l’intérieur de 3 blocs : (3!)^3 = 216
Total : 9! × 3! × 3! × (3!)^3 × (3!)^3 = 609 492 049 920
Un grain hexadécimal de 18 caractères est converti en BigInt, puis pris modulo MAX_PERMUTATIONS. Le nombre N est décomposé dans le système factoriel :
const Utils = {
getPermutation: (arr, k) => {
let available = [...arr];
let result = [];
let fact = 1;
for (let i = 2; i < available.length; i++) fact *= i;
for (let i = available.length - 1; i > 0; i--) {
const idx = Math.floor(k / fact);
result.push(available[idx]);
available.splice(idx, 1);
k %= fact;
fact /= i;
}
result.push(available[0]);
return result;
}
};
// Dans generate(seedStr) :
const MAX_PERMUTATIONS = 609492049920n;
const seedBigInt = BigInt("0x" + clean);
let N = Number(seedBigInt % MAX_PERMUTATIONS);
const pDigits = Utils.getPermutation([1,2,3,4,5,6,7,8,9], N % 362880); N = Math.floor(N / 362880);
// ... permutations restantes des blocs et lignes
Pour chaque cellule [r][c], les coordonnées originales sont calculées via les indices de permutation des blocs et intra-blocs. La valeur est extraite de BASE_GRID[oldR][oldC] puis mappée via pDigits.
Avantages clés
- Déterminisme : un même grain produit toujours une grille unique
- Efficacité : O(N) sans mutation d’array — idéal pour la génération en temps réel
- Unicité : 6×10¹¹ variantes évitent les collisions avec des grains de 18 caractères
- Validité : toutes les transformations préservent les invariants du Sudoku
- Portabilité : la grille est entièrement déterminée par le grain — parfait pour le partage entre joueurs
— Editorial Team
Aucun commentaire pour le moment.