use std::cmp;
use itertools::{Either, Itertools};
use tracing::warn;
use crate::{
IntVal,
actions::{
InitActions, IntEvent, IntInspectionActions, IntPropCond, PostingActions, ReasoningEngine,
},
constraints::{
Constraint, IntModelActions, IntSolverActions, Propagator, SimplificationStatus,
},
lower::{LoweringContext, LoweringError},
model::View,
solver::{IntLitMeaning, engine::Engine, queue::PriorityLevel},
};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct IntUnique {
pub(crate) bounds_prop: IntUniqueBounds<View<IntVal>>,
pub(crate) value_prop: IntUniqueValue<View<IntVal>>,
pub(crate) bounds_propagation: Option<bool>,
pub(crate) value_propagation: Option<bool>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct IntUniqueBounds<I> {
pub(crate) var: Vec<I>,
var_info: Vec<UniqueVarMeta>,
lb_cache: Vec<IntVal>,
ub_cache: Vec<IntVal>,
min_sorted: Vec<usize>,
max_sorted: Vec<usize>,
num_bounds: usize,
bounds: Vec<IntVal>,
predecessor: Vec<usize>,
diff: Vec<IntVal>,
hall_interval: Vec<usize>,
bucket: Vec<usize>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct IntUniqueValue<I> {
vars: Vec<I>,
action_list: Vec<usize>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct UniqueVarMeta {
next: usize,
min_rank: usize,
max_rank: usize,
}
impl IntUnique {
pub fn bounds_propagation(&self) -> bool {
self.bounds_propagation.unwrap_or(true)
}
pub fn value_propagation(&self) -> bool {
self.value_propagation.unwrap_or(false)
}
}
impl<E> Constraint<E> for IntUnique
where
E: ReasoningEngine,
View<IntVal>: IntModelActions<E>,
{
fn simplify(
&mut self,
ctx: &mut E::PropagationContext<'_>,
) -> Result<SimplificationStatus, E::Conflict> {
self.propagate(ctx)?;
Ok(SimplificationStatus::NoFixpoint)
}
fn to_solver(&self, slv: &mut LoweringContext<'_>) -> Result<(), LoweringError> {
let (_vals, vars): (Vec<_>, Vec<_>) = self.bounds_prop.var.iter().partition_map(|&var| {
let var = slv.solver_view(var);
if let Some(val) = var.val(slv) {
Either::Left(val)
} else {
Either::Right(var)
}
});
debug_assert!(_vals.iter().unique().collect_vec().len() == _vals.len());
debug_assert!(
_vals
.iter()
.all(|&val| vars.iter().all(|var| !var.in_domain(slv, val)))
);
if vars.len() <= 1 {
return Ok(());
}
let value_propagation = self.value_propagation();
if value_propagation {
IntUniqueValue::post(slv, vars.clone());
}
let mut bounds_propagation = self.bounds_propagation();
if !value_propagation && !bounds_propagation {
warn!(
"all propagation algorithms are disabled for `int_unique` constraint, override with bounds propagation to ensure consistency"
);
bounds_propagation = true;
}
if bounds_propagation {
IntUniqueBounds::post(slv, vars);
}
Ok(())
}
}
impl<E> Propagator<E> for IntUnique
where
E: ReasoningEngine,
View<IntVal>: IntSolverActions<E>,
{
fn advise_of_backtrack(&mut self, ctx: &mut E::NotificationContext<'_>) {
self.value_prop.advise_of_backtrack(ctx);
}
fn advise_of_int_change(
&mut self,
ctx: &mut E::NotificationContext<'_>,
data: u64,
event: IntEvent,
) -> bool {
self.value_prop.advise_of_int_change(ctx, data, event)
}
fn initialize(&mut self, ctx: &mut E::InitializationContext<'_>) {
self.value_prop.initialize(ctx);
self.bounds_prop.initialize(ctx);
}
fn propagate(&mut self, ctx: &mut E::PropagationContext<'_>) -> Result<(), E::Conflict> {
if !self.value_prop.action_list.is_empty() {
self.value_prop.propagate(ctx)?;
}
self.bounds_prop.propagate(ctx)
}
}
impl<I> IntUniqueBounds<I> {
fn filter_lower<E>(&mut self, ctx: &mut E::PropagationContext<'_>) -> Result<(), E::Conflict>
where
E: ReasoningEngine,
I: IntSolverActions<E>,
{
for i in 1..=self.num_bounds + 1 {
self.hall_interval[i] = i - 1;
self.predecessor[i] = i - 1;
self.diff[i] = self.bounds[i] - self.bounds[i - 1];
self.bucket[i] = usize::MAX;
}
for i in 0..self.var.len() {
let max_rank = self.var_info[self.max_sorted[i]].max_rank;
let min_rank = self.var_info[self.max_sorted[i]].min_rank;
let mut z = Self::path_max(&self.predecessor, min_rank + 1);
let j = self.predecessor[z];
self.diff[z] -= 1;
self.var_info[self.max_sorted[i]].next = self.bucket[z];
self.bucket[z] = self.max_sorted[i];
if self.diff[z] == 0 {
self.predecessor[z] = z + 1;
z = Self::path_max(&self.predecessor, self.predecessor[z]);
self.predecessor[z] = j;
};
Self::path_set(&mut self.predecessor, min_rank + 1, z, z);
if self.hall_interval[min_rank] > min_rank {
let w = Self::path_max(&self.hall_interval, self.hall_interval[min_rank]);
let hall_max = self.bounds[w];
let mut hall_min = self.bounds[min_rank];
let mut k = w;
while self.bounds[k] > hall_min {
let mut l = self.bucket[k];
while l != usize::MAX {
hall_min = cmp::min(hall_min, self.lb_cache[l]);
l = self.var_info[l].next;
}
k -= 1;
}
let mut k = w;
let mut reason = Vec::new();
reason.push(
self.var[self.max_sorted[i]].lit(ctx, IntLitMeaning::GreaterEq(hall_min)),
);
while self.bounds[k] > hall_min {
let mut l = self.bucket[k];
while l != usize::MAX {
reason.push(self.var[l].lit(ctx, IntLitMeaning::GreaterEq(hall_min)));
reason.push(self.var[l].lit(ctx, IntLitMeaning::Less(hall_max)));
l = self.var_info[l].next;
}
k -= 1;
}
self.var[self.max_sorted[i]].tighten_min(ctx, hall_max, reason)?;
self.lb_cache[self.max_sorted[i]] = hall_max;
Self::path_set(&mut self.hall_interval, min_rank, w, w);
}
if self.diff[z] == self.bounds[z] - self.bounds[max_rank] {
let h_max_rank = self.hall_interval[max_rank];
Self::path_set(&mut self.hall_interval, h_max_rank, j - 1, max_rank);
self.hall_interval[max_rank] = j - 1;
}
}
Ok(())
}
fn filter_upper<E>(&mut self, ctx: &mut E::PropagationContext<'_>) -> Result<(), E::Conflict>
where
E: ReasoningEngine,
I: IntSolverActions<E>,
{
for i in 0..=self.num_bounds {
self.hall_interval[i] = i + 1;
self.predecessor[i] = i + 1;
self.diff[i] = self.bounds[i + 1] - self.bounds[i];
self.bucket[i] = usize::MAX;
}
for i in (0..self.var.len()).rev() {
let max_rank = self.var_info[self.min_sorted[i]].max_rank;
let min_rank = self.var_info[self.min_sorted[i]].min_rank;
let mut z = Self::path_min(&self.predecessor, max_rank - 1);
let j = self.predecessor[z];
self.diff[z] -= 1;
self.var_info[self.min_sorted[i]].next = self.bucket[z];
self.bucket[z] = self.min_sorted[i];
if self.diff[z] == 0 {
self.predecessor[z] = z - 1;
z = Self::path_min(&self.predecessor, self.predecessor[z]);
self.predecessor[z] = j;
}
Self::path_set(&mut self.predecessor, max_rank - 1, z, z);
if self.hall_interval[max_rank] < max_rank {
let w = Self::path_min(&self.hall_interval, self.hall_interval[max_rank]);
let hall_min = self.bounds[w];
let mut hall_max = self.bounds[max_rank];
let mut k = w;
while self.bounds[k] < hall_max {
let mut l = self.bucket[k];
while l != usize::MAX {
hall_max = cmp::max(hall_max, self.ub_cache[l] + 1);
l = self.var_info[l].next;
}
k += 1;
}
let mut k = w;
let mut reason = Vec::new();
reason.push(self.var[self.min_sorted[i]].lit(ctx, IntLitMeaning::Less(hall_max)));
while self.bounds[k] < hall_max {
let mut l = self.bucket[k];
while l != usize::MAX {
reason.push(self.var[l].lit(ctx, IntLitMeaning::GreaterEq(hall_min)));
reason.push(self.var[l].lit(ctx, IntLitMeaning::Less(hall_max)));
l = self.var_info[l].next;
}
k += 1;
}
self.var[self.min_sorted[i]].tighten_max(ctx, hall_min - 1, reason)?;
self.ub_cache[self.min_sorted[i]] = hall_min - 1;
Self::path_set(&mut self.hall_interval, max_rank, w, w);
}
if self.diff[z] == self.bounds[min_rank] - self.bounds[z] {
let h_min_rank = self.hall_interval[min_rank];
Self::path_set(&mut self.hall_interval, h_min_rank, j + 1, min_rank);
self.hall_interval[min_rank] = j + 1;
}
}
Ok(())
}
pub(crate) fn new(vars: Vec<I>) -> Self {
let interval = vec![
UniqueVarMeta {
next: 0,
min_rank: 0,
max_rank: 0
};
vars.len()
];
let min_sorted: Vec<_> = (0..vars.len()).collect();
let max_sorted: Vec<_> = (0..vars.len()).collect();
let n = 2 * vars.len() + 2;
Self {
var: vars,
var_info: interval,
lb_cache: vec![0; n],
ub_cache: vec![0; n],
min_sorted,
max_sorted,
num_bounds: 0,
bounds: vec![0; n],
predecessor: vec![0; n],
diff: vec![0; n],
hall_interval: vec![0; n],
bucket: vec![0; n],
}
}
fn path_max(transition: &[usize], mut start: usize) -> usize {
while transition[start] > start {
start = transition[start];
}
start
}
fn path_min(transition: &[usize], mut start: usize) -> usize {
while transition[start] < start {
start = transition[start];
}
start
}
fn path_set(transition: &mut [usize], start: usize, end: usize, to: usize) {
let mut last;
let mut cur = start;
while cur != end {
last = cur;
cur = transition[cur];
transition[last] = to;
}
}
pub fn post<E>(solver: &mut E, vars: Vec<I>)
where
E: PostingActions + ?Sized,
I: IntSolverActions<Engine>,
{
solver.add_propagator(Box::new(Self::new(vars)));
}
fn sort<E>(&mut self, ctx: &mut E::PropagationContext<'_>)
where
E: ReasoningEngine,
I: IntSolverActions<E>,
{
let size: usize = self.var.len();
for (i, v) in self.var.iter().enumerate() {
(self.lb_cache[i], self.ub_cache[i]) = v.bounds(ctx);
}
self.min_sorted.sort_by_key(|&i| self.lb_cache[i]);
self.max_sorted.sort_by_key(|&i| self.ub_cache[i] + 1);
let mut min: IntVal = self.lb_cache[self.min_sorted[0]];
let mut max: IntVal = self.ub_cache[self.max_sorted[0]] + 1;
let mut last: IntVal = min - 2;
self.bounds[0] = last;
let mut i = 0;
let mut j = 0;
self.num_bounds = 0;
loop {
if i < size && min <= max {
if min != last {
self.num_bounds += 1;
last = min;
self.bounds[self.num_bounds] = min;
}
self.var_info[self.min_sorted[i]].min_rank = self.num_bounds;
i += 1;
if i < size {
min = self.lb_cache[self.min_sorted[i]];
}
} else {
if max != last {
self.num_bounds += 1;
last = max;
self.bounds[self.num_bounds] = max;
}
self.var_info[self.max_sorted[j]].max_rank = self.num_bounds;
j += 1;
if j == size {
break;
}
max = self.ub_cache[self.max_sorted[j]] + 1;
}
}
self.bounds[self.num_bounds + 1] = self.bounds[self.num_bounds] + 2; }
}
impl<E, I> Propagator<E> for IntUniqueBounds<I>
where
E: ReasoningEngine,
I: IntSolverActions<E>,
{
fn initialize(&mut self, ctx: &mut <E as ReasoningEngine>::InitializationContext<'_>) {
ctx.set_priority(PriorityLevel::Low);
for v in &self.var {
v.enqueue_when(ctx, IntPropCond::Bounds);
}
}
#[tracing::instrument(
name = "int_unique_bounds",
target = "solver",
level = "trace",
skip(self, ctx)
)]
fn propagate(&mut self, ctx: &mut E::PropagationContext<'_>) -> Result<(), E::Conflict> {
self.sort(ctx);
self.filter_lower(ctx)?;
self.filter_upper(ctx)?;
Ok(())
}
}
impl<I> IntUniqueValue<I> {
pub(crate) fn new(vars: Vec<I>) -> Self {
Self {
vars,
action_list: Vec::new(),
}
}
pub fn post<E>(solver: &mut E, vars: Vec<I>)
where
E: PostingActions + ?Sized,
I: IntSolverActions<Engine>,
{
solver.add_propagator(Box::new(Self::new(vars)));
}
}
impl<E, I> Propagator<E> for IntUniqueValue<I>
where
E: ReasoningEngine,
I: IntSolverActions<E>,
{
fn advise_of_backtrack(&mut self, _: &mut E::NotificationContext<'_>) {
self.action_list.clear();
}
fn advise_of_int_change(
&mut self,
_: &mut E::NotificationContext<'_>,
data: u64,
event: IntEvent,
) -> bool {
debug_assert_eq!(event, IntEvent::Fixed);
self.action_list.push(data as usize);
true
}
fn initialize(&mut self, ctx: &mut E::InitializationContext<'_>) {
for (i, v) in self.vars.iter().enumerate() {
if self.vars[i].val(ctx).is_some() {
self.action_list.push(i);
ctx.enqueue_now(true);
} else {
v.advise_when(ctx, IntPropCond::Fixed, i as u64);
}
}
ctx.advise_on_backtrack();
}
#[tracing::instrument(
name = "int_unique_value",
target = "solver",
level = "trace",
skip(self, ctx)
)]
fn propagate(&mut self, ctx: &mut E::PropagationContext<'_>) -> Result<(), E::Conflict> {
debug_assert!(!self.action_list.is_empty() && self.action_list.iter().all_unique());
for &i in &self.action_list {
let val = self.vars[i].val(ctx).unwrap();
let reason = &[self.vars[i].val_lit(ctx).unwrap()];
for (j, v) in self.vars.iter().enumerate() {
if j != i {
v.remove_val(ctx, val, reason)?;
}
}
}
self.action_list.clear();
Ok(())
}
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use tracing_test::traced_test;
use crate::{
IntSet, IntVal,
constraints::{
int_linear::IntLinearLessEqBounds,
int_unique::{IntUniqueBounds, IntUniqueValue},
},
model::Model,
solver::{LiteralStrategy, Solver, Status, Valuation},
};
#[test]
#[traced_test]
fn test_all_different_bounds_sat_1() {
let mut slv = Solver::default();
let a = slv
.new_int_decision(1..=3)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let b = slv
.new_int_decision(1..=3)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let c = slv
.new_int_decision(1..=3)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
IntUniqueBounds::post(&mut slv, vec![a, b, c]);
slv.assert_all_solutions(&[a, b, c], |sol| sol.iter().all_unique());
}
#[test]
#[traced_test]
fn test_all_different_bounds_sat_2() {
let mut slv = Solver::default();
let a = slv
.new_int_decision(3..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let b = slv
.new_int_decision(2..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let c = slv
.new_int_decision(3..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let d = slv
.new_int_decision(2..=5)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let e = slv
.new_int_decision(3..=6)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let f = slv
.new_int_decision(1..=6)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
IntUniqueBounds::post(&mut slv, vec![a, b, c, d, e, f]);
slv.assert_all_solutions(&[a, b, c, d, e, f], |sol| sol.iter().all_unique());
}
#[test]
#[traced_test]
fn test_all_different_bounds_sat_3() {
let mut slv = Solver::default();
let a = slv
.new_int_decision(3..=6)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let b = slv
.new_int_decision(3..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let c = slv
.new_int_decision(2..=5)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let d = slv
.new_int_decision(2..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let e = slv
.new_int_decision(3..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let f = slv
.new_int_decision(1..=6)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
IntUniqueBounds::post(&mut slv, vec![a, b, c, d, e, f]);
slv.assert_all_solutions(&[a, b, c, d, e, f], |sol| sol.iter().all_unique());
}
#[test]
#[traced_test]
fn test_all_different_bounds_unsat() {
let mut slv = Solver::default();
let a = slv
.new_int_decision(1..=3)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let b = slv
.new_int_decision(1..=3)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let c = slv
.new_int_decision(1..=3)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
IntUniqueBounds::post(&mut slv, vec![a, b, c]);
IntLinearLessEqBounds::post(&mut slv, vec![-a, -b, -c], -8);
slv.assert_unsatisfiable();
}
#[test]
#[traced_test]
fn test_all_different_value_sat() {
let mut slv = Solver::default();
let a = slv
.new_int_decision(1..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let b = slv
.new_int_decision(1..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let c = slv
.new_int_decision(1..=4)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
IntUniqueValue::post(&mut slv, vec![a, b, c]);
slv.assert_all_solutions(&[a, b, c], |sol| sol.iter().all_unique());
}
#[test]
#[traced_test]
fn test_all_different_value_unsat() {
let mut slv = Solver::default();
let a = slv
.new_int_decision(1..=2)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let b = slv
.new_int_decision(1..=2)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
let c = slv
.new_int_decision(1..=2)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view();
IntUniqueValue::post(&mut slv, vec![a, b, c]);
slv.assert_unsatisfiable();
}
#[test]
#[traced_test]
fn test_gapped_domain_regression() {
let mut prb = Model::default();
let prev: Vec<_> = [
IntSet::from_iter([15..=15, 20..=20]),
(20..=20).into(),
IntSet::from_iter([15..=15, 20..=20]),
]
.into_iter()
.map(|domain| prb.new_int_decision(domain))
.collect();
assert!(prb.unique(prev.iter().copied()).post().is_err());
}
fn test_sudoku(grid: &[&str], expected: Status) {
debug_assert_eq!(grid.len(), 9);
debug_assert!(grid.iter().all(|row| row.len() == 9));
let mut slv: Solver = Solver::default();
let all_vars: Vec<_> = grid
.iter()
.map(|row| {
let vars: Vec<_> = row
.chars()
.map(|c| {
if c.is_ascii_digit() {
let num = IntVal::from(c.to_digit(10).unwrap());
num.into()
} else {
slv.new_int_decision(1..=9)
.order_literals(LiteralStrategy::Eager)
.direct_literals(LiteralStrategy::Eager)
.view()
}
})
.collect();
IntUniqueValue::post(&mut slv, vars.clone());
vars
})
.collect();
for (i, _) in grid.iter().enumerate() {
let col_vars: Vec<_> = grid
.iter()
.enumerate()
.map(|(j, _)| all_vars[j][i])
.collect();
IntUniqueValue::post(&mut slv, col_vars);
}
for i in 0..3 {
for j in 0..3 {
let mut block_vars: Vec<_> = Vec::with_capacity(grid.len());
for x in 0..3 {
for y in 0..3 {
block_vars.push(all_vars[3 * i + x][3 * j + y]);
}
}
IntUniqueValue::post(&mut slv, block_vars);
}
}
assert_eq!(
slv.solve()
.on_solution(|sol| {
(0..9).for_each(|r| {
let row = all_vars[r].iter().map(|&v| v.val(sol)).collect_vec();
assert!(
row.iter().all_unique(),
"Values in row {r} are not all different: {row:?}",
);
});
(0..9).for_each(|c| {
let col = all_vars.iter().map(|row| row[c].val(sol)).collect_vec();
assert!(
col.iter().all_unique(),
"Values in column {c} are not all different: {col:?}",
);
});
(0..3).for_each(|i| {
(0..3).for_each(|j| {
let block = (0..3)
.flat_map(|x| (0..3).map(move |y| (x, y)))
.map(|(x, y)| all_vars[3 * i + x][3 * j + y].val(sol))
.collect_vec();
assert!(
block.iter().all_unique(),
"Values in block ({i}, {j}) are not all different: {block:?}",
);
});
});
})
.satisfy(),
expected
);
}
#[test]
#[traced_test]
fn test_sudoku_1() {
test_sudoku(
&[
"2581.4.37",
"936827514",
"47153.28.",
"7152.3.4.",
"849675321",
"36241..75",
"1249..753",
"593742168",
"687351492",
],
Status::Satisfied,
);
}
#[test]
#[traced_test]
fn test_sudoku_2() {
test_sudoku(
&[
"...2.5...",
".9....73.",
"..2..9.6.",
"2.....4.9",
"....7....",
"6.9.....1",
".8.4..1..",
".63....8.",
"...6.8...",
],
Status::Satisfied,
);
}
#[test]
#[traced_test]
fn test_sudoku_3() {
test_sudoku(
&[
"3..9.4..1",
"..2...4..",
".61...79.",
"6..247..5",
".........",
"2..836..4",
".46...23.",
"..9...6..",
"5..3.9..8",
],
Status::Satisfied,
);
}
#[test]
#[traced_test]
fn test_sudoku_4() {
test_sudoku(
&[
"....1....",
"3.14..86.",
"9..5..2..",
"7..16....",
".2.8.5.1.",
"....97..4",
"..3..4..6",
".48..69.7",
"....8....",
],
Status::Satisfied,
);
}
#[test]
#[traced_test]
fn test_sudoku_5() {
test_sudoku(
&[
"..4..3.7.",
".8..7....",
".7...82.5",
"4.....31.",
"9.......8",
".15.....4",
"1.69...3.",
"....2..6.",
".2.4..5..",
],
Status::Satisfied,
);
}
#[test]
#[traced_test]
fn test_sudoku_6() {
test_sudoku(
&[
".43.8.25.",
"6........",
".....1.94",
"9....4.7.",
"...6.8...",
".1.2....3",
"82.5.....",
"........5",
".34.9.71.",
],
Status::Satisfied,
);
}
}