Skip to content

Commit 4fd2841

Browse files
committed
fix resume and add test
1 parent d0aff78 commit 4fd2841

6 files changed

Lines changed: 293 additions & 36 deletions

File tree

src/lib.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -478,17 +478,41 @@ impl Solution {
478478

479479
self.solver.deadline = time_limit.map(|d| Instant::now() + d);
480480

481-
let mut stop_reason = self.solver.initial_solve()?;
482-
483-
if stop_reason == StopReason::Finished && self.solver.has_integer_vars() {
481+
let has_bb_state = self.solver.bb_state.is_some();
482+
483+
let stop_reason = if has_bb_state {
484+
// We are resuming a branch-and-bound search. The solver is already
485+
// at the best integer solution found so far (or the LP relaxation if
486+
// none was found yet). Skip initial_solve and go straight to B&B.
487+
//
488+
// Take bb_state out before cloning so we don't deep-clone the
489+
// entire DFS stack (which contains Solution/Solver snapshots for
490+
// every pending branch). solve_integer will .take() it from
491+
// self.solver anyway.
492+
let bb_state = self.solver.bb_state.take();
484493
let cur_solution = Solution {
485494
num_vars: self.num_vars,
486495
direction: self.direction,
487496
solver: self.solver.clone(),
488497
stop_reason: StopReason::Finished,
489498
};
490-
stop_reason = self.solver.solve_integer(cur_solution, self.direction)?;
491-
}
499+
self.solver.bb_state = bb_state;
500+
self.solver.solve_integer(cur_solution, self.direction)?
501+
} else {
502+
// No persisted B&B state — resume the LP solve first.
503+
let mut sr = self.solver.initial_solve()?;
504+
505+
if sr == StopReason::Finished && self.solver.has_integer_vars() {
506+
let cur_solution = Solution {
507+
num_vars: self.num_vars,
508+
direction: self.direction,
509+
solver: self.solver.clone(),
510+
stop_reason: StopReason::Finished,
511+
};
512+
sr = self.solver.solve_integer(cur_solution, self.direction)?;
513+
}
514+
sr
515+
};
492516

493517
self.stop_reason = stop_reason;
494518
Ok(self)

src/problems_solvers/tsp.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
//disable clippy
22
//
33

4-
use crate::{ComparisonOp, LinearExpr, OptimizationDirection, StopReason, Variable};
5-
use core::time::Duration;
4+
use crate::{ComparisonOp, LinearExpr, OptimizationDirection, Variable};
65
use std::io;
76

87
/// Solve the Travelling Salesman Problem using integer linear programming with iterative subtour elimination.
@@ -52,16 +51,9 @@ pub fn solve_tsp(problem: &TspProblem) -> Tour {
5251
// need exponentially many constraints. Instead, we solve the problem, check for subtours
5352
// in the integer solution, add violated subtour elimination constraints, and re-solve.
5453
// The solver's built-in branch & bound handles integrality at each iteration.
55-
lp_problem.set_time_limit(Duration::from_secs(5));
5654

5755
loop {
58-
let mut solution = lp_problem.solve().unwrap();
59-
60-
if *solution.stop_reason() == StopReason::Limit {
61-
info!("time limit reached, resuming without limit");
62-
solution = solution.resume(None).unwrap();
63-
}
64-
56+
let solution = lp_problem.solve().unwrap();
6557
info!(
6658
"solved integer problem, obj. value: {:.2}",
6759
solution.objective(),

src/solver.rs

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ pub(crate) struct Solver {
8686
sq_norms_update_helper: Vec<f64>,
8787
inv_basis_row_coeffs: SparseVec,
8888
row_coeffs: ScatteredVec,
89+
90+
/// Persisted branch-and-bound state for resuming after a time limit.
91+
pub(crate) bb_state: Option<BranchAndBoundState>,
92+
}
93+
94+
#[derive(Clone, Debug)]
95+
pub(crate) struct BranchAndBoundState {
96+
dfs_stack: Vec<Step>,
97+
best_cost: f64,
98+
/// Whether `self` (the Solver) represents a valid best integer solution found so far.
99+
has_best: bool,
89100
}
90101

91102
#[derive(Clone, Debug)]
@@ -407,6 +418,7 @@ impl Solver {
407418
sq_norms_update_helper,
408419
inv_basis_row_coeffs: SparseVec::new(),
409420
row_coeffs: ScatteredVec::empty(num_total_vars - num_constraints),
421+
bb_state: None,
410422
};
411423

412424
debug!(
@@ -556,34 +568,65 @@ impl Solver {
556568
cur_solution: Solution,
557569
direction: OptimizationDirection,
558570
) -> Result<StopReason, Error> {
559-
let mut best_cost = if direction == OptimizationDirection::Maximize {
560-
f64::NEG_INFINITY
561-
} else {
562-
f64::INFINITY
563-
};
564-
let mut best_solution = None;
565-
debug!("{:?}", cur_solution.iter().collect::<Vec<_>>());
566-
let mut dfs_stack =
567-
if let Some(var) = choose_branch_var(&cur_solution, &self.orig_var_domains) {
571+
// If we have persisted B&B state from a previous time-limited run, restore it.
572+
let (mut dfs_stack, mut best_cost, mut best_solution) =
573+
if let Some(state) = self.bb_state.take() {
568574
debug!(
569-
"starting branch&bound, current obj. value: {:.2}",
570-
self.cur_obj_val
575+
"resuming branch&bound, dfs_stack size: {}, best_cost: {:.2}, has_best: {}",
576+
state.dfs_stack.len(),
577+
state.best_cost,
578+
state.has_best
571579
);
572-
new_steps(cur_solution, var, &self.orig_var_domains)
580+
let best_solution = if state.has_best {
581+
// self was restored to the best solution's solver state before returning,
582+
// so cur_solution (built from self) represents the best solution.
583+
Some(cur_solution)
584+
} else {
585+
None
586+
};
587+
(state.dfs_stack, state.best_cost, best_solution)
573588
} else {
574-
debug!(
575-
"found optimal solution with initial relaxation! cost: {:.2}",
576-
self.cur_obj_val
577-
);
578-
return Ok(StopReason::Finished);
589+
// Fresh start
590+
let best_cost = if direction == OptimizationDirection::Maximize {
591+
f64::NEG_INFINITY
592+
} else {
593+
f64::INFINITY
594+
};
595+
let best_solution = None;
596+
debug!("{:?}", cur_solution.iter().collect::<Vec<_>>());
597+
let dfs_stack =
598+
if let Some(var) = choose_branch_var(&cur_solution, &self.orig_var_domains) {
599+
debug!(
600+
"starting branch&bound, current obj. value: {:.2}",
601+
self.cur_obj_val
602+
);
603+
new_steps(cur_solution, var, &self.orig_var_domains)
604+
} else {
605+
debug!(
606+
"found optimal solution with initial relaxation! cost: {:.2}",
607+
self.cur_obj_val
608+
);
609+
return Ok(StopReason::Finished);
610+
};
611+
(dfs_stack, best_cost, best_solution)
579612
};
580613

614+
// Cache orig_var_domains locally so the B&B loop never depends on self's
615+
// mutable state (self may be overwritten with the best solution's solver).
616+
let orig_var_domains = self.orig_var_domains.clone();
617+
581618
for iter in 0.. {
582619
if iter % 100 == 0 && check_deadline(&self.deadline) == StopReason::Limit {
620+
let has_best = best_solution.is_some();
583621
if let Some(solution) = best_solution {
584622
let solution: Solution = solution;
585623
*self = solution.solver;
586624
}
625+
self.bb_state = Some(BranchAndBoundState {
626+
dfs_stack,
627+
best_cost,
628+
has_best,
629+
});
587630
return Ok(StopReason::Limit);
588631
}
589632

@@ -593,6 +636,14 @@ impl Solver {
593636
None => break,
594637
};
595638
let mut cur_solution = cur_step.start_solution.clone();
639+
// Propagate the current deadline to the cloned solver. The step's
640+
// start_solution was captured earlier (possibly in a previous
641+
// time-limited run) and its deadline may have expired. Without this,
642+
// `add_constraint` / `fix_var` → `restore_feasibility()` would see
643+
// the stale deadline, return `Ok(StopReason::Limit)` immediately
644+
// without performing any dual-simplex pivots, and leave the solver
645+
// in a primal-infeasible state with garbage variable values.
646+
cur_solution.solver.deadline = self.deadline;
596647
let branch_direction = match cur_step.kind {
597648
BranchKind::Floor => ComparisonOp::Le,
598649
BranchKind::Ceil => ComparisonOp::Ge,
@@ -630,9 +681,9 @@ impl Solver {
630681
continue;
631682
}
632683

633-
if let Some(var) = choose_branch_var(&cur_solution, &self.orig_var_domains) {
684+
if let Some(var) = choose_branch_var(&cur_solution, &orig_var_domains) {
634685
// Search deeper
635-
let steps = new_steps(cur_solution, var, &self.orig_var_domains);
686+
let steps = new_steps(cur_solution, var, &orig_var_domains);
636687
dfs_stack.extend(steps);
637688
} else {
638689
// Found integral solution

src/tests/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ mod aoc2;
55
mod general;
66

77
mod tsp;
8+
9+
mod resume;

0 commit comments

Comments
 (0)