Skip to content

Commit

Permalink
solver: first working version of dancing links solver
Browse files Browse the repository at this point in the history
Perf beats the recursive solver by a nice marging already on hard
puzzles.
  • Loading branch information
Ryp committed Dec 2, 2023
1 parent 9393cf5 commit 0f94c87
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 74 deletions.
1 change: 0 additions & 1 deletion src/sdl2_frontend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const GameState = sudoku.GameState;
const BoardState = sudoku.BoardState;
const UnsetNumber = sudoku.UnsetNumber;
const u32_2 = sudoku.u32_2;
const all = sudoku.all;

const NumbersString = [_][*:0]const u8{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G" };
const CandidateBoxExtent = 27;
Expand Down
6 changes: 3 additions & 3 deletions src/sudoku/bench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const std = @import("std");
const assert = std.debug.assert;

const sudoku = @import("game.zig");
const brute_solver = @import("brute_solver.zig");
const solver = @import("solver.zig");
const boards = @import("boards.zig");

pub fn main() !void {
Expand All @@ -14,7 +14,7 @@ pub fn main() !void {
} });
defer sudoku.destroy_board_state(allocator, board);

sudoku.fill_board_from_string(board.numbers, boards.special_17_clues.board, board.extent);
sudoku.fill_board_from_string(board.numbers, boards.special_dancing_links.board, board.extent);

assert(brute_solver.solve(&board, .{}));
assert(solver.solve(&board, .{}));
}
6 changes: 6 additions & 0 deletions src/sudoku/boards.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ pub const special_17_clues = SudokuString{
.solution = "237841569186795243594326718315674892469582137728139456642918375853467921971253684",
};

// Slow on dancing links
pub const special_dancing_links = SudokuString{
.board = "........1.....2..3.45.............1...6.....237..6.......1..4.....2.35...8.......",
.solution = "", // FIXME
};

// NOTE: See https://stackoverflow.com/questions/24682039/whats-the-worst-case-valid-sudoku-puzzle-for-simple-backtracking-brute-force-al
pub const naive_backtracking_killer = "..............3.85..1.2.......5.7.....4...1...9.......5......73..2.1........4...9";
pub const naive_backtracking_killer_2 = "9..8...........5............2..1...3.1.....6....4...7.7.86.........3.1..4.....2..";
36 changes: 0 additions & 36 deletions src/sudoku/brute_solver_test.zig

This file was deleted.

12 changes: 1 addition & 11 deletions src/sudoku/game.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const std = @import("std");
const assert = std.debug.assert;

const generator = @import("generator.zig");
const brute_solver = @import("brute_solver.zig");
const solver = @import("solver.zig");

// I borrowed this name from HLSL
Expand All @@ -14,15 +13,6 @@ pub fn all(vector: anytype) bool {
return @reduce(.And, vector);
}

// I borrowed this name from HLSL
pub fn any(vector: anytype) bool {
const type_info = @typeInfo(@TypeOf(vector));
assert(type_info.Vector.child == bool);
assert(type_info.Vector.len > 1);

return @reduce(.Or, vector);
}

pub const u32_2 = @Vector(2, u32);

pub const MaxSudokuExtent = 16;
Expand Down Expand Up @@ -773,7 +763,7 @@ const PlayerSolveBoard = struct {
};

