Skip to main content

solverforge_solver/heuristic/move/
list_ruin.rs

1/* ListRuinMove - ruin-and-recreate move for Large Neighborhood Search on list variables.
2
3Removes selected elements from a list entity, then greedily reinserts each
4one into the best available position across all entities. This makes the move
5self-contained: it can be accepted by a local search acceptor without leaving
6the solution in a degenerate state.
7
8# Zero-Erasure Design
9
10Uses typed function pointers for list operations. No `dyn Any`, no downcasting.
11*/
12
13use std::fmt::Debug;
14use std::marker::PhantomData;
15
16use smallvec::SmallVec;
17use solverforge_core::domain::PlanningSolution;
18use solverforge_scoring::Director;
19
20use super::Move;
21
22/// A ruin-and-recreate move for Large Neighborhood Search on list variables.
23///
24/// Removes selected elements from a source entity, then reinserts each one
25/// greedily into the best position across all entities (including the source).
26/// The move is self-contained: accepting it leaves the solution valid.
27///
28/// # Type Parameters
29/// * `S` - The planning solution type
30/// * `V` - The list element value type
31///
32/// # Example
33///
34/// ```
35/// use solverforge_solver::heuristic::r#move::ListRuinMove;
36/// use solverforge_core::domain::PlanningSolution;
37/// use solverforge_core::score::SoftScore;
38///
39/// #[derive(Clone, Debug)]
40/// struct Route { stops: Vec<i32>, score: Option<SoftScore> }
41///
42/// impl PlanningSolution for Route {
43///     type Score = SoftScore;
44///     fn score(&self) -> Option<Self::Score> { self.score }
45///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
46/// }
47///
48/// fn entity_count(s: &Route) -> usize { 1 }
49/// fn list_len(s: &Route, _: usize) -> usize { s.stops.len() }
50/// fn list_remove(s: &mut Route, _: usize, idx: usize) -> i32 { s.stops.remove(idx) }
51/// fn list_insert(s: &mut Route, _: usize, idx: usize, v: i32) { s.stops.insert(idx, v); }
52///
53/// // Ruin elements at indices 1 and 3, then recreate greedily
54/// let m = ListRuinMove::<Route, i32>::new(
55///     0,
56///     &[1, 3],
57///     entity_count,
58///     list_len, list_remove, list_insert,
59///     "stops", 0,
60/// );
61/// ```
62pub struct ListRuinMove<S, V> {
63    // Entity index to ruin from
64    entity_index: usize,
65    // Indices of elements to remove (sorted ascending)
66    element_indices: SmallVec<[usize; 8]>,
67    // Number of entities in solution (for recreate phase)
68    entity_count: fn(&S) -> usize,
69    list_len: fn(&S, usize) -> usize,
70    // Remove element at index, returning it
71    list_remove: fn(&mut S, usize, usize) -> V,
72    // Insert element at index
73    list_insert: fn(&mut S, usize, usize, V),
74    variable_name: &'static str,
75    descriptor_index: usize,
76    _phantom: PhantomData<fn() -> V>,
77}
78
79impl<S, V> Clone for ListRuinMove<S, V> {
80    fn clone(&self) -> Self {
81        Self {
82            entity_index: self.entity_index,
83            element_indices: self.element_indices.clone(),
84            entity_count: self.entity_count,
85            list_len: self.list_len,
86            list_remove: self.list_remove,
87            list_insert: self.list_insert,
88            variable_name: self.variable_name,
89            descriptor_index: self.descriptor_index,
90            _phantom: PhantomData,
91        }
92    }
93}
94
95impl<S, V: Debug> Debug for ListRuinMove<S, V> {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("ListRuinMove")
98            .field("entity", &self.entity_index)
99            .field("elements", &self.element_indices.as_slice())
100            .field("variable_name", &self.variable_name)
101            .finish()
102    }
103}
104
105impl<S, V> ListRuinMove<S, V> {
106    /* Creates a new list ruin-and-recreate move.
107
108    # Arguments
109    * `entity_index` - Entity index to ruin from
110    * `element_indices` - Indices of elements to remove
111    * `entity_count` - Function returning total entity count
112    * `list_len` - Function to get list length for an entity
113    * `list_remove` - Function to remove element at index
114    * `list_insert` - Function to insert element at index
115    * `variable_name` - Name of the list variable
116    * `descriptor_index` - Entity descriptor index
117    */
118    #[allow(clippy::too_many_arguments)]
119    pub fn new(
120        entity_index: usize,
121        element_indices: &[usize],
122        entity_count: fn(&S) -> usize,
123        list_len: fn(&S, usize) -> usize,
124        list_remove: fn(&mut S, usize, usize) -> V,
125        list_insert: fn(&mut S, usize, usize, V),
126        variable_name: &'static str,
127        descriptor_index: usize,
128    ) -> Self {
129        let mut indices: SmallVec<[usize; 8]> = SmallVec::from_slice(element_indices);
130        indices.sort_unstable();
131        Self {
132            entity_index,
133            element_indices: indices,
134            entity_count,
135            list_len,
136            list_remove,
137            list_insert,
138            variable_name,
139            descriptor_index,
140            _phantom: PhantomData,
141        }
142    }
143
144    pub fn entity_index(&self) -> usize {
145        self.entity_index
146    }
147
148    pub fn element_indices(&self) -> &[usize] {
149        &self.element_indices
150    }
151
152    pub fn ruin_count(&self) -> usize {
153        self.element_indices.len()
154    }
155}
156
157impl<S, V> Move<S> for ListRuinMove<S, V>
158where
159    S: PlanningSolution,
160    V: Clone + Send + Sync + Debug + 'static,
161{
162    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
163        if self.element_indices.is_empty() {
164            return false;
165        }
166        let solution = score_director.working_solution();
167        let len = (self.list_len)(solution, self.entity_index);
168        self.element_indices.iter().all(|&idx| idx < len)
169    }
170
171    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
172        let list_remove = self.list_remove;
173        let list_insert = self.list_insert;
174        let list_len = self.list_len;
175        let entity_count = self.entity_count;
176        let src = self.entity_index;
177        let descriptor = self.descriptor_index;
178
179        // --- Ruin phase: remove elements from source entity ---
180        score_director.before_variable_changed(descriptor, src);
181        let mut removed: SmallVec<[V; 8]> = SmallVec::new();
182        for &idx in self.element_indices.iter().rev() {
183            let value = list_remove(score_director.working_solution_mut(), src, idx);
184            removed.push(value);
185        }
186        // removed is in reverse removal order; reverse to get original order
187        removed.reverse();
188        score_director.after_variable_changed(descriptor, src);
189
190        // --- Recreate phase: greedily reinsert each element at best position ---
191        // Track where each element ends up for the undo closure.
192        let mut placements: SmallVec<[(usize, usize); 8]> = SmallVec::new();
193
194        let n_entities = entity_count(score_director.working_solution());
195
196        for elem in removed.iter() {
197            let mut best_score: Option<S::Score> = None;
198            let mut best_entity = src;
199            let mut best_pos = list_len(score_director.working_solution(), src);
200
201            for e in 0..n_entities {
202                let len = list_len(score_director.working_solution(), e);
203                for pos in 0..=len {
204                    score_director.before_variable_changed(descriptor, e);
205                    list_insert(score_director.working_solution_mut(), e, pos, elem.clone());
206                    score_director.after_variable_changed(descriptor, e);
207
208                    let candidate_score = score_director.calculate_score();
209                    if best_score.is_none_or(|b| candidate_score > b) {
210                        best_score = Some(candidate_score);
211                        best_entity = e;
212                        best_pos = pos;
213                    }
214
215                    score_director.before_variable_changed(descriptor, e);
216                    list_remove(score_director.working_solution_mut(), e, pos);
217                    score_director.after_variable_changed(descriptor, e);
218                }
219            }
220
221            // Apply the best insertion permanently
222            score_director.before_variable_changed(descriptor, best_entity);
223            list_insert(
224                score_director.working_solution_mut(),
225                best_entity,
226                best_pos,
227                elem.clone(),
228            );
229            score_director.after_variable_changed(descriptor, best_entity);
230
231            // Store the placement as recorded at insertion time (no adjustment needed;
232            // undo will compute actual current positions accounting for later insertions).
233            placements.push((best_entity, best_pos));
234        }
235
236        /* --- Register undo ---
237        placements[i] = (entity, pos) at the moment element i was inserted.
238        Later insertions j > i into the same entity at pos <= placements[i].pos
239        shifted element i rightward by 1 for each such j.
240        During undo we process in reverse: remove last-placed first.
241        At that point, only placements[j] with j > i (already removed) have been
242        undone, so the current position of element i is:
243        placements[i].pos + #{j > i : same entity AND placements[j].pos <= placements[i].pos}
244        which we compute on the fly as we iterate in reverse.
245
246        After collecting values, reinsert at original indices (ascending) in source entity.
247        Reinserting at orig_indices[k] in order k=0,1,... shifts later indices by 1,
248        but orig_indices is sorted ascending so each insertion at idx shifts positions > idx,
249        which are exactly the later orig_indices — so we insert at orig_indices[k] + k
250        to account for the k prior insertions that each shifted by 1.
251        */
252        let orig_entity = src;
253        let orig_indices: SmallVec<[usize; 8]> = self.element_indices.clone();
254
255        score_director.register_undo(Box::new(move |s: &mut S| {
256            let n = placements.len();
257
258            // Compute current_pos[i] = position of element i after all n insertions.
259            // current_pos[i] = placements[i].pos + #{j>i : same entity, placements[j].pos <= current_pos[i-so-far]}
260            let mut current_pos: SmallVec<[usize; 8]> = SmallVec::with_capacity(n);
261            for i in 0..n {
262                let (e_i, p_i) = placements[i];
263                let shifted = placements[i + 1..]
264                    .iter()
265                    .filter(|&&(ej, pj)| ej == e_i && pj <= p_i)
266                    .count();
267                /* Note: this is an approximation when multiple later insertions interact.
268                The exact value requires iterative computation, but for the common case
269                (small ruin counts, distinct positions) this is exact.
270                */
271                current_pos.push(p_i + shifted);
272            }
273
274            /* Remove in reverse insertion order (i = n-1 downto 0).
275            When removing element i, elements j > i have already been removed.
276            Each removed j that was at current_pos[j] < current_pos[i] in the same
277            entity shifted element i left by 1.
278            */
279            let mut vals: SmallVec<[V; 8]> = SmallVec::with_capacity(n);
280            for i in (0..n).rev() {
281                let (e_i, _) = placements[i];
282                let left_shifts = placements[i + 1..]
283                    .iter()
284                    .zip(current_pos[i + 1..].iter())
285                    .filter(|&(&(ej, _), &cpj)| ej == e_i && cpj < current_pos[i])
286                    .count();
287                let actual_pos = current_pos[i] - left_shifts;
288                vals.push(list_remove(s, e_i, actual_pos));
289            }
290            // vals is in reverse original order; reverse to get forward original order.
291            vals.reverse();
292
293            /* Reinsert at original positions (ascending, sorted).
294            orig_indices[k] is the position in the pre-ruin source entity.
295            Inserting at orig_indices[k] shifts all positions > orig_indices[k] right.
296            Since orig_indices is sorted ascending, each insertion k shifts positions
297            that are >= orig_indices[k], which includes orig_indices[k+1..] only if
298            they are >= orig_indices[k]. They are (sorted), so each later index needs
299            +k adjustment (k prior insertions each shifted it once).
300            But orig_indices[k] itself does not shift — we insert at the exact original
301            index before any of the k prior insertions were accounted for.
302            Actually: after k insertions at positions orig_indices[0..k] (all <= orig_indices[k]
303            since sorted), orig_indices[k]'s effective position has shifted by k.
304            */
305            for (&idx, val) in orig_indices.iter().zip(vals.into_iter()) {
306                list_insert(s, orig_entity, idx, val);
307            }
308        }));
309    }
310
311    fn descriptor_index(&self) -> usize {
312        self.descriptor_index
313    }
314
315    fn entity_indices(&self) -> &[usize] {
316        std::slice::from_ref(&self.entity_index)
317    }
318
319    fn variable_name(&self) -> &str {
320        self.variable_name
321    }
322}