Skip to main content

solverforge_solver/heuristic/selector/
sublist_change.rs

1/* Sublist change move selector for segment relocation (Or-opt).
2
3Generates `SublistChangeMove`s that relocate contiguous segments within or
4between list variables. The Or-opt family of moves (segments of size 1, 2, 3, …)
5is among the most effective VRP improvements after basic 2-opt.
6
7# Complexity
8
9For n entities with average route length m and max segment size k:
10- Intra-entity: O(n * m * k) sources × O(m) destinations
11- Inter-entity: O(n * m * k) sources × O(n * m) destinations
12- Total: O(n² * m² * k)
13
14Use a forager that quits early (`FirstAccepted`, `AcceptedCount`) to keep
15iteration practical for large instances.
16
17# Example
18
19```
20use solverforge_solver::heuristic::selector::sublist_change::SublistChangeMoveSelector;
21use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
22use solverforge_solver::heuristic::selector::MoveSelector;
23use solverforge_core::domain::PlanningSolution;
24use solverforge_core::score::SoftScore;
25
26#[derive(Clone, Debug)]
27struct Vehicle { visits: Vec<i32> }
28
29#[derive(Clone, Debug)]
30struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
31
32impl PlanningSolution for Solution {
33type Score = SoftScore;
34fn score(&self) -> Option<Self::Score> { self.score }
35fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
36}
37
38fn list_len(s: &Solution, entity_idx: usize) -> usize {
39s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
40}
41fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
42s.vehicles.get_mut(entity_idx)
43.map(|v| v.visits.drain(start..end).collect())
44.unwrap_or_default()
45}
46fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
47if let Some(v) = s.vehicles.get_mut(entity_idx) {
48for (i, item) in items.into_iter().enumerate() {
49v.visits.insert(pos + i, item);
50}
51}
52}
53
54// Or-opt: relocate segments of size 1..=3
55let selector = SublistChangeMoveSelector::<Solution, i32, _>::new(
56FromSolutionEntitySelector::new(0),
571, 3,
58list_len,
59sublist_remove,
60sublist_insert,
61"visits",
620,
63);
64```
65*/
66
67use std::fmt::Debug;
68use std::marker::PhantomData;
69
70use solverforge_core::domain::PlanningSolution;
71use solverforge_scoring::Director;
72
73use crate::heuristic::r#move::SublistChangeMove;
74
75use super::entity::EntitySelector;
76use super::list_support::{collect_selected_entities, ordered_index};
77use super::move_selector::{
78    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor, MoveSelector, MoveStreamContext,
79};
80use super::sublist_support::count_sublist_change_moves_for_len;
81
82/// A move selector that generates sublist change (Or-opt) moves.
83///
84/// For each source entity and each valid segment `[start, start+len)`, generates
85/// moves that insert the segment at every valid destination position in every
86/// entity (including the source entity for intra-route relocation).
87///
88/// # Type Parameters
89/// * `S` - The solution type
90/// * `V` - The list element type
91/// * `ES` - The entity selector type
92pub struct SublistChangeMoveSelector<S, V, ES> {
93    entity_selector: ES,
94    // Minimum segment size (inclusive). Usually 1.
95    min_sublist_size: usize,
96    // Maximum segment size (inclusive). Usually 3-5.
97    max_sublist_size: usize,
98    list_len: fn(&S, usize) -> usize,
99    list_get: fn(&S, usize, usize) -> Option<V>,
100    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
101    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
102    variable_name: &'static str,
103    descriptor_index: usize,
104    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
105}
106
107enum SublistChangeStage {
108    Intra,
109    Inter,
110}
111
112pub struct SublistChangeMoveCursor<S, V>
113where
114    S: PlanningSolution,
115    V: Clone + PartialEq + Send + Sync + Debug + 'static,
116{
117    store: CandidateStore<S, SublistChangeMove<S, V>>,
118    entities: Vec<usize>,
119    route_lens: Vec<usize>,
120    context: MoveStreamContext,
121    src_idx: usize,
122    seg_start_offset: usize,
123    seg_size_offset: usize,
124    stage: SublistChangeStage,
125    intra_dst_offset: usize,
126    dst_idx: usize,
127    inter_dst_offset: usize,
128    min_seg: usize,
129    max_seg: usize,
130    list_len: fn(&S, usize) -> usize,
131    list_get: fn(&S, usize, usize) -> Option<V>,
132    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
133    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
134    variable_name: &'static str,
135    descriptor_index: usize,
136}
137
138impl<S, V> SublistChangeMoveCursor<S, V>
139where
140    S: PlanningSolution,
141    V: Clone + PartialEq + Send + Sync + Debug + 'static,
142{
143    #[allow(clippy::too_many_arguments)]
144    fn new(
145        entities: Vec<usize>,
146        route_lens: Vec<usize>,
147        context: MoveStreamContext,
148        min_seg: usize,
149        max_seg: usize,
150        list_len: fn(&S, usize) -> usize,
151        list_get: fn(&S, usize, usize) -> Option<V>,
152        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
153        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
154        variable_name: &'static str,
155        descriptor_index: usize,
156    ) -> Self {
157        Self {
158            store: CandidateStore::new(),
159            entities,
160            route_lens,
161            context,
162            src_idx: 0,
163            seg_start_offset: 0,
164            seg_size_offset: 0,
165            stage: SublistChangeStage::Intra,
166            intra_dst_offset: 0,
167            dst_idx: 0,
168            inter_dst_offset: 0,
169            min_seg,
170            max_seg,
171            list_len,
172            list_get,
173            sublist_remove,
174            sublist_insert,
175            variable_name,
176            descriptor_index,
177        }
178    }
179
180    fn segment_size_count(&self, src_len: usize, seg_start: usize) -> usize {
181        let max_valid = self.max_seg.min(src_len.saturating_sub(seg_start));
182        max_valid.saturating_sub(self.min_seg) + usize::from(max_valid >= self.min_seg)
183    }
184
185    fn current_segment(&self) -> Option<(usize, usize, usize, usize, usize)> {
186        let src_entity = *self.entities.get(self.src_idx)?;
187        let src_len = self.route_lens[self.src_idx];
188        if src_len < self.min_seg {
189            return Some((src_entity, src_len, 0, 0, 0));
190        }
191        let seg_start = ordered_index(
192            self.seg_start_offset,
193            src_len,
194            self.context,
195            0x5B15_7C4A_46E0_0002 ^ src_entity as u64 ^ self.descriptor_index as u64,
196        );
197        let size_count = self.segment_size_count(src_len, seg_start);
198        if size_count == 0 {
199            return Some((src_entity, src_len, seg_start, 0, 0));
200        }
201        let size_offset = ordered_index(
202            self.seg_size_offset,
203            size_count,
204            self.context,
205            0x5B15_7C4A_46E0_0003 ^ src_entity as u64 ^ seg_start as u64,
206        );
207        let seg_size = self.min_seg + size_offset;
208        Some((
209            src_entity,
210            src_len,
211            seg_start,
212            seg_start + seg_size,
213            seg_size,
214        ))
215    }
216
217    fn advance_segment(&mut self) {
218        let Some((_, src_len, seg_start, _, _)) = self.current_segment() else {
219            return;
220        };
221        let size_count = self.segment_size_count(src_len, seg_start);
222        self.seg_size_offset += 1;
223        if self.seg_size_offset >= size_count {
224            self.seg_size_offset = 0;
225            self.seg_start_offset += 1;
226        }
227        while self.src_idx < self.route_lens.len()
228            && self.seg_start_offset >= self.route_lens[self.src_idx]
229        {
230            self.src_idx += 1;
231            self.seg_start_offset = 0;
232            self.seg_size_offset = 0;
233        }
234        self.stage = SublistChangeStage::Intra;
235        self.intra_dst_offset = 0;
236        self.dst_idx = 0;
237        self.inter_dst_offset = 0;
238    }
239
240    fn push_move(
241        &mut self,
242        src_entity: usize,
243        seg_start: usize,
244        seg_end: usize,
245        dst_entity: usize,
246        dst_pos: usize,
247    ) -> CandidateId {
248        self.store.push(SublistChangeMove::new(
249            src_entity,
250            seg_start,
251            seg_end,
252            dst_entity,
253            dst_pos,
254            self.list_len,
255            self.list_get,
256            self.sublist_remove,
257            self.sublist_insert,
258            self.variable_name,
259            self.descriptor_index,
260        ))
261    }
262}
263
264impl<S, V> MoveCursor<S, SublistChangeMove<S, V>> for SublistChangeMoveCursor<S, V>
265where
266    S: PlanningSolution,
267    V: Clone + PartialEq + Send + Sync + Debug + 'static,
268{
269    fn next_candidate(&mut self) -> Option<CandidateId> {
270        loop {
271            let (src_entity, src_len, seg_start, seg_end, seg_size) = self.current_segment()?;
272            if src_len < self.min_seg || seg_size == 0 {
273                self.advance_segment();
274                continue;
275            }
276
277            match self.stage {
278                SublistChangeStage::Intra => {
279                    let post_removal_len = src_len - seg_size;
280                    while self.intra_dst_offset <= post_removal_len {
281                        let dst_pos = ordered_index(
282                            self.intra_dst_offset,
283                            post_removal_len + 1,
284                            self.context,
285                            0x5B15_7C4A_46E0_0004 ^ src_entity as u64 ^ seg_start as u64,
286                        );
287                        self.intra_dst_offset += 1;
288                        if dst_pos == seg_start {
289                            continue;
290                        }
291                        return Some(
292                            self.push_move(src_entity, seg_start, seg_end, src_entity, dst_pos),
293                        );
294                    }
295                    self.stage = SublistChangeStage::Inter;
296                    self.dst_idx = 0;
297                    self.inter_dst_offset = 0;
298                }
299                SublistChangeStage::Inter => {
300                    while self.dst_idx < self.entities.len() {
301                        if self.dst_idx == self.src_idx {
302                            self.dst_idx += 1;
303                            continue;
304                        }
305                        let dst_entity = self.entities[self.dst_idx];
306                        let dst_len = self.route_lens[self.dst_idx];
307                        if self.inter_dst_offset <= dst_len {
308                            let dst_pos = ordered_index(
309                                self.inter_dst_offset,
310                                dst_len + 1,
311                                self.context,
312                                0x5B15_7C4A_46E0_0005
313                                    ^ src_entity as u64
314                                    ^ dst_entity as u64
315                                    ^ seg_start as u64,
316                            );
317                            self.inter_dst_offset += 1;
318                            return Some(
319                                self.push_move(src_entity, seg_start, seg_end, dst_entity, dst_pos),
320                            );
321                        }
322                        self.dst_idx += 1;
323                        self.inter_dst_offset = 0;
324                    }
325                    self.advance_segment();
326                }
327            }
328        }
329    }
330
331    fn candidate(
332        &self,
333        id: CandidateId,
334    ) -> Option<MoveCandidateRef<'_, S, SublistChangeMove<S, V>>> {
335        self.store.candidate(id)
336    }
337
338    fn take_candidate(&mut self, id: CandidateId) -> SublistChangeMove<S, V> {
339        self.store.take_candidate(id)
340    }
341}
342
343impl<S, V> Iterator for SublistChangeMoveCursor<S, V>
344where
345    S: PlanningSolution,
346    V: Clone + PartialEq + Send + Sync + Debug + 'static,
347{
348    type Item = SublistChangeMove<S, V>;
349
350    fn next(&mut self) -> Option<Self::Item> {
351        let id = self.next_candidate()?;
352        Some(self.take_candidate(id))
353    }
354}
355
356impl<S, V: Debug, ES: Debug> Debug for SublistChangeMoveSelector<S, V, ES> {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        f.debug_struct("SublistChangeMoveSelector")
359            .field("entity_selector", &self.entity_selector)
360            .field("min_sublist_size", &self.min_sublist_size)
361            .field("max_sublist_size", &self.max_sublist_size)
362            .field("variable_name", &self.variable_name)
363            .field("descriptor_index", &self.descriptor_index)
364            .finish()
365    }
366}
367
368impl<S, V, ES> SublistChangeMoveSelector<S, V, ES> {
369    /* Creates a new sublist change move selector.
370
371    # Arguments
372    * `entity_selector` - Selects entities to generate moves for
373    * `min_sublist_size` - Minimum segment length (must be ≥ 1)
374    * `max_sublist_size` - Maximum segment length
375    * `list_len` - Function to get list length
376    * `sublist_remove` - Function to drain a range `[start, end)`, returning removed elements
377    * `sublist_insert` - Function to insert a slice at a position
378    * `variable_name` - Name of the list variable
379    * `descriptor_index` - Entity descriptor index
380
381    # Panics
382    Panics if `min_sublist_size == 0` or `max_sublist_size < min_sublist_size`.
383    */
384    #[allow(clippy::too_many_arguments)]
385    pub fn new(
386        entity_selector: ES,
387        min_sublist_size: usize,
388        max_sublist_size: usize,
389        list_len: fn(&S, usize) -> usize,
390        list_get: fn(&S, usize, usize) -> Option<V>,
391        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
392        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
393        variable_name: &'static str,
394        descriptor_index: usize,
395    ) -> Self {
396        assert!(min_sublist_size >= 1, "min_sublist_size must be at least 1");
397        assert!(
398            max_sublist_size >= min_sublist_size,
399            "max_sublist_size must be >= min_sublist_size"
400        );
401        Self {
402            entity_selector,
403            min_sublist_size,
404            max_sublist_size,
405            list_len,
406            list_get,
407            sublist_remove,
408            sublist_insert,
409            variable_name,
410            descriptor_index,
411            _phantom: PhantomData,
412        }
413    }
414}
415
416impl<S, V, ES> MoveSelector<S, SublistChangeMove<S, V>> for SublistChangeMoveSelector<S, V, ES>
417where
418    S: PlanningSolution,
419    V: Clone + PartialEq + Send + Sync + Debug + 'static,
420    ES: EntitySelector<S>,
421{
422    type Cursor<'a>
423        = SublistChangeMoveCursor<S, V>
424    where
425        Self: 'a;
426
427    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
428        self.open_cursor_with_context(score_director, MoveStreamContext::default())
429    }
430
431    fn open_cursor_with_context<'a, D: Director<S>>(
432        &'a self,
433        score_director: &D,
434        context: MoveStreamContext,
435    ) -> Self::Cursor<'a> {
436        let mut selected =
437            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
438        selected.apply_stream_order(
439            context,
440            0x5B15_7C4A_46E0_0001 ^ self.descriptor_index as u64,
441        );
442        SublistChangeMoveCursor::new(
443            selected.entities,
444            selected.route_lens,
445            context,
446            self.min_sublist_size,
447            self.max_sublist_size,
448            self.list_len,
449            self.list_get,
450            self.sublist_remove,
451            self.sublist_insert,
452            self.variable_name,
453            self.descriptor_index,
454        )
455    }
456
457    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
458        let selected =
459            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
460        let total_elements = selected.route_lens.iter().sum::<usize>();
461        let entity_count = selected.entities.len();
462
463        selected
464            .route_lens
465            .iter()
466            .map(|&route_len| {
467                let inter_destinations =
468                    total_elements.saturating_sub(route_len) + entity_count.saturating_sub(1);
469                count_sublist_change_moves_for_len(
470                    route_len,
471                    inter_destinations,
472                    self.min_sublist_size,
473                    self.max_sublist_size,
474                )
475            })
476            .sum()
477    }
478}