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