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