fn player_solve_board(game: *GameState) void {
if (brute_solver.solve(&game.board, .{})) {
if (solver.solve(&game.board, .{})) {
player_clear_candidates(game);
// NOTE: Already done in the body of clear_candidates.
// push_state_to_history(game);
Expand Down
34 changes: 29 additions & 5 deletions src/sudoku/solver.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,32 @@ const BoardState = sudoku.BoardState;
const UnsetNumber = sudoku.UnsetNumber;
const u32_2 = sudoku.u32_2;
const all = sudoku.all;
const any = sudoku.any;

const AABB_u32_2 = struct {
min: u32_2,
max: u32_2,
const dancing_links = @import("solver_dancing_links.zig");
const backtracking = @import("solver_backtracking.zig");

const Algorithm = enum {
DancingLinks,
SortedBacktracking,
};

const Options = struct {
algorithm: Algorithm = Algorithm.DancingLinks,
recursive: bool = true,
};

pub fn solve(board: *BoardState, options: Options) bool {
switch (options.algorithm) {
Algorithm.DancingLinks => {
assert(options.recursive);
return dancing_links.solve(board);
},
Algorithm.SortedBacktracking => {
return backtracking.solve(board, options.recursive);
},
}
}

fn count_bits_u16(mask_ro: u16) u4 {
var mask = mask_ro;
var count: u4 = 0;
Expand Down Expand Up @@ -249,6 +268,11 @@ fn find_hidden_pair_region(board: BoardState, candidate_masks: []const u16, regi
return null;
}

const AABB_u32_2 = struct {
min: u32_2,
max: u32_2,
};

pub const PointingLine = struct {
number: u4,
line_region: []u32,
Expand Down Expand Up @@ -381,7 +405,7 @@ pub fn find_box_line_reduction_for_line(board: BoardState, candidate_masks: []co
for (box_region, 0..) |cell_index, region_cell_index| {
const cell_coord = sudoku.cell_coord_from_index(board.extent, cell_index);

if (!any(cell_coord == line_coord)) {
if (all(cell_coord != line_coord)) {
if (candidate_masks[cell_index] & number_mask != 0) {
deletion_mask |= sudoku.mask_for_number(@intCast(region_cell_index));
}
Expand Down
30 changes: 13 additions & 17 deletions src/sudoku/brute_solver.zig → src/sudoku/solver_backtracking.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,26 @@ const sudoku = @import("game.zig");
const BoardState = sudoku.BoardState;
const UnsetNumber = sudoku.UnsetNumber;

const CellInfo = struct {
index: u8,
col: u4,
row: u4,
};

const Options = struct {
recursive: bool = false,
};

pub fn solve(board: *BoardState, options: Options) bool {
pub fn solve(board: *BoardState, recursive: bool) bool {
var free_cell_list_full: [sudoku.MaxSudokuExtent * sudoku.MaxSudokuExtent]CellInfo = undefined;
var free_cell_list = populate_free_list(board, &free_cell_list_full);

sort_free_cell_list(board, free_cell_list);

if (options.recursive) {
return solve_recursive(board, free_cell_list, 0);
if (recursive) {
return solve_backtracking_recursive(board, free_cell_list, 0);
} else {
return solve_iterative(board, free_cell_list);
return solve_backtracking_iterative(board, free_cell_list);
}
}

fn solve_recursive(board: *BoardState, free_cell_list: []CellInfo, list_index: u32) bool {
const CellInfo = struct {
index: u8,
col: u4,
row: u4,
};

fn solve_backtracking_recursive(board: *BoardState, free_cell_list: []CellInfo, list_index: u32) bool {
if (list_index >= free_cell_list.len) {
return true;
}
Expand All @@ -48,7 +44,7 @@ fn solve_recursive(board: *BoardState, free_cell_list: []CellInfo, list_index: u
if (is_valid) {
cell_number.* = @intCast(number);

if (solve_recursive(board, free_cell_list, list_index + 1)) {
if (solve_backtracking_recursive(board, free_cell_list, list_index + 1)) {
return true;
}
}
Expand All @@ -58,7 +54,7 @@ fn solve_recursive(board: *BoardState, free_cell_list: []CellInfo, list_index: u
return false;
}

fn solve_iterative(board: *BoardState, free_cell_list: []CellInfo) bool {
fn solve_backtracking_iterative(board: *BoardState, free_cell_list: []CellInfo) bool {
var current_guess_full = std.mem.zeroes([sudoku.MaxSudokuExtent * sudoku.MaxSudokuExtent]u4);
var current_guess = current_guess_full[0..free_cell_list.len];

Expand Down
Loading

0 comments on commit 0f94c87

Please sign in to comment.