From abb4cebce36a27298af44dbc28097ab1cf4a708d Mon Sep 17 00:00:00 2001 From: David Wang Date: Fri, 3 Mar 2017 07:26:05 +1100 Subject: [PATCH] Equality: bound variable range by using min/max of other variables. magicsquare_3x3: 873 -> 63 guesses. magicsquare_4x4: 13475456 -> 539910 guesses (all solutions). sendmoremoney_carry: 154 -> 6 guesses. sendmoremoney_naive: 633681 -> 4 guesses. zebra: 453 -> no guesswork! --- src/constraint/equality.rs | 87 +++++++++++++++++++++++++++++++++++++- src/puzzle.rs | 45 ++++++++++++++++++++ tests/sendmoremoney.rs | 6 +-- 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/src/constraint/equality.rs b/src/constraint/equality.rs index cf3df92..24d4f4d 100644 --- a/src/constraint/equality.rs +++ b/src/constraint/equality.rs @@ -1,6 +1,7 @@ //! Equality implementation. use ::{Constraint,LinExpr,PuzzleSearch,Val,VarToken}; +use intdiv::IntDiv; pub struct Equality { // The equation: 0 = constant + coef1 * var1 + coef2 * var2 + ... @@ -68,11 +69,82 @@ impl Constraint for Equality { true } + + fn on_updated(&self, search: &mut PuzzleSearch) -> bool { + let mut sum_min = self.eqn.constant; + let mut sum_max = self.eqn.constant; + + for (&var, &coef) in self.eqn.coef.iter() { + if let Some((min_val, max_val)) = search.get_min_max(var) { + if coef > 0 { + sum_min += coef * min_val; + sum_max += coef * max_val; + } else { + sum_min += coef * max_val; + sum_max += coef * min_val; + } + } else { + return false; + } + } + + // Minimum (maximum) of var can be bounded by summing the + // maximum (minimum) of everything else. Repeat until no + // changes further changes to the extremes found. + let mut iters = self.eqn.coef.len(); + let mut iter = self.eqn.coef.iter().cycle(); + while iters > 0 { + iters = iters - 1; + if !(sum_min <= 0 && 0 <= sum_max) { + return false; + } + + let (&var, &coef) = iter.next().expect("cycle"); + if search.is_assigned(var) { + continue; + } + + if let Some((min_val, max_val)) = search.get_min_max(var) { + let min_bnd; + let max_bnd; + + if coef > 0 { + min_bnd = (coef * max_val - sum_max).div_round_up(coef); + max_bnd = (coef * min_val - sum_min).div_round_down(coef); + } else { + min_bnd = (coef * max_val - sum_min).div_round_up(coef); + max_bnd = (coef * min_val - sum_max).div_round_down(coef); + } + + if min_val < min_bnd || max_bnd < max_val { + search.bound_candidate_range(var, min_bnd, max_bnd); + + if let Some((new_min, new_max)) = search.get_min_max(var) { + if coef > 0 { + sum_min = sum_min + coef * (new_min - min_val); + sum_max = sum_max + coef * (new_max - max_val); + } else { + sum_min = sum_min + coef * (new_max - max_val); + sum_max = sum_max + coef * (new_min - min_val); + } + } else { + return false; + } + + iters = self.eqn.coef.len(); + } + } else { + return false; + } + } + + true + } } #[cfg(test)] mod tests { - use ::Puzzle; + use ::{Puzzle,Val}; #[test] fn test_contradiction() { @@ -98,4 +170,17 @@ mod tests { assert_eq!(search[v0], 1); assert_eq!(search[v1], 3); } + + #[test] + fn test_reduce_range() { + let mut puzzle = Puzzle::new(); + let v0 = puzzle.new_var_with_candidates(&[1,2,3]); + let v1 = puzzle.new_var_with_candidates(&[3,4,5]); + + puzzle.equals(v0 + v1, 5); + + let search = puzzle.step().expect("contradiction"); + assert_eq!(search.get_unassigned(v0).collect::>(), &[1,2]); + assert_eq!(search.get_unassigned(v1).collect::>(), &[3,4]); + } } diff --git a/src/puzzle.rs b/src/puzzle.rs index a150ac4..45703a8 100644 --- a/src/puzzle.rs +++ b/src/puzzle.rs @@ -483,6 +483,22 @@ impl<'a> PuzzleSearch<'a> { } } + /// Get the minimum and maximum values for variable. + pub fn get_min_max(&self, var: VarToken) -> Option<(Val, Val)> { + let VarToken(idx) = var; + match &self.vars[idx] { + &VarState::Assigned(val) => Some((val, val)), + &VarState::Unassigned(ref cs) => match cs { + &Candidates::None => None, + &Candidates::Value(val) => Some((val, val)), + &Candidates::Set(ref rc) => { + rc.iter().cloned().min().into_iter() + .zip(rc.iter().cloned().max()).next() + } + }, + } + } + /// Set a variable to a known value. pub fn set_candidate(&mut self, var: VarToken, val: Val) { let VarToken(idx) = var; @@ -535,6 +551,35 @@ impl<'a> PuzzleSearch<'a> { } } + /// Bound an unassigned variable to the given range. + pub fn bound_candidate_range(&mut self, var: VarToken, min: Val, max: Val) { + let VarToken(idx) = var; + if let VarState::Unassigned(ref mut cs) = self.vars[idx] { + match cs { + &mut Candidates::None => return, + &mut Candidates::Value(v) => { + if !(min <= v && v <= max) { + *cs = Candidates::None; + self.wake.union_with(&self.puzzle.wake[idx]); + } + }, + &mut Candidates::Set(ref mut rc) => { + let &curr_min = rc.iter().min().expect("candidates"); + let &curr_max = rc.iter().max().expect("candidates"); + + if curr_min < min || max < curr_max { + let mut set = Rc::make_mut(rc); + *set = set.iter() + .filter(|&val| min <= *val && *val <= max) + .cloned() + .collect(); + self.wake.union_with(&self.puzzle.wake[idx]); + } + }, + } + } + } + /// Solve the puzzle, finding up to count solutions. fn solve(&mut self, count: usize, solutions: &mut Vec) { if !self.constrain() { diff --git a/tests/sendmoremoney.rs b/tests/sendmoremoney.rs index 65c7109..b7b02b7 100644 --- a/tests/sendmoremoney.rs +++ b/tests/sendmoremoney.rs @@ -67,18 +67,16 @@ fn sendmoremoney_carry() { sys.equals(c3 + s + m, 10 * m + o); let dict = sys.solve_unique().expect("solution"); - println!("sendmoremoney_carry: {} guesses", sys.num_guesses()); print_send_more_money(&dict, &vars); verify_send_more_money(&dict, &vars); + println!("sendmoremoney_carry: {} guesses", sys.num_guesses()); } -/* #[test] fn sendmoremoney_naive() { let (mut sys, vars) = make_send_more_money(); let dict = sys.solve_unique().expect("solution"); - println!("sendmoremoney_naive: {} guesses", sys.num_guesses()); print_send_more_money(&dict, &vars); verify_send_more_money(&dict, &vars); + println!("sendmoremoney_naive: {} guesses", sys.num_guesses()); } -*/