solverforge_solver/heuristic/selector/sublist_swap.rs
1//! Sublist swap move selector for segment exchange.
2//!
3//! Generates `SubListSwapMove`s that swap contiguous segments within or between
4//! list variables. Useful for balanced inter-route segment exchanges in VRP.
5//!
6//! # Complexity
7//!
8//! For n entities with average route length m and max segment size k:
9//! - Intra-entity pairs: O(n * m² * k²) — triangular over non-overlapping segments
10//! - Inter-entity pairs: O(n² * m² * k²) — all pairs across entities
11//!
12//! Use a forager that quits early for large instances.
13//!
14//! # Example
15//!
16//! ```
17//! use solverforge_solver::heuristic::selector::sublist_swap::SubListSwapMoveSelector;
18//! use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
19//! use solverforge_solver::heuristic::selector::MoveSelector;
20//! use solverforge_core::domain::PlanningSolution;
21//! use solverforge_core::score::SimpleScore;
22//!
23//! #[derive(Clone, Debug)]
24//! struct Vehicle { visits: Vec<i32> }
25//!
26//! #[derive(Clone, Debug)]
27//! struct Solution { vehicles: Vec<Vehicle>, score: Option<SimpleScore> }
28//!
29//! impl PlanningSolution for Solution {
30//! type Score = SimpleScore;
31//! fn score(&self) -> Option<Self::Score> { self.score }
32//! fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
33//! }
34//!
35//! fn list_len(s: &Solution, entity_idx: usize) -> usize {
36//! s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
37//! }
38//! fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
39//! s.vehicles.get_mut(entity_idx)
40//! .map(|v| v.visits.drain(start..end).collect())
41//! .unwrap_or_default()
42//! }
43//! fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
44//! if let Some(v) = s.vehicles.get_mut(entity_idx) {
45//! for (i, item) in items.into_iter().enumerate() {
46//! v.visits.insert(pos + i, item);
47//! }
48//! }
49//! }
50//!
51//! // Swap segments of size 1..=3 between routes
52//! let selector = SubListSwapMoveSelector::<Solution, i32, _>::new(
53//! FromSolutionEntitySelector::new(0),
54//! 1, 3,
55//! list_len,
56//! sublist_remove,
57//! sublist_insert,
58//! "visits",
59//! 0,
60//! );
61//! ```
62
63use std::fmt::Debug;
64use std::marker::PhantomData;
65
66use solverforge_core::domain::PlanningSolution;
67use solverforge_scoring::ScoreDirector;
68
69use crate::heuristic::r#move::{ListMoveImpl, SubListSwapMove};
70
71use super::entity::EntitySelector;
72use super::typed_move_selector::MoveSelector;
73
74/// A move selector that generates sublist swap moves.
75///
76/// For each pair of segments (which may span different entities), generates
77/// a swap move. Intra-entity swaps require non-overlapping segments.
78///
79/// # Type Parameters
80/// * `S` - The solution type
81/// * `V` - The list element type
82/// * `ES` - The entity selector type
83pub struct SubListSwapMoveSelector<S, V, ES> {
84 entity_selector: ES,
85 /// Minimum segment size (inclusive).
86 min_sublist_size: usize,
87 /// Maximum segment size (inclusive).
88 max_sublist_size: usize,
89 list_len: fn(&S, usize) -> usize,
90 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
91 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
92 variable_name: &'static str,
93 descriptor_index: usize,
94 _phantom: PhantomData<(fn() -> S, fn() -> V)>,
95}
96
97impl<S, V: Debug, ES: Debug> Debug for SubListSwapMoveSelector<S, V, ES> {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("SubListSwapMoveSelector")
100 .field("entity_selector", &self.entity_selector)
101 .field("min_sublist_size", &self.min_sublist_size)
102 .field("max_sublist_size", &self.max_sublist_size)
103 .field("variable_name", &self.variable_name)
104 .field("descriptor_index", &self.descriptor_index)
105 .finish()
106 }
107}
108
109impl<S, V, ES> SubListSwapMoveSelector<S, V, ES> {
110 /// Creates a new sublist swap move selector.
111 ///
112 /// # Arguments
113 /// * `entity_selector` - Selects entities to consider for swaps
114 /// * `min_sublist_size` - Minimum segment length (must be ≥ 1)
115 /// * `max_sublist_size` - Maximum segment length
116 /// * `list_len` - Function to get list length for an entity
117 /// * `sublist_remove` - Function to drain range `[start, end)`, returning elements
118 /// * `sublist_insert` - Function to insert elements at a position
119 /// * `variable_name` - Name of the list variable
120 /// * `descriptor_index` - Entity descriptor index
121 ///
122 /// # Panics
123 /// Panics if `min_sublist_size == 0` or `max_sublist_size < min_sublist_size`.
124 #[allow(clippy::too_many_arguments)]
125 pub fn new(
126 entity_selector: ES,
127 min_sublist_size: usize,
128 max_sublist_size: usize,
129 list_len: fn(&S, usize) -> usize,
130 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
131 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
132 variable_name: &'static str,
133 descriptor_index: usize,
134 ) -> Self {
135 assert!(min_sublist_size >= 1, "min_sublist_size must be at least 1");
136 assert!(
137 max_sublist_size >= min_sublist_size,
138 "max_sublist_size must be >= min_sublist_size"
139 );
140 Self {
141 entity_selector,
142 min_sublist_size,
143 max_sublist_size,
144 list_len,
145 sublist_remove,
146 sublist_insert,
147 variable_name,
148 descriptor_index,
149 _phantom: PhantomData,
150 }
151 }
152}
153
154impl<S, V, ES> MoveSelector<S, SubListSwapMove<S, V>> for SubListSwapMoveSelector<S, V, ES>
155where
156 S: PlanningSolution,
157 V: Clone + PartialEq + Send + Sync + Debug + 'static,
158 ES: EntitySelector<S>,
159{
160 fn iter_moves<'a, D: ScoreDirector<S>>(
161 &'a self,
162 score_director: &'a D,
163 ) -> impl Iterator<Item = SubListSwapMove<S, V>> + 'a {
164 let solution = score_director.working_solution();
165 let list_len = self.list_len;
166 let sublist_remove = self.sublist_remove;
167 let sublist_insert = self.sublist_insert;
168 let variable_name = self.variable_name;
169 let descriptor_index = self.descriptor_index;
170 let min_seg = self.min_sublist_size;
171 let max_seg = self.max_sublist_size;
172
173 let entities: Vec<usize> = self
174 .entity_selector
175 .iter(score_director)
176 .map(|r| r.entity_index)
177 .collect();
178
179 let route_lens: Vec<usize> = entities.iter().map(|&e| list_len(solution, e)).collect();
180
181 let mut moves = Vec::new();
182
183 for (i, &entity_a) in entities.iter().enumerate() {
184 let len_a = route_lens[i];
185
186 // Intra-entity: pairs of non-overlapping segments in entity_a
187 // Enumerate all valid first segments, then all non-overlapping second segments
188 for first_start in 0..len_a {
189 for first_size in min_seg..=max_seg {
190 let first_end = first_start + first_size;
191 if first_end > len_a {
192 break;
193 }
194
195 // Second segment must not overlap: second_start >= first_end
196 for second_start in first_end..len_a {
197 for second_size in min_seg..=max_seg {
198 let second_end = second_start + second_size;
199 if second_end > len_a {
200 break;
201 }
202 moves.push(SubListSwapMove::new(
203 entity_a,
204 first_start,
205 first_end,
206 entity_a,
207 second_start,
208 second_end,
209 list_len,
210 sublist_remove,
211 sublist_insert,
212 variable_name,
213 descriptor_index,
214 ));
215 }
216 }
217 }
218 }
219
220 // Inter-entity: all segment pairs between entity_a and entity_b (b > a)
221 for (j, &entity_b) in entities.iter().enumerate() {
222 if j <= i {
223 continue;
224 }
225 let len_b = route_lens[j];
226 if len_b == 0 {
227 continue;
228 }
229
230 for first_start in 0..len_a {
231 for first_size in min_seg..=max_seg {
232 let first_end = first_start + first_size;
233 if first_end > len_a {
234 break;
235 }
236
237 for second_start in 0..len_b {
238 for second_size in min_seg..=max_seg {
239 let second_end = second_start + second_size;
240 if second_end > len_b {
241 break;
242 }
243 moves.push(SubListSwapMove::new(
244 entity_a,
245 first_start,
246 first_end,
247 entity_b,
248 second_start,
249 second_end,
250 list_len,
251 sublist_remove,
252 sublist_insert,
253 variable_name,
254 descriptor_index,
255 ));
256 }
257 }
258 }
259 }
260 }
261 }
262
263 moves.into_iter()
264 }
265
266 fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
267 let solution = score_director.working_solution();
268 let list_len = self.list_len;
269
270 let entities: Vec<usize> = self
271 .entity_selector
272 .iter(score_director)
273 .map(|r| r.entity_index)
274 .collect();
275
276 let route_lens: Vec<usize> = entities.iter().map(|&e| list_len(solution, e)).collect();
277 let n = entities.len();
278 if n == 0 {
279 return 0;
280 }
281
282 let k_range = self.max_sublist_size - self.min_sublist_size + 1;
283 let total_elements: usize = route_lens.iter().sum();
284 let avg_len = total_elements / n;
285 // Rough estimate: n * avg_len * k * (avg_len * k + (n-1) * avg_len * k) / 2
286 n * avg_len * k_range * avg_len.max(1) * k_range * (n + 1) / 2
287 }
288}
289
290/// Wraps a `SubListSwapMoveSelector` to yield `ListMoveImpl::SubListSwap`.
291pub struct ListMoveSubListSwapSelector<S, V, ES> {
292 inner: SubListSwapMoveSelector<S, V, ES>,
293}
294
295impl<S, V: Debug, ES: Debug> Debug for ListMoveSubListSwapSelector<S, V, ES> {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 f.debug_struct("ListMoveSubListSwapSelector")
298 .field("inner", &self.inner)
299 .finish()
300 }
301}
302
303impl<S, V, ES> ListMoveSubListSwapSelector<S, V, ES> {
304 /// Wraps an existing [`SubListSwapMoveSelector`].
305 pub fn new(inner: SubListSwapMoveSelector<S, V, ES>) -> Self {
306 Self { inner }
307 }
308}
309
310impl<S, V, ES> MoveSelector<S, ListMoveImpl<S, V>> for ListMoveSubListSwapSelector<S, V, ES>
311where
312 S: PlanningSolution,
313 V: Clone + PartialEq + Send + Sync + Debug + 'static,
314 ES: EntitySelector<S>,
315{
316 fn iter_moves<'a, D: ScoreDirector<S>>(
317 &'a self,
318 score_director: &'a D,
319 ) -> impl Iterator<Item = ListMoveImpl<S, V>> + 'a {
320 self.inner
321 .iter_moves(score_director)
322 .map(ListMoveImpl::SubListSwap)
323 }
324
325 fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
326 self.inner.size(score_director)
327 }
328}