Retour à l'accueil

Générateur Sudoku JS : bijection et factorielles

L'article décrit l'évolution de l'algorithme de génération de grilles Sudoku en JavaScript des échanges simples à une approche bijective avec système numérique factoriel. Garantit 609 492 049 920 grilles valides uniques à partir d'un modèle de base sans collisions.

Générateur Sudoku bijectif : 609 milliards de grilles uniques
Advertisement 728x90

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.

Google AdInline article slot
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.

Google AdInline article slot

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 :

Google AdInline article slot
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

Advertisement 728x90

Lire ensuite