Implement Algorithm X for solving and generating sudokus
This commit is contained in:
parent
7ac3e6ad06
commit
8f3217f3cc
@ -15,20 +15,11 @@ export const typeDefs = gql`
|
||||
"The height of each region."
|
||||
regionHeight: Int!
|
||||
|
||||
"The total number of cells in the board."
|
||||
cells: Int!
|
||||
|
||||
"The 'raw' board, an array of cells in region-first order."
|
||||
raw: [Cell!]!
|
||||
"The number of cells in the board."
|
||||
size: Int!
|
||||
|
||||
"The rows of the board, from top to bottom."
|
||||
rows: [[Cell!]!]!
|
||||
|
||||
"The columns of the board, from left to right."
|
||||
columns: [[Cell!]!]!
|
||||
|
||||
"The regions of the board, book-ordered."
|
||||
regions: [[Cell!]!]!
|
||||
cells: [[Cell!]!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
195
src/sudoku/dlx.ts
Normal file
195
src/sudoku/dlx.ts
Normal file
@ -0,0 +1,195 @@
|
||||
export class DNode {
|
||||
up: DNode;
|
||||
down: DNode;
|
||||
left: DNode;
|
||||
right: DNode;
|
||||
column: CNode;
|
||||
meta: any;
|
||||
|
||||
constructor(column: CNode) {
|
||||
this.up = this.down = this.left = this.right = this;
|
||||
this.column = column;
|
||||
}
|
||||
}
|
||||
|
||||
export class CNode extends DNode {
|
||||
left: CNode;
|
||||
right: CNode;
|
||||
|
||||
size: number;
|
||||
name: string | undefined;
|
||||
|
||||
constructor(name = undefined) {
|
||||
super(null!);
|
||||
this.column = this;
|
||||
|
||||
this.size = 0;
|
||||
this.name = name;
|
||||
this.left = this.right = this;
|
||||
}
|
||||
}
|
||||
|
||||
type SolutionCallback = (output: DNode[]) => boolean;
|
||||
type ColumnSelector = (header: CNode) => CNode;
|
||||
|
||||
function selectColumnSizeHeuristic(header: CNode): CNode {
|
||||
let minSize = Infinity;
|
||||
let minColumn: CNode | undefined;
|
||||
let curColumn = header;
|
||||
while ((curColumn = curColumn.right) !== header) {
|
||||
if (curColumn.column.size < minSize) {
|
||||
minSize = curColumn.column.size;
|
||||
minColumn = curColumn;
|
||||
}
|
||||
}
|
||||
if (!minColumn) {
|
||||
throw new Error("minColumn is undefined, this shouldn't be possible.");
|
||||
}
|
||||
return minColumn;
|
||||
}
|
||||
|
||||
export function linkNodesLR(nodes: DNode[]) {
|
||||
// given an array of nodes, link them together left-to-right circularly
|
||||
let last = nodes[0];
|
||||
for (let j = 1; j < nodes.length; j++) {
|
||||
const node = nodes[j];
|
||||
last.right = node;
|
||||
node.left = last;
|
||||
last = node;
|
||||
}
|
||||
|
||||
nodes[0].left = last;
|
||||
last.right = nodes[0];
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function addNodeToColumn(column: CNode, meta: any) {
|
||||
column.size++;
|
||||
|
||||
let node = new DNode(column);
|
||||
node.down = column; // new last node, points down to column
|
||||
node.up = column.up; // new last node points up to previous last node
|
||||
|
||||
// previous last node points down and column points up to new node,
|
||||
// repairing the circle
|
||||
column.up.down = column.up = node;
|
||||
|
||||
node.meta = meta;
|
||||
return node;
|
||||
}
|
||||
|
||||
export function maskRow(node: DNode) {
|
||||
let row = node;
|
||||
do {
|
||||
row.column.size--;
|
||||
row.down.up = row.up;
|
||||
row.up.down = row.down;
|
||||
} while ((row = row.right) !== node);
|
||||
}
|
||||
|
||||
export function unmaskRow(node: DNode) {
|
||||
let row = node;
|
||||
do {
|
||||
row.column.size++;
|
||||
row.down.up = row.up.down = row;
|
||||
} while ((row = row.right) !== node);
|
||||
}
|
||||
|
||||
export class DLX {
|
||||
header: CNode;
|
||||
updates: number = 0;
|
||||
callback: SolutionCallback;
|
||||
columnSelector: ColumnSelector;
|
||||
|
||||
constructor(
|
||||
header: CNode,
|
||||
callback: SolutionCallback = () => false,
|
||||
columnSelector: ColumnSelector = selectColumnSizeHeuristic
|
||||
) {
|
||||
this.header = header;
|
||||
this.callback = callback;
|
||||
this.columnSelector = columnSelector;
|
||||
}
|
||||
|
||||
cover(c: CNode) {
|
||||
// remove c from column header list
|
||||
c.right.left = c.left;
|
||||
c.left.right = c.right;
|
||||
this.updates++;
|
||||
|
||||
let row = c as DNode;
|
||||
while ((row = row.down) !== c) {
|
||||
// traverse DOWN the rows this column contained
|
||||
let col = row;
|
||||
while ((col = col.right) !== row) {
|
||||
// traverse the columns of this row (to the RIGHT)
|
||||
|
||||
// remove this node from its column, and shrink its column's size
|
||||
this.updates++;
|
||||
col.up.down = col.down;
|
||||
col.down.up = col.up;
|
||||
col.column.size--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uncover(c: CNode) {
|
||||
let row = c as DNode;
|
||||
while ((row = row.up) !== c) {
|
||||
// traverse UP the rows this column contained
|
||||
let col = row;
|
||||
while ((col = col.left) !== row) {
|
||||
// traverse the columns of this row (to the LEFT)
|
||||
|
||||
// do the inverse of cover()
|
||||
this.updates++;
|
||||
col.up.down = col.down.up = col;
|
||||
col.column.size++;
|
||||
}
|
||||
}
|
||||
// insert c back into column header list
|
||||
this.updates++;
|
||||
c.left.right = c.right.left = c;
|
||||
}
|
||||
|
||||
solution: DNode[] = [];
|
||||
|
||||
_search(k: number): boolean {
|
||||
if (this.header.right === this.header) {
|
||||
return this.callback(this.solution);
|
||||
}
|
||||
|
||||
const column = this.columnSelector(this.header);
|
||||
this.cover(column);
|
||||
|
||||
let complete = false;
|
||||
let row = column as DNode;
|
||||
while ((row = row.down) !== column) {
|
||||
// add this row to the partial solution
|
||||
this.solution[k] = row;
|
||||
|
||||
// cover the columns of every other node on this row
|
||||
let curCol = row;
|
||||
while ((curCol = curCol.right) !== row) {
|
||||
this.cover(curCol.column);
|
||||
}
|
||||
|
||||
complete = this._search(k + 1);
|
||||
|
||||
curCol = row;
|
||||
while ((curCol = curCol.left) !== row) {
|
||||
this.uncover(curCol.column);
|
||||
}
|
||||
|
||||
if (complete) break;
|
||||
}
|
||||
|
||||
this.uncover(column);
|
||||
return complete;
|
||||
}
|
||||
|
||||
search() {
|
||||
this.updates = 0;
|
||||
this._search(0);
|
||||
}
|
||||
}
|
@ -1,15 +1,9 @@
|
||||
import { SudokuMath } from "./math";
|
||||
import { StaticPool, isTimeoutError } from "node-worker-threads-pool";
|
||||
|
||||
import WORKERS from "physical-cpu-count";
|
||||
const TIMEOUT = 5000;
|
||||
const TIMEOUT = 20000;
|
||||
|
||||
export type Cell = { value: number | null };
|
||||
export type CellArray = Cell[];
|
||||
export type Row = CellArray;
|
||||
export type Column = CellArray;
|
||||
export type Region = CellArray;
|
||||
export type Puzzle = CellArray;
|
||||
|
||||
export type GenerateArguments = {
|
||||
regionWidth: number;
|
||||
@ -20,21 +14,16 @@ export type GenerateArguments = {
|
||||
export type Sudoku = {
|
||||
regionWidth: number;
|
||||
regionHeight: number;
|
||||
cells: number;
|
||||
raw: Puzzle;
|
||||
rows: () => Row[];
|
||||
columns: () => Column[];
|
||||
regions: () => Region[];
|
||||
size: number;
|
||||
cells: Cell[][];
|
||||
};
|
||||
|
||||
const pool = new StaticPool<GenerateArguments, [any, Puzzle]>({
|
||||
const pool = new StaticPool<GenerateArguments, Cell[][]>({
|
||||
size: WORKERS,
|
||||
task: "./src/sudoku/worker.js",
|
||||
});
|
||||
|
||||
const _math = new SudokuMath(0, 0);
|
||||
let activeWorkers = 0;
|
||||
|
||||
export async function generate(
|
||||
regionWidth: number,
|
||||
regionHeight: number,
|
||||
@ -46,7 +35,7 @@ export async function generate(
|
||||
|
||||
try {
|
||||
activeWorkers++;
|
||||
const [math, puzzle] = await pool.exec(
|
||||
const puzzle = await pool.exec(
|
||||
{
|
||||
regionWidth,
|
||||
regionHeight,
|
||||
@ -55,16 +44,11 @@ export async function generate(
|
||||
TIMEOUT
|
||||
);
|
||||
|
||||
Object.assign(_math, math);
|
||||
|
||||
return {
|
||||
regionWidth,
|
||||
regionHeight,
|
||||
cells: math.boardCells,
|
||||
raw: puzzle,
|
||||
rows: () => _math.regionsToRows(puzzle, true),
|
||||
columns: () => _math.regionsToCols(puzzle, true),
|
||||
regions: () => _math.chunkRegions(puzzle),
|
||||
size: (regionWidth * regionHeight) ** 2,
|
||||
cells: puzzle,
|
||||
};
|
||||
} catch (err) {
|
||||
if (isTimeoutError(err)) {
|
||||
|
@ -1,326 +1,278 @@
|
||||
import { Cell, Row, Column, Region, Puzzle } from "./index";
|
||||
import {
|
||||
clone,
|
||||
range,
|
||||
shuffle,
|
||||
chunkify,
|
||||
mapArray,
|
||||
addCellValuesToSet,
|
||||
} from "./util";
|
||||
DLX,
|
||||
DNode,
|
||||
CNode,
|
||||
linkNodesLR,
|
||||
addNodeToColumn,
|
||||
maskRow,
|
||||
unmaskRow,
|
||||
} from "./dlx";
|
||||
import { shuffle, range } from "./util";
|
||||
import { Cell } from "./index";
|
||||
|
||||
function getTakenValues(region: Region, row: Row, col: Column) {
|
||||
const filter = new Set<number>();
|
||||
addCellValuesToSet(filter, region);
|
||||
addCellValuesToSet(filter, row);
|
||||
addCellValuesToSet(filter, col);
|
||||
return filter;
|
||||
}
|
||||
type NodeMeta = {
|
||||
row: number;
|
||||
col: number;
|
||||
region: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type BoardInfo = [number, number, number, number, number];
|
||||
|
||||
export class SudokuMath {
|
||||
regionWidth: number;
|
||||
regionHeight: number;
|
||||
boardWidth: number;
|
||||
boardHeight: number;
|
||||
width: number;
|
||||
height: number;
|
||||
boardCells: number;
|
||||
regionCells: number;
|
||||
legalValues: number[];
|
||||
|
||||
_regionsFromIndex: number[];
|
||||
_rowsFromIndex: number[];
|
||||
_colsFromIndex: number[];
|
||||
_rowIndexToRegionIndex: number[];
|
||||
_colIndexToRegionIndex: number[];
|
||||
_regionIndexToRowIndex: number[];
|
||||
_regionIndexToColIndex: number[];
|
||||
values: number;
|
||||
values2: number;
|
||||
|
||||
indexes: number[];
|
||||
|
||||
candidates: BoardInfo[];
|
||||
|
||||
constructor(regionWidth: number, regionHeight: number) {
|
||||
this.regionWidth = regionWidth;
|
||||
this.regionHeight = regionHeight;
|
||||
this.boardWidth = regionHeight;
|
||||
this.boardHeight = regionWidth;
|
||||
this.width = regionWidth * this.boardWidth;
|
||||
this.height = regionHeight * this.boardHeight;
|
||||
this.boardCells = this.width * this.height;
|
||||
this.regionCells = regionWidth * regionHeight;
|
||||
this.legalValues = range(1, this.regionCells);
|
||||
|
||||
this._regionsFromIndex = Array(this.boardCells);
|
||||
this._rowsFromIndex = Array(this.boardCells);
|
||||
this._colsFromIndex = Array(this.boardCells);
|
||||
this._rowIndexToRegionIndex = Array(this.boardCells);
|
||||
this._colIndexToRegionIndex = Array(this.boardCells);
|
||||
this._regionIndexToRowIndex = Array(this.boardCells);
|
||||
this._regionIndexToColIndex = Array(this.boardCells);
|
||||
this.values = regionWidth * regionHeight;
|
||||
this.values2 = this.values * this.values;
|
||||
|
||||
for (let i = 0; i < this.boardCells; i++) {
|
||||
this._regionsFromIndex[i] = this._regionFromRegionIndex(i);
|
||||
this.indexes = Array(this.values2)
|
||||
.fill(null)
|
||||
.map((_, i) => i);
|
||||
|
||||
const [row, col] = this._rowColFromRegionIndex(i);
|
||||
this._rowsFromIndex[i] = row;
|
||||
this._colsFromIndex[i] = col;
|
||||
|
||||
const rowIndex = row * this.width + col;
|
||||
const colIndex = col * this.height + row;
|
||||
this._rowIndexToRegionIndex[rowIndex] = i;
|
||||
this._colIndexToRegionIndex[i] = rowIndex;
|
||||
this._regionIndexToRowIndex[i] = rowIndex;
|
||||
this._regionIndexToColIndex[colIndex] = i;
|
||||
}
|
||||
this.candidates = Array.from(Array(this.values ** 3), (_, i) =>
|
||||
this.getRowColRegionValFromCandidate(i)
|
||||
);
|
||||
}
|
||||
|
||||
_regionFromRegionIndex(i: number) {
|
||||
return Math.trunc(i / this.regionCells);
|
||||
}
|
||||
|
||||
regionFromRegionIndex(i: number) {
|
||||
return this._regionsFromIndex[i];
|
||||
}
|
||||
|
||||
_rowColFromRegionIndex(i: number) {
|
||||
const region = this.regionFromRegionIndex(i);
|
||||
const cell = i % this.regionCells;
|
||||
const regionRow = Math.trunc(region / this.boardWidth);
|
||||
const regionCol = region % this.boardWidth;
|
||||
const cellRow = Math.trunc(cell / this.regionWidth);
|
||||
const cellCol = cell % this.regionWidth;
|
||||
getConstraintIDs(val: number, row: number, col: number, region: number) {
|
||||
return [
|
||||
regionRow * this.regionHeight + cellRow,
|
||||
regionCol * this.regionWidth + cellCol,
|
||||
// each cell has a value
|
||||
row * this.values + col,
|
||||
// each row has one of each value
|
||||
this.values2 + row * this.values + val,
|
||||
// each col has one of each value
|
||||
this.values2 * 2 + col * this.values + val,
|
||||
// each region has one of each value
|
||||
this.values2 * 3 + region * this.values + val,
|
||||
];
|
||||
}
|
||||
|
||||
rowColFromRegionIndex(i: number) {
|
||||
return [this._rowsFromIndex[i], this._colsFromIndex[i]];
|
||||
getRowColRegionValFromCandidate(candidate: number): BoardInfo {
|
||||
const boardIndex = Math.floor(candidate / this.values);
|
||||
const row = Math.floor(boardIndex / this.values);
|
||||
const col = boardIndex % this.values;
|
||||
const region =
|
||||
Math.floor(row / this.regionHeight) * this.regionHeight +
|
||||
Math.floor(col / this.regionWidth);
|
||||
const val = candidate % this.values;
|
||||
return [candidate, row, col, region, val];
|
||||
}
|
||||
|
||||
regionIndexToRowIndex(i: number) {
|
||||
return this._regionIndexToRowIndex[i];
|
||||
}
|
||||
|
||||
rowIndexToRegionIndex(i: number) {
|
||||
return this._rowIndexToRegionIndex[i];
|
||||
}
|
||||
|
||||
regionIndexToColIndex(i: number) {
|
||||
return this._regionIndexToColIndex[i];
|
||||
}
|
||||
|
||||
colIndexToRegionIndex(i: number) {
|
||||
return this._colIndexToRegionIndex[i];
|
||||
}
|
||||
|
||||
chunkRegions(cells: Cell[]) {
|
||||
return chunkify(cells, this.regionCells);
|
||||
}
|
||||
|
||||
regionsToRows(cells: Cell[], split = false) {
|
||||
const rows = mapArray(cells, this._regionIndexToRowIndex);
|
||||
return split ? chunkify(rows, this.width) : rows;
|
||||
}
|
||||
|
||||
regionsToCols(cells: Cell[], split = false) {
|
||||
const cols = mapArray(cells, this._regionIndexToColIndex);
|
||||
return split ? chunkify(cols, this.height) : cols;
|
||||
}
|
||||
|
||||
rowsToRegions(cells: Cell[], split = false) {
|
||||
const regions = mapArray(cells, this._rowIndexToRegionIndex);
|
||||
return split ? chunkify(regions, this.regionCells) : regions;
|
||||
}
|
||||
|
||||
colsToRegions(cells: Cell[], split = false) {
|
||||
const regions = mapArray(cells, this._colIndexToRegionIndex);
|
||||
return split ? chunkify(regions, this.regionCells) : regions;
|
||||
}
|
||||
|
||||
getBlankPuzzle(): Puzzle {
|
||||
return Array(this.boardCells)
|
||||
.fill(null)
|
||||
.map((value) => ({ value }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remaining legal values given a set of taken values.
|
||||
*
|
||||
* @param taken a set of taken values
|
||||
*/
|
||||
getLegalValues(taken: Set<number>) {
|
||||
return this.legalValues.filter((value) => !taken.has(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which values are unavailable at a given location, looking at the
|
||||
* row, column, and region of the given cell index.
|
||||
*
|
||||
* @param cell
|
||||
* @param puzzle
|
||||
* @param regions
|
||||
*/
|
||||
getTakenValues(cell: number, puzzle: Puzzle, regions: Region[]) {
|
||||
const rows = this.regionsToRows(puzzle, true);
|
||||
const cols = this.regionsToCols(puzzle, true);
|
||||
const [row, col] = this.rowColFromRegionIndex(cell);
|
||||
const region = this.regionFromRegionIndex(cell);
|
||||
return getTakenValues(regions[region], rows[row], cols[col]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a puzzle has only one solution.
|
||||
*
|
||||
* @param puzzle
|
||||
*/
|
||||
hasOneSolution(puzzle: Cell[]) {
|
||||
const optimistic = clone(puzzle);
|
||||
if (this.optimisticSolver(optimistic)) {
|
||||
return true;
|
||||
}
|
||||
return this.backTrackingSolver(optimistic, 2) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a single-solution sudoku puzzle with
|
||||
*
|
||||
* @param clues the number of cells to have pre-filled
|
||||
*/
|
||||
generatePuzzle(clues: number) {
|
||||
const puzzle = this.getBlankPuzzle();
|
||||
this.backTrackingSolver(puzzle, 1, shuffle);
|
||||
|
||||
if (clues === -1 || clues >= puzzle.length) return puzzle;
|
||||
|
||||
const orig = clone(puzzle);
|
||||
|
||||
const toRemove = puzzle.length - clues;
|
||||
let removed: number[] = [];
|
||||
let removeNext: number[] = shuffle(puzzle.map((_, i) => i));
|
||||
|
||||
const remove = () => {
|
||||
const x = removeNext.shift() as any;
|
||||
removed.push(x);
|
||||
puzzle[x].value = null;
|
||||
};
|
||||
|
||||
const replace = () => {
|
||||
const x = removed.pop() as any;
|
||||
removeNext.push(x);
|
||||
puzzle[x].value = orig[x].value;
|
||||
};
|
||||
|
||||
const removeCell = () => {
|
||||
remove();
|
||||
if (this.hasOneSolution(puzzle)) {
|
||||
return true;
|
||||
}
|
||||
replace();
|
||||
return false;
|
||||
};
|
||||
|
||||
let fails = 0;
|
||||
while (removed.length < toRemove) {
|
||||
if (!removeCell()) {
|
||||
fails++;
|
||||
} else {
|
||||
console.log(`Removed ${removed.length} cells.`);
|
||||
fails = 0;
|
||||
}
|
||||
if (fails > removeNext.length) {
|
||||
fails = 0;
|
||||
Array(removed.length)
|
||||
.fill(null)
|
||||
.forEach(() => replace());
|
||||
shuffle(removeNext);
|
||||
_checkInput(cells: Cell[][]) {
|
||||
if (cells.length !== this.values || cells[0].length !== this.values) {
|
||||
throw new Error(
|
||||
"Given cells array does not match regionWidth & regionHeight"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return puzzle;
|
||||
}
|
||||
// this takes a bit of time and the value may need to be cached
|
||||
getDLXHeader(
|
||||
cells: undefined | Cell[][] = undefined,
|
||||
randomSearch = false
|
||||
): [CNode, DNode[]] {
|
||||
if (cells) this._checkInput(cells);
|
||||
|
||||
/**
|
||||
* Attempt to solve the puzzle "optimistically". Only sets values which are
|
||||
* certain, i.e. no guesses are made.
|
||||
*
|
||||
* Useful as a first pass.
|
||||
*
|
||||
* @param puzzle a region-ordered array of cells (each cell an object with
|
||||
* a `value` key.
|
||||
* @returns whether the puzzle was completely solved
|
||||
*/
|
||||
optimisticSolver(puzzle: Puzzle) {
|
||||
const regions = this.chunkRegions(puzzle);
|
||||
const rows = this.regionsToRows(puzzle, true);
|
||||
const cols = this.regionsToCols(puzzle, true);
|
||||
const header = new CNode();
|
||||
header.name = "h";
|
||||
|
||||
const solve = (): boolean => {
|
||||
let foundValue = false;
|
||||
let foundEmpty = false;
|
||||
const constraints = new Array<CNode>(this.values2 * 4)
|
||||
.fill(null!)
|
||||
.map((_, i) => {
|
||||
const column = new CNode();
|
||||
column.name = `${i}`;
|
||||
column.meta = i;
|
||||
return column;
|
||||
});
|
||||
|
||||
for (let i = 0, len = puzzle.length; i < len; i++) {
|
||||
const cell = puzzle[i];
|
||||
if (!!cell.value) continue;
|
||||
foundEmpty = true;
|
||||
const region = this.regionFromRegionIndex(i);
|
||||
const [row, col] = this.rowColFromRegionIndex(i);
|
||||
const taken = getTakenValues(regions[region], rows[row], cols[col]);
|
||||
if (taken.size === this.regionCells - 1) {
|
||||
cell.value = this.getLegalValues(taken)[0];
|
||||
foundValue = true;
|
||||
// link together the header and constraint columns
|
||||
linkNodesLR([header, ...constraints]);
|
||||
|
||||
const candidates = randomSearch
|
||||
? shuffle(Array.from(this.candidates))
|
||||
: this.candidates;
|
||||
|
||||
const dlxRows: DNode[] = [];
|
||||
candidates.forEach(([i, row, col, region, val]) => {
|
||||
if (cells) {
|
||||
const exist = cells[row][col].value;
|
||||
if (exist && exist - 1 !== val) {
|
||||
// skip candidates matching this constraint's position, but not its value
|
||||
// the effect is the exisitng value is preserved in the output
|
||||
return;
|
||||
}
|
||||
}
|
||||
return foundValue && foundEmpty ? solve() : !foundEmpty;
|
||||
};
|
||||
|
||||
return solve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Backtracking solver. Mutates the puzzle during solve but eventually returns
|
||||
* it to its initial state.
|
||||
*
|
||||
* @param puzzle see optimisticSolver
|
||||
* @param stopAfter stop looking after this many solutions
|
||||
* @param guessStrategy a function which takes an array of possible
|
||||
* values for a cell, and returns the same values (in any order)
|
||||
* @returns the number of solutions found
|
||||
*/
|
||||
backTrackingSolver(
|
||||
puzzle: Puzzle,
|
||||
stopAfter: number = -1,
|
||||
guessStrategy: (values: number[]) => number[] = (values: number[]) => values
|
||||
) {
|
||||
const regions = this.chunkRegions(puzzle);
|
||||
const rows = this.regionsToRows(puzzle, true);
|
||||
const cols = this.regionsToCols(puzzle, true);
|
||||
|
||||
let solutions = 0;
|
||||
const solve = (): boolean => {
|
||||
for (let i = 0, len = puzzle.length; i < len; i++) {
|
||||
const cell = puzzle[i];
|
||||
if (!cell.value) {
|
||||
const region = this.regionFromRegionIndex(i);
|
||||
const [row, col] = this.rowColFromRegionIndex(i);
|
||||
const avail = guessStrategy(
|
||||
this.getLegalValues(
|
||||
getTakenValues(regions[region], rows[row], cols[col])
|
||||
const meta = { row, col, region, value: val + 1 };
|
||||
const dlxRow = linkNodesLR(
|
||||
this.getConstraintIDs(val, row, col, region).map((id) =>
|
||||
addNodeToColumn(constraints[id], meta)
|
||||
)
|
||||
);
|
||||
for (let j = 0; j < avail.length; j++) {
|
||||
cell.value = avail[j];
|
||||
if (solve() && solutions === stopAfter) {
|
||||
return true;
|
||||
dlxRows.push(dlxRow[0]);
|
||||
});
|
||||
|
||||
return [header, dlxRows];
|
||||
}
|
||||
cell.value = null;
|
||||
|
||||
_baseBoard(): Cell[][] {
|
||||
// return a sudoku board with a random set of values in the first row
|
||||
// used in generateComplete for small speedup
|
||||
const firstRow = range(1, this.values + 1);
|
||||
shuffle(firstRow);
|
||||
return [
|
||||
firstRow.map((value) => ({ value })),
|
||||
...Array(this.values - 1)
|
||||
.fill(null)
|
||||
.map(() =>
|
||||
Array(this.values)
|
||||
.fill(0)
|
||||
.map((val) => ({ value: val > 0 ? val : null }))
|
||||
),
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
solutions++;
|
||||
|
||||
generateComplete() {
|
||||
const result = this._baseBoard();
|
||||
const [header] = this.getDLXHeader(result, true);
|
||||
|
||||
const callback = (solution: DNode[]) => {
|
||||
solution.forEach((node) => {
|
||||
const meta: NodeMeta = node.meta;
|
||||
result[meta.row][meta.col] = { value: meta.value };
|
||||
});
|
||||
// return the first solution
|
||||
return true;
|
||||
};
|
||||
|
||||
solve();
|
||||
const dlx = new DLX(header, callback);
|
||||
|
||||
return solutions;
|
||||
dlx.search();
|
||||
return result;
|
||||
}
|
||||
|
||||
generate(clues: number, attempts = Infinity, totalTime = Infinity) {
|
||||
const completed = this.generateComplete();
|
||||
|
||||
const [header, dlxRows] = this.getDLXHeader(); // complete header - no candidates removed
|
||||
|
||||
let solutions = 0;
|
||||
const dlx = new DLX(header, () => ++solutions >= 2);
|
||||
|
||||
const candidates: DNode[][][] = Array.from(Array(this.values), () =>
|
||||
Array.from(Array(this.values), () => Array(this.values))
|
||||
);
|
||||
dlxRows.forEach((node) => {
|
||||
const meta = node.meta;
|
||||
candidates[meta.row][meta.col][meta.value - 1] = node;
|
||||
});
|
||||
|
||||
// board positions which have been removed, in the order they've been removed
|
||||
const removed: Set<number> = new Set();
|
||||
const masked: DNode[] = [];
|
||||
|
||||
const hasOneSolution = () => {
|
||||
solutions = 0;
|
||||
dlx.search();
|
||||
return solutions === 1;
|
||||
};
|
||||
|
||||
const mask = () => {
|
||||
// mask all DLX rows which are nullified by existing values
|
||||
for (let n = 0; n < this.values2; n++) {
|
||||
if (removed.has(n)) {
|
||||
continue;
|
||||
}
|
||||
const row = Math.floor(n / this.values);
|
||||
const col = n % this.values;
|
||||
const existValue = completed[row][col].value;
|
||||
const nodes = candidates[row][col];
|
||||
// console.log(row, col);
|
||||
// console.log(existValue);
|
||||
nodes.forEach((node) => {
|
||||
if (node.meta.value !== existValue) {
|
||||
// console.log(node.meta);
|
||||
// console.log("masking node");
|
||||
masked.push(node);
|
||||
maskRow(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const unmask = () => {
|
||||
// unmask all DLX rows
|
||||
while (masked.length > 0) {
|
||||
unmaskRow(masked.pop()!);
|
||||
}
|
||||
};
|
||||
|
||||
const start = Date.now();
|
||||
const elapsed = () => Date.now() - start;
|
||||
|
||||
const removeable = Array.from(this.indexes);
|
||||
const attempt = () => {
|
||||
// attempt remove cells until 'clues' cells remain
|
||||
shuffle(removeable);
|
||||
for (let n = 0; n < this.values2; n++) {
|
||||
if (elapsed() > totalTime || this.values2 - removed.size == clues) {
|
||||
break;
|
||||
}
|
||||
|
||||
let toRemove = removeable[n];
|
||||
removed.add(toRemove);
|
||||
mask();
|
||||
|
||||
if (!hasOneSolution()) {
|
||||
removed.delete(toRemove);
|
||||
}
|
||||
|
||||
unmask();
|
||||
}
|
||||
};
|
||||
|
||||
while (
|
||||
this.values2 - removed.size > clues &&
|
||||
attempts > 0 &&
|
||||
elapsed() < totalTime
|
||||
) {
|
||||
// try to reach the clue goal up to `attempts` times or as long as
|
||||
// elapsed time is less than `totalTime`
|
||||
attempts--;
|
||||
removed.clear();
|
||||
attempt();
|
||||
}
|
||||
|
||||
removed.forEach((index) => {
|
||||
completed[Math.floor(index / this.values)][
|
||||
index % this.values
|
||||
].value = null;
|
||||
});
|
||||
return completed;
|
||||
}
|
||||
|
||||
solve(existing: Cell[][]): void {
|
||||
const [header] = this.getDLXHeader(existing);
|
||||
const callback = (solution: DNode[]) => {
|
||||
solution.forEach((node) => {
|
||||
const meta: NodeMeta = node.meta;
|
||||
existing[meta.row][meta.col] = { value: meta.value };
|
||||
});
|
||||
// return the first solution
|
||||
return true;
|
||||
};
|
||||
new DLX(header, callback).search();
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Cell } from "./index";
|
||||
|
||||
function randInt(lower: number, upper: number) {
|
||||
return lower + Math.trunc(Math.random() * (upper - lower + 1));
|
||||
export function randInt(lower: number, upper: number) {
|
||||
return Math.floor(Math.random() * (upper - lower)) + lower;
|
||||
}
|
||||
|
||||
export function shuffle(arr: any[]) {
|
||||
export function shuffle<T>(arr: T[]) {
|
||||
const length = arr.length;
|
||||
let lastIndex = length - 1;
|
||||
const last = length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const rand = randInt(i, lastIndex);
|
||||
const rand = randInt(i, last);
|
||||
const tmp = arr[rand];
|
||||
|
||||
arr[rand] = arr[i];
|
||||
@ -17,8 +17,8 @@ export function shuffle(arr: any[]) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function chunkify(arr: any[], chunkSize: number) {
|
||||
const chunks = Array(arr.length / chunkSize);
|
||||
export function chunkify<T>(arr: T[], chunkSize: number) {
|
||||
const chunks = Array<T[]>(arr.length / chunkSize);
|
||||
for (let i = 0, len = chunks.length; i < len; i++) {
|
||||
const start = i * chunkSize;
|
||||
chunks[i] = arr.slice(start, start + chunkSize);
|
||||
@ -27,7 +27,7 @@ export function chunkify(arr: any[], chunkSize: number) {
|
||||
}
|
||||
|
||||
export function range(start: number, end: number) {
|
||||
return Array(1 + end - start)
|
||||
return Array(end - start)
|
||||
.fill(null)
|
||||
.map((_, i) => start + i);
|
||||
}
|
||||
@ -57,3 +57,30 @@ export function addCellValuesToSet(set: Set<number>, cells: Cell[]) {
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
export function prettyPrint(puzzle: Cell[][]) {
|
||||
let width = Math.sqrt(puzzle[0].length);
|
||||
let height = Math.sqrt(puzzle.length);
|
||||
|
||||
puzzle.forEach((row, i) => {
|
||||
let line = "";
|
||||
row.forEach(({ value: cell }, j) => {
|
||||
if (j > 0 && j % width == 0) {
|
||||
line += "| ";
|
||||
}
|
||||
line += ((cell ? cell : " ") + " ").padStart(3, " ");
|
||||
});
|
||||
|
||||
if (i > 0 && i % height == 0) {
|
||||
let divider = "";
|
||||
row.forEach((_, j) => {
|
||||
if (j > 0 && j % width == 0) {
|
||||
divider += " ";
|
||||
}
|
||||
divider += "-- ";
|
||||
});
|
||||
console.log(divider);
|
||||
}
|
||||
console.log(line);
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
const { SudokuMath } = require("./math");
|
||||
const { parentPort } = require("worker_threads");
|
||||
|
||||
const maths = {};
|
||||
|
||||
parentPort.on("message", ({ regionWidth, regionHeight, clues }) => {
|
||||
const math = new SudokuMath(regionWidth, regionHeight);
|
||||
const puzzle = math.generatePuzzle(clues);
|
||||
parentPort.postMessage([math, puzzle]);
|
||||
const math =
|
||||
maths[`${regionWidth}:${regionHeight}`] ||
|
||||
(maths[`${regionWidth}:${regionHeight}`] = new SudokuMath(
|
||||
regionWidth,
|
||||
regionHeight
|
||||
));
|
||||
const puzzle = math.generate(clues);
|
||||
parentPort.postMessage(puzzle);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user