diff --git a/Cargo.lock b/Cargo.lock index 1d5e28a..b147e7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,7 @@ dependencies = [ "clap", "criterion-plot", "is-terminal", - "itertools", + "itertools 0.10.5", "num-traits", "once_cell", "oorandom", @@ -132,7 +132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -219,6 +219,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -455,6 +464,7 @@ name = "sudoku" version = "0.1.0" dependencies = [ "criterion", + "itertools 0.13.0", "rand", ] diff --git a/Cargo.toml b/Cargo.toml index 1accdda..c920d28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,20 @@ default-run = "example" [dependencies] criterion = "0.5.1" rand = "0.8.5" +itertools = "0.13.0" [[bench]] name = "solver_benches" harness = false +[[bench]] +name = "generator_benches" +harness = false + +[[bench]] +name = "techniques_benches" +harness = false + [[bin]] name = "example" test = false diff --git a/README.md b/README.md index b722759..e9e6170 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # sudoku -A library for sudoku puzzle generating & solving. +A library for sudoku puzzle generating, analyzing & solving. Run `cargo run` to see the example. \ No newline at end of file diff --git a/benches/generator_benches.rs b/benches/generator_benches.rs new file mode 100644 index 0000000..5a16f6a --- /dev/null +++ b/benches/generator_benches.rs @@ -0,0 +1,31 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use sudoku::generator::{ + random_sudoku_puzzle_easy, random_sudoku_puzzle_extrahard, random_sudoku_puzzle_hard, + random_sudoku_puzzle_normal, +}; + +fn benchmarks(c: &mut Criterion) { + c.bench_function("easy", |b| { + b.iter(|| { + random_sudoku_puzzle_easy(); + }) + }); + c.bench_function("normal", |b| { + b.iter(|| { + random_sudoku_puzzle_normal(); + }) + }); + c.bench_function("hard", |b| { + b.iter(|| { + random_sudoku_puzzle_hard(); + }) + }); + c.bench_function("extrahard", |b| { + b.iter(|| { + random_sudoku_puzzle_extrahard(); + }) + }); +} + +criterion_group!(benches, benchmarks); +criterion_main!(benches); diff --git a/benches/techniques_benches.rs b/benches/techniques_benches.rs new file mode 100644 index 0000000..63d7d88 --- /dev/null +++ b/benches/techniques_benches.rs @@ -0,0 +1,65 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use sudoku::{ + generator::random_sudoku_puzzle_ultimate, + state::full_state::FullState, + techniques::{ + fish::{Jellyfish, Swordfish, XWing}, + hidden_subsets::HiddenPair, + locked_candidates::{Claiming, Pointing}, + naked_subsets::{NakedPair, NakedSubset}, + Technique, + }, +}; + +fn benchmarks(c: &mut Criterion) { + let puzzle = random_sudoku_puzzle_ultimate(); + let state = FullState::from(puzzle); + + c.bench_function("Hidden Pair", |b| { + b.iter(|| { + HiddenPair::default().analyze(&state); + }) + }); + c.bench_function("Naked Pair", |b| { + b.iter(|| { + NakedPair::default().analyze(&state); + }) + }); + c.bench_function("Pointing", |b| { + b.iter(|| { + Pointing::default().analyze(&state); + }) + }); + c.bench_function("Claiming", |b| { + b.iter(|| { + Claiming::default().analyze(&state); + }) + }); + c.bench_function("Naked Subset", |b| { + b.iter(|| { + NakedSubset::default().analyze(&state); + }) + }); + c.bench_function("X-Wing", |b| { + b.iter(|| { + XWing::default().analyze(&state); + }) + }); + c.bench_function("Swordfish", |b| { + b.iter(|| { + Swordfish::default().analyze(&state); + }) + }); + c.bench_function("Jellyfish", |b| { + b.iter(|| { + Jellyfish::default().analyze(&state); + }) + }); +} + +criterion_group! { + name=benches; + config=Criterion::default().significance_level(0.1).sample_size(1000); + targets=benchmarks +} +criterion_main!(benches); diff --git a/src/bin/example.rs b/src/bin/example.rs index f810d3f..600fead 100644 --- a/src/bin/example.rs +++ b/src/bin/example.rs @@ -1,8 +1,9 @@ use sudoku::{ - generator::random_sudoku_puzzle_normal, + generator::random_sudoku_puzzle_hard, solver::{advanced::AdvancedSolver, Grader, Solver}, state::full_state::FullState, techniques::{ + fish::XWing, hidden_subsets::{HiddenPair, HiddenPairBlock, HiddenPairColumn, HiddenPairRow}, locked_candidates::{Claiming, Pointing}, naked_subsets::{NakedPair, NakedPairBlock, NakedPairColumn, NakedPairRow, NakedSubset}, @@ -14,7 +15,7 @@ use sudoku::{ }; fn main() { - let grid = random_sudoku_puzzle_normal(); + let grid = random_sudoku_puzzle_hard(); println!("The sudoku puzzle: "); println!("{}", grid); println!(); @@ -38,7 +39,7 @@ fn main() { } println!(); - let reducing_techniques: [(&mut dyn ReducingCandidates, &str); 11] = [ + let reducing_techniques: [(&mut dyn ReducingCandidates, &str); 12] = [ (&mut Pointing::default(), "Pointing"), (&mut Claiming::default(), "Claiming"), (&mut NakedPair::default(), "NakedPair"), @@ -50,6 +51,7 @@ fn main() { (&mut HiddenPairRow::default(), "HiddenPairRow"), (&mut HiddenPairColumn::default(), "HiddenPairColumn"), (&mut NakedSubset::default(), "NakedSubset"), + (&mut XWing::default(), "X-Wing"), ]; println!("Reducing-candidates techniques appliable: "); diff --git a/src/generator.rs b/src/generator.rs index 43d6544..9811acd 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -137,7 +137,7 @@ pub fn random_sudoku_puzzle_hard() -> Grid { pub fn random_sudoku_puzzle_extrahard() -> Grid { random_sudoku_puzzle::( 45, - 55.0 * 2.6 * 7.2f32.ln(), + 55.0 * 2.6 * 6.0f32.ln(), 55.0 * 3.2 * 12.0f32.ln(), ) } diff --git a/src/solver/advanced.rs b/src/solver/advanced.rs index b0505ff..58b58d0 100644 --- a/src/solver/advanced.rs +++ b/src/solver/advanced.rs @@ -4,6 +4,7 @@ use crate::{ TrackingCandidates, TrackingCellCountOfCandidate, }, techniques::{ + fish::{Jellyfish, Swordfish, XWing}, hidden_subsets::HiddenPair, locked_candidates::{Claiming, Pointing}, naked_subsets::{NakedPair, NakedSubset}, @@ -77,12 +78,15 @@ where } } - let reducing_techniques: [&mut dyn ReducingCandidates; 5] = [ + let reducing_techniques: [&mut dyn ReducingCandidates; 8] = [ &mut Pointing::default(), &mut Claiming::default(), &mut NakedPair::default(), &mut HiddenPair::default(), + &mut XWing::default(), &mut NakedSubset::default(), + &mut Swordfish::default(), + &mut Jellyfish::default(), ]; // TODO: Fish, Unique Rectangle diff --git a/src/techniques.rs b/src/techniques.rs index f20d331..c5c9b8c 100644 --- a/src/techniques.rs +++ b/src/techniques.rs @@ -70,6 +70,7 @@ pub enum House { Block(usize), } +pub mod fish; pub mod hidden_subsets; pub mod locked_candidates; pub mod naked_subsets; diff --git a/src/techniques/fish.rs b/src/techniques/fish.rs new file mode 100644 index 0000000..6da6fe8 --- /dev/null +++ b/src/techniques/fish.rs @@ -0,0 +1,277 @@ +use itertools::Itertools; + +use crate::state::{State, TrackingCandidates, TrackingCellCountOfCandidate}; + +use super::{House, ReducingCandidates, ReducingCandidatesOption, Technique}; + +fn basic_fish_row_base(state: &T, size: usize) -> Option +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + for base_rows in (0..9).combinations(size) { + 'outer: for num in 1..=9 { + for r in &base_rows { + if state.cell_cnt_of_candidate_in_row(*r, num) < 2 + || state.cell_cnt_of_candidate_in_row(*r, num) > size as i8 + { + continue 'outer; + } + } + + let cover_cols = (0..9) + .filter(|c| { + base_rows + .iter() + .any(|r| state.is_cell_empty(*r, *c) && state.is_candidate_of(*r, *c, num)) + }) + .collect::>(); + if cover_cols.len() != size { + continue 'outer; + } + + for c in &cover_cols { + if state.cell_cnt_of_candidate_in_col(*c, num) < 2 { + continue 'outer; + } + } + + let overlap = base_rows + .iter() + .flat_map(|r| cover_cols.iter().map(move |c| (*r, *c))); + + for r in base_rows.iter() { + for c in 0..9 { + if state.is_cell_empty(*r, c) + && state.is_candidate_of(*r, c, num) + && !overlap.clone().any(|coord| coord == (*r, c)) + { + continue 'outer; + } + } + } + + let remove: Vec<(usize, usize)> = cover_cols + .iter() + .flat_map(|c| { + let overlap = overlap.clone(); + (0..9).filter_map(move |r| { + if state.is_cell_empty(r, *c) + && state.is_candidate_of(r, *c, num) + && !overlap.clone().any(|coord| coord == (r, *c)) + { + Some((r, *c)) + } else { + None + } + }) + }) + .collect(); + + if remove.is_empty() { + continue 'outer; + } + + return Some(FishInfo { + size, + base_set: base_rows.iter().map(|r| House::Row(*r)).collect(), + cover_set: cover_cols.iter().map(|c| House::Column(*c)).collect(), + overlap: overlap.collect_vec(), + candidate: num, + rem_cells: remove, + }); + } + } + None +} + +fn basic_fish_col_base(state: &T, size: usize) -> Option +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + for base_cols in (0..9).combinations(size) { + 'outer: for num in 1..=9 { + for c in &base_cols { + if state.cell_cnt_of_candidate_in_col(*c, num) < 2 + || state.cell_cnt_of_candidate_in_col(*c, num) > size as i8 + { + continue 'outer; + } + } + + let cover_rows = (0..9) + .filter(|r| { + base_cols + .iter() + .any(|c| state.is_cell_empty(*r, *c) && state.is_candidate_of(*r, *c, num)) + }) + .collect::>(); + if cover_rows.len() != size { + continue 'outer; + } + + for r in &cover_rows { + if state.cell_cnt_of_candidate_in_row(*r, num) < 2 { + continue 'outer; + } + } + + let overlap = base_cols + .iter() + .flat_map(|c| cover_rows.iter().map(move |r| (*r, *c))); + + for c in base_cols.iter() { + for r in 0..9 { + if state.is_cell_empty(r, *c) + && state.is_candidate_of(r, *c, num) + && !overlap.clone().any(|coord| coord == (r, *c)) + { + continue 'outer; + } + } + } + + let remove: Vec<(usize, usize)> = cover_rows + .iter() + .flat_map(|r| { + let overlap = overlap.clone(); + (0..9).filter_map(move |c| { + if state.is_cell_empty(*r, c) + && state.is_candidate_of(*r, c, num) + && !overlap.clone().any(|coord| coord == (*r, c)) + { + Some((*r, c)) + } else { + None + } + }) + }) + .collect(); + + if remove.is_empty() { + continue 'outer; + } + + return Some(FishInfo { + size, + base_set: base_cols.iter().map(|c| House::Column(*c)).collect(), + cover_set: cover_rows.iter().map(|r| House::Row(*r)).collect(), + overlap: overlap.collect_vec(), + candidate: num, + rem_cells: remove, + }); + } + } + None +} + +#[derive(Clone, Debug)] +pub struct FishInfo { + pub size: usize, + pub base_set: Vec, + pub cover_set: Vec, + pub overlap: Vec<(usize, usize)>, + pub candidate: i8, + pub rem_cells: Vec<(usize, usize)>, +} + +#[derive(Default)] +pub struct XWing(pub Option); +impl Technique for XWing +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + fn analyze(&mut self, state: &T) { + self.0 = basic_fish_row_base(state, 2); + if self.0.is_some() { + return; + } + self.0 = basic_fish_col_base(state, 2); + } + fn appliable(&self) -> bool { + self.0.is_some() + } + fn score(&self) -> Option { + if self.0.is_some() { + return Some(3.2); + } + None + } +} +impl ReducingCandidates for XWing +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + fn option(&self) -> Option { + self.0 + .clone() + .map(|info| ReducingCandidatesOption(vec![(info.rem_cells, vec![info.candidate])])) + } +} + +#[derive(Default)] +pub struct Swordfish(pub Option); +impl Technique for Swordfish +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + fn analyze(&mut self, state: &T) { + self.0 = basic_fish_row_base(state, 3); + if self.0.is_some() { + return; + } + self.0 = basic_fish_col_base(state, 3); + } + fn appliable(&self) -> bool { + self.0.is_some() + } + fn score(&self) -> Option { + if self.0.is_some() { + return Some(3.8); + } + None + } +} +impl ReducingCandidates for Swordfish +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + fn option(&self) -> Option { + self.0 + .clone() + .map(|info| ReducingCandidatesOption(vec![(info.rem_cells, vec![info.candidate])])) + } +} + +#[derive(Default)] +pub struct Jellyfish(pub Option); +impl Technique for Jellyfish +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + fn analyze(&mut self, state: &T) { + self.0 = basic_fish_row_base(state, 4); + if self.0.is_some() { + return; + } + self.0 = basic_fish_col_base(state, 4); + } + fn appliable(&self) -> bool { + self.0.is_some() + } + fn score(&self) -> Option { + if self.0.is_some() { + return Some(5.2); + } + None + } +} +impl ReducingCandidates for Jellyfish +where + T: State + TrackingCandidates + TrackingCellCountOfCandidate, +{ + fn option(&self) -> Option { + self.0 + .clone() + .map(|info| ReducingCandidatesOption(vec![(info.rem_cells, vec![info.candidate])])) + } +} diff --git a/src/test.rs b/src/test.rs index b190609..0d70efe 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,14 +1,17 @@ use rand::random; use crate::{ - generator::{random_sudoku_puzzle_easy, random_sudoku_puzzle_extraeasy, random_sudoku_puzzle_extrahard, random_sudoku_puzzle_hard, random_sudoku_puzzle_normal, random_sudoku_puzzle_ultimate}, + generator::{ + random_sudoku_puzzle_easy, random_sudoku_puzzle_extraeasy, random_sudoku_puzzle_extrahard, + random_sudoku_puzzle_hard, random_sudoku_puzzle_normal, random_sudoku_puzzle_ultimate, + }, judge::judge_sudoku, solver::{advanced::AdvancedSolver, stochastic::StochasticSolver, Solver}, state::{ full_state::FullState, simple_state::SimpleState, CandidatesSettable, Fillable, State, TrackingCandidateCountOfCell, TrackingCandidates, TrackingCellCountOfCandidate, }, - utils::{block_idx_2_coord, coord_2_block_idx}, + utils::{block_idx_2_coord, coord_2_block_idx, overlap_region}, }; #[test] @@ -123,53 +126,6 @@ fn sudoku_state() { } } -// #[test] -// fn techniques_single() { -// for _ in 0..100 { -// let puzzle = random_sudoku_puzzle_normal(); -// let mut puzzle = FullState::from(puzzle); -// let res_hidden_single_row = hidden_single_row(&puzzle); -// let res_hidden_single_col = hidden_single_col(&puzzle); -// let res_hidden_single_blk = hidden_single_blk(&puzzle); -// let res_naked_single = naked_single(&puzzle); -// let singles = [ -// res_hidden_single_row, -// res_hidden_single_col, -// res_hidden_single_blk, -// res_naked_single, -// ]; -// for single in singles { -// if single.is_some() { -// let (r, c, num) = single.unwrap(); -// puzzle.fill_cell(r, c, num); -// assert!(judge_sudoku(&puzzle.grid()).0); -// puzzle.unfill_cell(r, c); -// } -// } -// } -// } - -// #[test] -// fn techniques_pair() { -// for _ in 0..10 { -// let mut res_hidden_pair_row = None; -// let mut puzzle = random_sudoku_puzzle_normal(); -// while res_hidden_pair_row.is_none() { -// puzzle = random_sudoku_puzzle_normal(); -// let puzzle = FullState::from(puzzle); -// res_hidden_pair_row = hidden_pair_row(&puzzle); -// } -// let mut puzzle = FullState::from(puzzle); -// let ((r1, c1), _, (r2, c2), _, num1, num2) = res_hidden_pair_row.clone().unwrap(); -// let nums: Vec = (1..=9).filter(|n| *n != num1 && *n != num2).collect(); -// for num in &nums { -// puzzle.remove_candidate_of_cell(r1, c1, *num); -// puzzle.remove_candidate_of_cell(r2, c2, *num); -// } -// let res_hidden_pair_row_1 = hidden_pair_row(&puzzle); -// assert_ne!(res_hidden_pair_row, res_hidden_pair_row_1); -// } -// } #[test] fn generate_sudoku() { println!("generating extraeasy puzzle"); @@ -188,8 +144,8 @@ fn generate_sudoku() { #[test] fn sudoku_solver() { - for _ in 0..100 { - let puzzle = random_sudoku_puzzle_easy(); + for _ in 0..50 { + let puzzle = random_sudoku_puzzle_ultimate(); let mut solver1 = StochasticSolver::::from(puzzle); let mut solver2 = AdvancedSolver::::from(puzzle); assert!(solver1.have_unique_solution()); @@ -200,3 +156,110 @@ fn sudoku_solver() { assert!(judge_sudoku(&solution2).1); } } + +#[test] +fn overlap_region_test() { + // row and columns + for r in 0..9 { + for c in 0..9 { + let region1 = overlap_region((0, r), (1, c)); + let region2 = overlap_region((1, c), (0, r)); + for c1 in 0..9 { + for r1 in 0..9 { + if (r1, c) == (r, c1) { + assert!( + region1 + .iter() + .find(|(r0, c0)| (*r0, *c0) == (r1, c)) + .is_some(), + "row {} & column {}, region: {:?}", + r, + c, + region1 + ); + assert!( + region2 + .iter() + .find(|(r0, c0)| (*r0, *c0) == (r1, c)) + .is_some(), + "row {} & column {}, region: {:?}", + r, + c, + region2 + ); + } + } + } + } + } + + // rows and blocks + for r in 0..9 { + for b in 0..9 { + let region1 = overlap_region((0, r), (2, b)); + let region2 = overlap_region((2, b), (0, r)); + for c1 in 0..9 { + for bidx1 in 0..9 { + let (r2, c2) = block_idx_2_coord(b, bidx1); + if (r, c1) == (r2, c2) { + assert!( + region1 + .iter() + .find(|(r0, c0)| (*r0, *c0) == (r, c1)) + .is_some(), + "row {} & block {}, region: {:?}", + r, + b, + region1 + ); + assert!( + region2 + .iter() + .find(|(r0, c0)| (*r0, *c0) == (r, c1)) + .is_some(), + "row {} & block {}, region: {:?}", + r, + b, + region2 + ); + } + } + } + } + } + + // columns and blocks + for c in 0..9 { + for b in 0..9 { + let region1 = overlap_region((1, c), (2, b)); + let region2 = overlap_region((2, b), (1, c)); + for r1 in 0..9 { + for bidx1 in 0..9 { + let (r2, c2) = block_idx_2_coord(b, bidx1); + if (r1, c) == (r2, c2) { + assert!( + region1 + .iter() + .find(|(r0, c0)| (*r0, *c0) == (r1, c)) + .is_some(), + "column {} & block {}, region: {:?}", + c, + b, + region1 + ); + assert!( + region2 + .iter() + .find(|(r0, c0)| (*r0, *c0) == (r1, c)) + .is_some(), + "column {} & block {}, region: {:?}", + c, + b, + region2 + ); + } + } + } + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 61f300a..ae5e642 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -18,4 +18,61 @@ pub fn count_one(mut bits: usize) -> usize { bits = (bits & 0x0000FFFF0000FFFFusize) + ((bits >> 16) & 0x0000FFFF0000FFFFusize); bits = (bits & 0x00000000FFFFFFFFusize) + ((bits >> 32) & 0x00000000FFFFFFFFusize); bits -} \ No newline at end of file +} + +pub fn overlap_region( + (h1, h_idx1): (usize, usize), + (h2, h_idx2): (usize, usize), +) -> Vec<(usize, usize)> { + // 0 stands for row, 1 for column and 2 for block + match h1 { + 0 => match h2 { + 0 => { + if h_idx1 == h_idx2 { + (0..9).map(|c| (h_idx1, c)).collect() + } else { + vec![] + } + } + 1 => vec![(h_idx1, h_idx2)], + 2 => { + if h_idx2 / 3 == h_idx1 / 3 { + (0..3).map(|c0| (h_idx1, c0 + h_idx2 % 3 * 3)).collect() + } else { + vec![] + } + } + _ => vec![], + }, + 1 => match h2 { + 0 => overlap_region((h2, h_idx2), (h1, h_idx1)), + 1 => { + if h_idx1 == h_idx2 { + (0..9).map(|r| (r, h_idx1)).collect() + } else { + vec![] + } + } + 2 => { + if h_idx2 % 3 == h_idx1 / 3 { + (0..3).map(|r0| (r0 + h_idx2 / 3 * 3, h_idx1)).collect() + } else { + vec![] + } + } + _ => vec![], + }, + 2 => match h2 { + 0..=1 => overlap_region((h2, h_idx2), (h1, h_idx1)), + 2 => { + if h_idx1 == h_idx2 { + (0..9).map(|bidx| block_idx_2_coord(h_idx1, bidx)).collect() + } else { + vec![] + } + } + _ => vec![], + }, + _ => vec![], + } +}