Skip to main content

csp_solver/solver/
backtrack.rs

1//! Chronological backtracking search.
2
3use crate::constraint::VarId;
4use crate::domain::Domain;
5use crate::ordering::{self, Ordering};
6use crate::solver::ac3;
7use crate::solver::propagate;
8use crate::solver::SearchContext;
9use crate::variable::Variable;
10use crate::Pruning;
11
12/// Solution type: a vector of values indexed by variable ID.
13pub type Solution<D> = Vec<<D as Domain>::Value>;
14
15/// Run backtracking search. Returns up to `max_solutions` solutions.
16pub fn backtrack_search<D: Domain>(
17    variables: &mut [Variable<D>],
18    constraints: &[crate::constraint::ConstraintEnum<D>],
19    adjacency: &crate::adjacency::Adjacency,
20    config: &BacktrackConfig,
21    stats: &mut crate::SolveStats,
22) -> Vec<Solution<D>>
23where
24    D::Value: PartialEq,
25{
26    let num_vars = variables.len();
27    let mut assignment: Vec<Option<D::Value>> = vec![None; num_vars];
28    let mut stack: Vec<VarId> = (0..num_vars as u32).collect();
29    let mut solutions = Vec::new();
30    let mut ctx = SearchContext { variables, constraints, adjacency, stats };
31
32    backtrack_recurse(
33        &mut ctx, config,
34        &mut assignment, &mut stack, &mut solutions, 0,
35    );
36
37    solutions
38}
39
40/// Run backtracking search with pre-assigned variables.
41pub fn backtrack_search_with_given<D: Domain>(
42    variables: &mut [Variable<D>],
43    constraints: &[crate::constraint::ConstraintEnum<D>],
44    adjacency: &crate::adjacency::Adjacency,
45    config: &BacktrackConfig,
46    stats: &mut crate::SolveStats,
47    given: &[(VarId, D::Value)],
48) -> Vec<Solution<D>>
49where
50    D::Value: PartialEq,
51{
52    let num_vars = variables.len();
53    let mut assignment: Vec<Option<D::Value>> = vec![None; num_vars];
54
55    for (var, val) in given {
56        assignment[*var as usize] = Some(val.clone());
57    }
58
59    let mut stack: Vec<VarId> = (0..num_vars as u32)
60        .filter(|v| assignment[*v as usize].is_none())
61        .collect();
62
63    let mut solutions = Vec::new();
64    let mut ctx = SearchContext { variables, constraints, adjacency, stats };
65
66    backtrack_recurse(
67        &mut ctx, config,
68        &mut assignment, &mut stack, &mut solutions, 0,
69    );
70
71    solutions
72}
73
74/// Configuration for backtracking search.
75pub struct BacktrackConfig {
76    pub pruning: Pruning,
77    pub ordering: Ordering,
78    pub max_solutions: usize,
79    pub constraint_weights: Vec<f64>,
80    pub var_constraint_ids: Vec<Vec<usize>>,
81    /// Maximum number of search nodes before aborting early.
82    /// See [`crate::SolveConfig::node_budget`].
83    pub node_budget: Option<u64>,
84}
85
86fn backtrack_recurse<D: Domain>(
87    ctx: &mut SearchContext<'_, D>,
88    config: &BacktrackConfig,
89    assignment: &mut Vec<Option<D::Value>>,
90    stack: &mut Vec<VarId>,
91    solutions: &mut Vec<Solution<D>>,
92    depth: usize,
93) -> bool
94where
95    D::Value: PartialEq,
96{
97    if stack.is_empty() {
98        let sol: Solution<D> = assignment
99            .iter()
100            .map(|v| v.as_ref().unwrap().clone())
101            .collect();
102        solutions.push(sol);
103        return solutions.len() >= config.max_solutions;
104    }
105
106    // Budget guard: abort early if the search has exceeded its node
107    // budget. Return the `done` signal so the recursion unwinds
108    // cleanly, preserving whatever solutions have been found so far.
109    // Checked before `nodes_explored += 1` so the post-budget node is
110    // never counted and the flag is set exactly once per search.
111    if let Some(budget) = config.node_budget
112        && ctx.stats.nodes_explored >= budget
113    {
114        ctx.stats.budget_exceeded = true;
115        return true;
116    }
117
118    ctx.stats.nodes_explored += 1;
119
120    let idx = ordering::select_variable(
121        stack, ctx.variables, config.ordering,
122        &config.constraint_weights, &config.var_constraint_ids,
123    )
124    .unwrap();
125
126    let var = stack.swap_remove(idx);
127    let values: Vec<_> = ctx.variables[var as usize].domain.iter().collect();
128
129    for val in values {
130        assignment[var as usize] = Some(val.clone());
131
132        // Restrict domain to singleton {val} so AC-3 revise() sees it.
133        ctx.variables[var as usize].restrict_to(&val, depth);
134
135        let mut valid = true;
136        for &ci in ctx.adjacency.constraints_for(var) {
137            let ci = ci as usize;
138            let scope = ctx.constraints[ci].scope();
139            if scope.iter().all(|&v| assignment[v as usize].is_some())
140                && !ctx.constraints[ci].check(assignment)
141            {
142                valid = false;
143                break;
144            }
145        }
146
147        if valid {
148            let dwo = match config.pruning {
149                Pruning::None => false,
150                Pruning::ForwardChecking => propagate::forward_check(
151                    var, ctx.variables, ctx.constraints, ctx.adjacency,
152                    assignment.as_mut_slice(), ctx.stats, depth,
153                ),
154                Pruning::Ac3 => ac3::ac3_from_variable(
155                    var, ctx.variables, ctx.constraints, ctx.adjacency,
156                    assignment, ctx.stats, depth,
157                ),
158                Pruning::AcFc => propagate::ac_fc(
159                    var, ctx.variables, ctx.constraints, ctx.adjacency,
160                    assignment.as_mut_slice(), ctx.stats, depth,
161                ),
162            };
163
164            if !dwo
165                && backtrack_recurse(
166                    ctx, config,
167                    assignment, stack, solutions, depth + 1,
168                )
169            {
170                return true;
171            }
172        }
173
174        ctx.stats.backtracks += 1;
175        assignment[var as usize] = None;
176        for v in ctx.variables.iter_mut() {
177            v.restore(depth);
178        }
179    }
180
181    stack.push(var);
182    false
183}