Skip to main content

csp_solver/solver/
backjump.rs

1//! Conflict-directed backjumping with bitset conflict tracking.
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
12use super::backtrack::Solution;
13
14/// Run backjumping search. Returns up to `max_solutions` solutions.
15pub fn backjump_search<D: Domain>(
16    variables: &mut [Variable<D>],
17    constraints: &[crate::constraint::ConstraintEnum<D>],
18    adjacency: &crate::adjacency::Adjacency,
19    config: &BackjumpConfig,
20    stats: &mut crate::SolveStats,
21) -> Vec<Solution<D>>
22where
23    D::Value: PartialEq,
24{
25    let num_vars = variables.len();
26    let mut assignment: Vec<Option<D::Value>> = vec![None; num_vars];
27    let mut stack: Vec<VarId> = (0..num_vars as u32).collect();
28    let mut solutions = Vec::new();
29    let mut ctx = SearchContext { variables, constraints, adjacency, stats };
30    let mut conflict = ConflictState {
31        assigned_order: Vec::new(),
32        membership: vec![false; num_vars],
33    };
34
35    backjump_recurse(
36        &mut ctx, config,
37        &mut assignment, &mut stack, &mut solutions,
38        &mut conflict, 0,
39    );
40
41    solutions
42}
43
44enum BackjumpResult {
45    Continue,
46    Done,
47    JumpTo(usize),
48}
49
50/// Configuration for backjumping search.
51pub struct BackjumpConfig {
52    pub pruning: Pruning,
53    pub ordering: Ordering,
54    pub max_solutions: usize,
55    pub constraint_weights: Vec<f64>,
56    pub var_constraint_ids: Vec<Vec<usize>>,
57    /// Maximum number of search nodes before aborting early.
58    /// See [`crate::SolveConfig::node_budget`].
59    pub node_budget: Option<u64>,
60}
61
62/// Mutable state for conflict-directed backjumping, threaded through the
63/// recursive search to avoid per-function argument bloat.
64struct ConflictState {
65    /// Variables in assignment order (stack of decisions).
66    assigned_order: Vec<VarId>,
67    /// Bitset: `membership[v]` is true iff `v` is in the current conflict set.
68    membership: Vec<bool>,
69}
70
71fn backjump_recurse<D: Domain>(
72    ctx: &mut SearchContext<'_, D>,
73    config: &BackjumpConfig,
74    assignment: &mut Vec<Option<D::Value>>,
75    stack: &mut Vec<VarId>,
76    solutions: &mut Vec<Solution<D>>,
77    conflict: &mut ConflictState,
78    depth: usize,
79) -> BackjumpResult
80where
81    D::Value: PartialEq,
82{
83    if stack.is_empty() {
84        let sol: Solution<D> = assignment
85            .iter()
86            .map(|v| v.as_ref().unwrap().clone())
87            .collect();
88        solutions.push(sol);
89        if solutions.len() >= config.max_solutions {
90            return BackjumpResult::Done;
91        }
92        return BackjumpResult::Continue;
93    }
94
95    // Budget guard — see `backtrack.rs::backtrack_recurse` for details.
96    if let Some(budget) = config.node_budget
97        && ctx.stats.nodes_explored >= budget
98    {
99        ctx.stats.budget_exceeded = true;
100        return BackjumpResult::Done;
101    }
102
103    ctx.stats.nodes_explored += 1;
104
105    let idx = ordering::select_variable(
106        stack, ctx.variables, config.ordering,
107        &config.constraint_weights, &config.var_constraint_ids,
108    )
109    .unwrap();
110
111    let var = stack.swap_remove(idx);
112    conflict.assigned_order.push(var);
113
114    // Bitset-backed conflict set
115    let mut conflict_vars: Vec<VarId> = Vec::new();
116
117    let values: Vec<_> = ctx.variables[var as usize].domain.iter().collect();
118    let mut exhausted = true;
119
120    for val in values {
121        assignment[var as usize] = Some(val.clone());
122
123        // Restrict domain to singleton {val} for AC-3.
124        ctx.variables[var as usize].restrict_to(&val, depth);
125
126        let mut valid = true;
127        for &ci in ctx.adjacency.constraints_for(var) {
128            let ci = ci as usize;
129            let scope = ctx.constraints[ci].scope();
130            if scope.iter().all(|&v| assignment[v as usize].is_some())
131                && !ctx.constraints[ci].check(assignment)
132            {
133                valid = false;
134                for &v in scope {
135                    if v != var
136                        && assignment[v as usize].is_some()
137                        && !conflict.membership[v as usize]
138                    {
139                        conflict.membership[v as usize] = true;
140                        conflict_vars.push(v);
141                    }
142                }
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                for &neighbor in ctx.adjacency.neighbors_of_var(var) {
166                    if assignment[neighbor as usize].is_some()
167                        && !conflict.membership[neighbor as usize]
168                    {
169                        conflict.membership[neighbor as usize] = true;
170                        conflict_vars.push(neighbor);
171                    }
172                }
173            } else {
174                match backjump_recurse(
175                    ctx, config,
176                    assignment, stack, solutions,
177                    conflict, depth + 1,
178                ) {
179                    BackjumpResult::Done => return BackjumpResult::Done,
180                    BackjumpResult::Continue => {
181                        exhausted = false;
182                    }
183                    BackjumpResult::JumpTo(target_depth) => {
184                        ctx.stats.backtracks += 1;
185                        assignment[var as usize] = None;
186                        for v in ctx.variables.iter_mut() {
187                            v.restore(depth);
188                        }
189                        conflict.assigned_order.pop();
190                        stack.push(var);
191
192                        if target_depth < depth {
193                            for &cv in &conflict_vars {
194                                conflict.membership[cv as usize] = false;
195                            }
196                            return BackjumpResult::JumpTo(target_depth);
197                        }
198                        continue;
199                    }
200                }
201            }
202        }
203
204        ctx.stats.backtracks += 1;
205        assignment[var as usize] = None;
206        for v in ctx.variables.iter_mut() {
207            v.restore(depth);
208        }
209    }
210
211    conflict.assigned_order.pop();
212    stack.push(var);
213
214    let result = if exhausted && !conflict_vars.is_empty() {
215        // Single-pass scan for most recent conflict variable
216        let mut max_depth = 0;
217        let mut found = false;
218        for (pos, &av) in conflict.assigned_order.iter().enumerate() {
219            if conflict.membership[av as usize] && pos >= max_depth {
220                max_depth = pos;
221                found = true;
222            }
223        }
224        if found && max_depth < depth {
225            BackjumpResult::JumpTo(max_depth)
226        } else {
227            BackjumpResult::Continue
228        }
229    } else {
230        BackjumpResult::Continue
231    };
232
233    for &cv in &conflict_vars {
234        conflict.membership[cv as usize] = false;
235    }
236
237    result
238}