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
157pub(crate) fn final_positions_after_insertions(
158    placements: &SmallVec<[(usize, usize); 8]>,
159) -> SmallVec<[usize; 8]> {
160    let mut current_positions: SmallVec<[usize; 8]> = SmallVec::with_capacity(placements.len());
161
162    for i in 0..placements.len() {
163        let (entity_i, insert_pos_i) = placements[i];
164
165        for j in 0..i {
166            let (entity_j, _) = placements[j];
167            if entity_j == entity_i && current_positions[j] >= insert_pos_i {
168                current_positions[j] += 1;
169            }
170        }
171
172        current_positions.push(insert_pos_i);
173    }
174
175    current_positions
176}
177
178impl<S, V> Move<S> for ListRuinMove<S, V>
179where
180    S: PlanningSolution,
181    V: Clone + Send + Sync + Debug + 'static,
182{
183    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
184        if self.element_indices.is_empty() {
185            return false;
186        }
187        let solution = score_director.working_solution();
188        let len = (self.list_len)(solution, self.entity_index);
189        self.element_indices.iter().all(|&idx| idx < len)
190    }
191
192    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
193        let list_remove = self.list_remove;
194        let list_insert = self.list_insert;
195        let list_len = self.list_len;
196        let entity_count = self.entity_count;
197        let src = self.entity_index;
198        let descriptor = self.descriptor_index;
199
200        // --- Ruin phase: remove elements from source entity ---
201        score_director.before_variable_changed(descriptor, src);
202        let mut removed: SmallVec<[V; 8]> = SmallVec::new();
203        for &idx in self.element_indices.iter().rev() {
204            let value = list_remove(score_director.working_solution_mut(), src, idx);
205            removed.push(value);
206        }
207        // removed is in reverse removal order; reverse to get original order
208        removed.reverse();
209        score_director.after_variable_changed(descriptor, src);
210
211        // --- Recreate phase: greedily reinsert each element at best position ---
212        // Track where each element ends up for the undo closure.
213        let mut placements: SmallVec<[(usize, usize); 8]> = SmallVec::new();
214
215        let n_entities = entity_count(score_director.working_solution());
216
217        for elem in removed.iter() {
218            let mut best_score: Option<S::Score> = None;
219            let mut best_entity = src;
220            let mut best_pos = list_len(score_director.working_solution(), src);
221
222            for e in 0..n_entities {
223                let len = list_len(score_director.working_solution(), e);
224                for pos in 0..=len {
225                    score_director.before_variable_changed(descriptor, e);
226                    list_insert(score_director.working_solution_mut(), e, pos, elem.clone());
227                    score_director.after_variable_changed(descriptor, e);
228
229                    let candidate_score = score_director.calculate_score();
230                    if best_score.is_none_or(|b| candidate_score > b) {
231                        best_score = Some(candidate_score);
232                        best_entity = e;
233                        best_pos = pos;
234                    }
235
236                    score_director.before_variable_changed(descriptor, e);
237                    list_remove(score_director.working_solution_mut(), e, pos);
238                    score_director.after_variable_changed(descriptor, e);
239                }
240            }
241
242            // Apply the best insertion permanently
243            score_director.before_variable_changed(descriptor, best_entity);
244            list_insert(
245                score_director.working_solution_mut(),
246                best_entity,
247                best_pos,
248                elem.clone(),
249            );
250            score_director.after_variable_changed(descriptor, best_entity);
251
252            // Store the placement as recorded at insertion time (no adjustment needed;
253            // undo will compute actual current positions accounting for later insertions).
254            placements.push((best_entity, best_pos));
255        }
256
257        /* --- Register undo ---
258        placements[i] = (entity, pos) at the moment element i was inserted.
259        Later insertions j > i into the same entity at pos <= placements[i].pos
260        shifted element i rightward by 1 for each such j.
261        During undo we process in reverse: remove last-placed first.
262        At that point, only placements[j] with j > i (already removed) have been
263        undone, so the current position of element i is:
264        placements[i].pos + #{j > i : same entity AND placements[j].pos <= placements[i].pos}
265        which we compute on the fly as we iterate in reverse.
266
267        After collecting values, reinsert at original indices (ascending) in source entity.
268        Reinserting at orig_indices[k] in order k=0,1,... shifts later indices by 1,
269        but orig_indices is sorted ascending so each insertion at idx shifts positions > idx,
270        which are exactly the later orig_indices — so we insert at orig_indices[k] + k
271        to account for the k prior insertions that each shifted by 1.
272        */
273        let orig_entity = src;
274        let orig_indices: SmallVec<[usize; 8]> = self.element_indices.clone();
275
276        score_director.register_undo(Box::new(move |s: &mut S| {
277            let n = placements.len();
278            let mut current_pos = final_positions_after_insertions(&placements);
279
280            /* Remove in reverse insertion order (i = n-1 downto 0).
281            When removing element i, elements j > i have already been removed.
282            Any earlier element in the same entity that currently sits after the
283            removed position shifts left by one.
284            */
285            let mut vals: SmallVec<[V; 8]> = SmallVec::with_capacity(n);
286            for i in (0..n).rev() {
287                let (e_i, _) = placements[i];
288                let actual_pos = current_pos[i];
289                vals.push(list_remove(s, e_i, actual_pos));
290
291                for j in 0..i {
292                    let (e_j, _) = placements[j];
293                    if e_j == e_i && current_pos[j] > actual_pos {
294                        current_pos[j] -= 1;
295                    }
296                }
297            }
298            // vals is in reverse original order; reverse to get forward original order.
299            vals.reverse();
300
301            /* Reinsert at original positions (ascending, sorted).
302            orig_indices[k] is the position in the pre-ruin source entity.
303            Inserting at orig_indices[k] shifts all positions > orig_indices[k] right.
304            Since orig_indices is sorted ascending, each insertion k shifts positions
305            that are >= orig_indices[k], which includes orig_indices[k+1..] only if
306            they are >= orig_indices[k]. They are (sorted), so each later index needs
307            +k adjustment (k prior insertions each shifted it once).
308            But orig_indices[k] itself does not shift — we insert at the exact original
309            index before any of the k prior insertions were accounted for.
310            Actually: after k insertions at positions orig_indices[0..k] (all <= orig_indices[k]
311            since sorted), orig_indices[k]'s effective position has shifted by k.
312            */
313            for (&idx, val) in orig_indices.iter().zip(vals.into_iter()) {
314                list_insert(s, orig_entity, idx, val);
315            }
316        }));
317    }
318
319    fn descriptor_index(&self) -> usize {
320        self.descriptor_index
321    }
322
323    fn entity_indices(&self) -> &[usize] {
324        std::slice::from_ref(&self.entity_index)
325    }
326
327    fn variable_name(&self) -> &str {
328        self.variable_name
329    }
330}