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}