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;
74use crate::list_placement::{selected_segment_allows, OwnerRestriction, SelectedOwnerRestrictions};
75
76use super::entity::EntitySelector;
77use super::list_support::{collect_selected_entities, ordered_index};
78use super::move_selector::{
79    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor, MoveSelector, MoveStreamContext,
80};
81use super::precedence_route::{PrecedenceRouteGraph, PrecedenceRouteHooks};
82use super::sublist_support::count_sublist_change_moves_for_len;
83
84/// A move selector that generates sublist change (Or-opt) moves.
85///
86/// For each source entity and each valid segment `[start, start+len)`, generates
87/// moves that insert the segment at every valid destination position in every
88/// entity (including the source entity for intra-route relocation).
89///
90/// # Type Parameters
91/// * `S` - The solution type
92/// * `V` - The list element type
93/// * `ES` - The entity selector type
94pub struct SublistChangeMoveSelector<S, V, ES> {
95    entity_selector: ES,
96    // Minimum segment size (inclusive). Usually 1.
97    min_sublist_size: usize,
98    // Maximum segment size (inclusive). Usually 3-5.
99    max_sublist_size: usize,
100    list_len: fn(&S, usize) -> usize,
101    list_get: fn(&S, usize, usize) -> Option<V>,
102    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
103    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
104    element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
105    precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
106    variable_name: &'static str,
107    descriptor_index: usize,
108    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
109}
110
111enum SublistChangeStage {
112    Intra,
113    Inter,
114}
115
116pub struct SublistChangeMoveCursor<S, V>
117where
118    S: PlanningSolution,
119    V: Clone + PartialEq + Send + Sync + Debug + 'static,
120{
121    store: CandidateStore<S, SublistChangeMove<S, V>>,
122    entities: Vec<usize>,
123    route_lens: Vec<usize>,
124    context: MoveStreamContext,
125    src_idx: usize,
126    seg_start_offset: usize,
127    seg_size_offset: usize,
128    stage: SublistChangeStage,
129    intra_dst_offset: usize,
130    dst_idx: usize,
131    inter_dst_offset: usize,
132    min_seg: usize,
133    max_seg: usize,
134    list_len: fn(&S, usize) -> usize,
135    list_get: fn(&S, usize, usize) -> Option<V>,
136    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
137    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
138    element_owners: Option<Vec<Vec<OwnerRestriction>>>,
139    fixed_to_current_entity: bool,
140    precedence_route_graph: Option<PrecedenceRouteGraph>,
141    variable_name: &'static str,
142    descriptor_index: usize,
143}
144
145impl<S, V> SublistChangeMoveCursor<S, V>
146where
147    S: PlanningSolution,
148    V: Clone + PartialEq + Send + Sync + Debug + 'static,
149{
150    #[allow(clippy::too_many_arguments)]
151    fn new(
152        entities: Vec<usize>,
153        route_lens: Vec<usize>,
154        context: MoveStreamContext,
155        min_seg: usize,
156        max_seg: usize,
157        list_len: fn(&S, usize) -> usize,
158        list_get: fn(&S, usize, usize) -> Option<V>,
159        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
160        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
161        element_owners: Option<Vec<Vec<OwnerRestriction>>>,
162        fixed_to_current_entity: bool,
163        precedence_route_graph: Option<PrecedenceRouteGraph>,
164        variable_name: &'static str,
165        descriptor_index: usize,
166    ) -> Self {
167        Self {
168            store: CandidateStore::new(),
169            entities,
170            route_lens,
171            context,
172            src_idx: 0,
173            seg_start_offset: 0,
174            seg_size_offset: 0,
175            stage: SublistChangeStage::Intra,
176            intra_dst_offset: 0,
177            dst_idx: 0,
178            inter_dst_offset: 0,
179            min_seg,
180            max_seg,
181            list_len,
182            list_get,
183            sublist_remove,
184            sublist_insert,
185            element_owners,
186            fixed_to_current_entity,
187            precedence_route_graph,
188            variable_name,
189            descriptor_index,
190        }
191    }
192
193    pub(crate) fn with_precedence_route_graph(
194        mut self,
195        precedence_route_graph: Option<PrecedenceRouteGraph>,
196    ) -> Self {
197        self.precedence_route_graph = precedence_route_graph;
198        self
199    }
200
201    fn segment_size_count(&self, src_len: usize, seg_start: usize) -> usize {
202        let max_valid = self.max_seg.min(src_len.saturating_sub(seg_start));
203        max_valid.saturating_sub(self.min_seg) + usize::from(max_valid >= self.min_seg)
204    }
205
206    fn current_segment(&self) -> Option<(usize, usize, usize, usize, usize)> {
207        let src_entity = *self.entities.get(self.src_idx)?;
208        let src_len = self.route_lens[self.src_idx];
209        if src_len < self.min_seg {
210            return Some((src_entity, src_len, 0, 0, 0));
211        }
212        let seg_start = ordered_index(
213            self.seg_start_offset,
214            src_len,
215            self.context,
216            0x5B15_7C4A_46E0_0002 ^ src_entity as u64 ^ self.descriptor_index as u64,
217        );
218        let size_count = self.segment_size_count(src_len, seg_start);
219        if size_count == 0 {
220            return Some((src_entity, src_len, seg_start, 0, 0));
221        }
222        let size_offset = ordered_index(
223            self.seg_size_offset,
224            size_count,
225            self.context,
226            0x5B15_7C4A_46E0_0003 ^ src_entity as u64 ^ seg_start as u64,
227        );
228        let seg_size = self.min_seg + size_offset;
229        Some((
230            src_entity,
231            src_len,
232            seg_start,
233            seg_start + seg_size,
234            seg_size,
235        ))
236    }
237
238    fn advance_segment(&mut self) {
239        let Some((_, src_len, seg_start, _, _)) = self.current_segment() else {
240            return;
241        };
242        let size_count = self.segment_size_count(src_len, seg_start);
243        self.seg_size_offset += 1;
244        if self.seg_size_offset >= size_count {
245            self.seg_size_offset = 0;
246            self.seg_start_offset += 1;
247        }
248        while self.src_idx < self.route_lens.len()
249            && self.seg_start_offset >= self.route_lens[self.src_idx]
250        {
251            self.src_idx += 1;
252            self.seg_start_offset = 0;
253            self.seg_size_offset = 0;
254        }
255        self.stage = SublistChangeStage::Intra;
256        self.intra_dst_offset = 0;
257        self.dst_idx = 0;
258        self.inter_dst_offset = 0;
259    }
260
261    fn push_move(
262        &mut self,
263        src_entity: usize,
264        seg_start: usize,
265        seg_end: usize,
266        dst_entity: usize,
267        dst_pos: usize,
268    ) -> CandidateId {
269        self.store.push(SublistChangeMove::new(
270            src_entity,
271            seg_start,
272            seg_end,
273            dst_entity,
274            dst_pos,
275            self.list_len,
276            self.list_get,
277            self.sublist_remove,
278            self.sublist_insert,
279            self.variable_name,
280            self.descriptor_index,
281        ))
282    }
283
284    fn segment_owner_allows(
285        &self,
286        src_idx: usize,
287        seg_start: usize,
288        seg_end: usize,
289        dst_entity: usize,
290    ) -> bool {
291        self.element_owners.as_ref().is_none_or(|owners| {
292            selected_segment_allows(owners, src_idx, seg_start, seg_end, dst_entity)
293        })
294    }
295}
296
297impl<S, V> MoveCursor<S, SublistChangeMove<S, V>> for SublistChangeMoveCursor<S, V>
298where
299    S: PlanningSolution,
300    V: Clone + PartialEq + Send + Sync + Debug + 'static,
301{
302    fn next_candidate(&mut self) -> Option<CandidateId> {
303        loop {
304            let (src_entity, src_len, seg_start, seg_end, seg_size) = self.current_segment()?;
305            if src_len < self.min_seg || seg_size == 0 {
306                self.advance_segment();
307                continue;
308            }
309
310            match self.stage {
311                SublistChangeStage::Intra => {
312                    let post_removal_len = src_len - seg_size;
313                    while self.intra_dst_offset <= post_removal_len {
314                        let dst_pos = ordered_index(
315                            self.intra_dst_offset,
316                            post_removal_len + 1,
317                            self.context,
318                            0x5B15_7C4A_46E0_0004 ^ src_entity as u64 ^ seg_start as u64,
319                        );
320                        self.intra_dst_offset += 1;
321                        if dst_pos == seg_start {
322                            continue;
323                        }
324                        if self.element_owners.is_some()
325                            && !self.segment_owner_allows(
326                                self.src_idx,
327                                seg_start,
328                                seg_end,
329                                src_entity,
330                            )
331                        {
332                            continue;
333                        }
334                        if self.precedence_route_graph.as_ref().is_some_and(|graph| {
335                            graph.intra_sublist_change_introduces_cycle(
336                                src_entity, seg_start, seg_end, dst_pos,
337                            )
338                        }) {
339                            continue;
340                        }
341                        return Some(
342                            self.push_move(src_entity, seg_start, seg_end, src_entity, dst_pos),
343                        );
344                    }
345                    if self.fixed_to_current_entity {
346                        self.advance_segment();
347                        continue;
348                    }
349                    self.stage = SublistChangeStage::Inter;
350                    self.dst_idx = 0;
351                    self.inter_dst_offset = 0;
352                }
353                SublistChangeStage::Inter => {
354                    while self.dst_idx < self.entities.len() {
355                        if self.dst_idx == self.src_idx {
356                            self.dst_idx += 1;
357                            continue;
358                        }
359                        let dst_entity = self.entities[self.dst_idx];
360                        let dst_len = self.route_lens[self.dst_idx];
361                        if self.inter_dst_offset <= dst_len {
362                            let dst_pos = ordered_index(
363                                self.inter_dst_offset,
364                                dst_len + 1,
365                                self.context,
366                                0x5B15_7C4A_46E0_0005
367                                    ^ src_entity as u64
368                                    ^ dst_entity as u64
369                                    ^ seg_start as u64,
370                            );
371                            self.inter_dst_offset += 1;
372                            if self.element_owners.is_some()
373                                && !self.segment_owner_allows(
374                                    self.src_idx,
375                                    seg_start,
376                                    seg_end,
377                                    dst_entity,
378                                )
379                            {
380                                continue;
381                            }
382                            return Some(
383                                self.push_move(src_entity, seg_start, seg_end, dst_entity, dst_pos),
384                            );
385                        }
386                        self.dst_idx += 1;
387                        self.inter_dst_offset = 0;
388                    }
389                    self.advance_segment();
390                }
391            }
392        }
393    }
394
395    fn candidate(
396        &self,
397        id: CandidateId,
398    ) -> Option<MoveCandidateRef<'_, S, SublistChangeMove<S, V>>> {
399        self.store.candidate(id)
400    }
401
402    fn take_candidate(&mut self, id: CandidateId) -> SublistChangeMove<S, V> {
403        self.store.take_candidate(id)
404    }
405}
406
407impl<S, V> Iterator for SublistChangeMoveCursor<S, V>
408where
409    S: PlanningSolution,
410    V: Clone + PartialEq + Send + Sync + Debug + 'static,
411{
412    type Item = SublistChangeMove<S, V>;
413
414    fn next(&mut self) -> Option<Self::Item> {
415        let id = self.next_candidate()?;
416        Some(self.take_candidate(id))
417    }
418}
419
420impl<S, V: Debug, ES: Debug> Debug for SublistChangeMoveSelector<S, V, ES> {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        f.debug_struct("SublistChangeMoveSelector")
423            .field("entity_selector", &self.entity_selector)
424            .field("min_sublist_size", &self.min_sublist_size)
425            .field("max_sublist_size", &self.max_sublist_size)
426            .field("variable_name", &self.variable_name)
427            .field("descriptor_index", &self.descriptor_index)
428            .finish()
429    }
430}
431
432impl<S, V, ES> SublistChangeMoveSelector<S, V, ES> {
433    /* Creates a new sublist change move selector.
434
435    # Arguments
436    * `entity_selector` - Selects entities to generate moves for
437    * `min_sublist_size` - Minimum segment length (must be ≥ 1)
438    * `max_sublist_size` - Maximum segment length
439    * `list_len` - Function to get list length
440    * `sublist_remove` - Function to drain a range `[start, end)`, returning removed elements
441    * `sublist_insert` - Function to insert a slice at a position
442    * `variable_name` - Name of the list variable
443    * `descriptor_index` - Entity descriptor index
444
445    # Panics
446    Panics if `min_sublist_size == 0` or `max_sublist_size < min_sublist_size`.
447    */
448    #[allow(clippy::too_many_arguments)]
449    pub fn new(
450        entity_selector: ES,
451        min_sublist_size: usize,
452        max_sublist_size: usize,
453        list_len: fn(&S, usize) -> usize,
454        list_get: fn(&S, usize, usize) -> Option<V>,
455        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
456        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
457        variable_name: &'static str,
458        descriptor_index: usize,
459    ) -> Self {
460        assert!(min_sublist_size >= 1, "min_sublist_size must be at least 1");
461        assert!(
462            max_sublist_size >= min_sublist_size,
463            "max_sublist_size must be >= min_sublist_size"
464        );
465        Self {
466            entity_selector,
467            min_sublist_size,
468            max_sublist_size,
469            list_len,
470            list_get,
471            sublist_remove,
472            sublist_insert,
473            element_owner_fn: None,
474            precedence_route_hooks: None,
475            variable_name,
476            descriptor_index,
477            _phantom: PhantomData,
478        }
479    }
480
481    pub fn with_element_owner_fn(
482        mut self,
483        element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
484    ) -> Self {
485        self.element_owner_fn = element_owner_fn;
486        self
487    }
488
489    pub(crate) fn with_precedence_route_hooks(
490        mut self,
491        precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
492    ) -> Self {
493        self.precedence_route_hooks = precedence_route_hooks;
494        self
495    }
496
497    pub(crate) fn precedence_route_hooks(&self) -> Option<PrecedenceRouteHooks<S, V>> {
498        self.precedence_route_hooks
499    }
500}
501
502impl<S, V, ES> MoveSelector<S, SublistChangeMove<S, V>> for SublistChangeMoveSelector<S, V, ES>
503where
504    S: PlanningSolution,
505    V: Clone + PartialEq + Send + Sync + Debug + 'static,
506    ES: EntitySelector<S>,
507{
508    type Cursor<'a>
509        = SublistChangeMoveCursor<S, V>
510    where
511        Self: 'a;
512
513    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
514        self.open_cursor_with_context(score_director, MoveStreamContext::default())
515    }
516
517    fn open_cursor_with_context<'a, D: Director<S>>(
518        &'a self,
519        score_director: &D,
520        context: MoveStreamContext,
521    ) -> Self::Cursor<'a> {
522        let mut selected =
523            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
524        selected.apply_stream_order(
525            context,
526            0x5B15_7C4A_46E0_0001 ^ self.descriptor_index as u64,
527        );
528        let owner_restrictions = crate::list_placement::selected_owner_restrictions(
529            self.element_owner_fn,
530            score_director.working_solution(),
531            score_director
532                .entity_count(self.descriptor_index)
533                .unwrap_or(0),
534            &selected.entities,
535            &selected.route_lens,
536            self.list_get,
537        );
538        let fixed_to_current_entity = owner_restrictions
539            .as_ref()
540            .is_some_and(SelectedOwnerRestrictions::is_fixed_to_current);
541        let element_owners = owner_restrictions.and_then(SelectedOwnerRestrictions::into_mixed);
542        SublistChangeMoveCursor::new(
543            selected.entities,
544            selected.route_lens,
545            context,
546            self.min_sublist_size,
547            self.max_sublist_size,
548            self.list_len,
549            self.list_get,
550            self.sublist_remove,
551            self.sublist_insert,
552            element_owners,
553            fixed_to_current_entity,
554            None,
555            self.variable_name,
556            self.descriptor_index,
557        )
558    }
559
560    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
561        let selected =
562            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
563        let Some(owner_restrictions) = crate::list_placement::selected_owner_restrictions(
564            self.element_owner_fn,
565            score_director.working_solution(),
566            score_director
567                .entity_count(self.descriptor_index)
568                .unwrap_or(0),
569            &selected.entities,
570            &selected.route_lens,
571            self.list_get,
572        ) else {
573            return unfiltered_sublist_change_size(
574                &selected.route_lens,
575                self.min_sublist_size,
576                self.max_sublist_size,
577            );
578        };
579
580        if owner_restrictions.is_fixed_to_current() {
581            return selected
582                .route_lens
583                .iter()
584                .map(|&route_len| {
585                    count_sublist_change_moves_for_len(
586                        route_len,
587                        0,
588                        self.min_sublist_size,
589                        self.max_sublist_size,
590                    )
591                })
592                .sum();
593        }
594        let element_owners = owner_restrictions
595            .mixed()
596            .expect("non-fixed owner restrictions must retain mixed owner matrix");
597
598        let mut count = 0;
599        for (src_idx, (&src_entity, &src_len)) in selected
600            .entities
601            .iter()
602            .zip(selected.route_lens.iter())
603            .enumerate()
604        {
605            for seg_start in 0..src_len {
606                let max_seg = self.max_sublist_size.min(src_len - seg_start);
607                for seg_size in self.min_sublist_size..=max_seg {
608                    let seg_end = seg_start + seg_size;
609                    if selected_segment_allows(
610                        element_owners,
611                        src_idx,
612                        seg_start,
613                        seg_end,
614                        src_entity,
615                    ) {
616                        count += src_len - seg_size;
617                    }
618                    for (dst_idx, (&dst_entity, &dst_len)) in selected
619                        .entities
620                        .iter()
621                        .zip(selected.route_lens.iter())
622                        .enumerate()
623                    {
624                        if dst_idx == src_idx {
625                            continue;
626                        }
627                        if selected_segment_allows(
628                            element_owners,
629                            src_idx,
630                            seg_start,
631                            seg_end,
632                            dst_entity,
633                        ) {
634                            count += dst_len + 1;
635                        }
636                    }
637                }
638            }
639        }
640        count
641    }
642}
643
644fn unfiltered_sublist_change_size(
645    route_lens: &[usize],
646    min_sublist_size: usize,
647    max_sublist_size: usize,
648) -> usize {
649    let total_elements = route_lens.iter().sum::<usize>();
650    let entity_count = route_lens.len();
651
652    route_lens
653        .iter()
654        .map(|&route_len| {
655            let inter_destinations =
656                total_elements.saturating_sub(route_len) + entity_count.saturating_sub(1);
657            count_sublist_change_moves_for_len(
658                route_len,
659                inter_destinations,
660                min_sublist_size,
661                max_sublist_size,
662            )
663        })
664        .sum()
665}