Skip to main content

solverforge_solver/heuristic/selector/
sublist_swap.rs

1/* Sublist swap move selector for segment exchange.
2
3Generates `SublistSwapMove`s that swap contiguous segments within or between
4list variables. Useful for balanced inter-route segment exchanges in VRP.
5
6# Complexity
7
8For 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
12Use a forager that quits early for large instances.
13
14# Example
15
16```
17use solverforge_solver::heuristic::selector::sublist_swap::SublistSwapMoveSelector;
18use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
19use solverforge_solver::heuristic::selector::MoveSelector;
20use solverforge_core::domain::PlanningSolution;
21use solverforge_core::score::SoftScore;
22
23#[derive(Clone, Debug)]
24struct Vehicle { visits: Vec<i32> }
25
26#[derive(Clone, Debug)]
27struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
28
29impl PlanningSolution for Solution {
30type Score = SoftScore;
31fn score(&self) -> Option<Self::Score> { self.score }
32fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
33}
34
35fn list_len(s: &Solution, entity_idx: usize) -> usize {
36s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
37}
38fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
39s.vehicles.get_mut(entity_idx)
40.map(|v| v.visits.drain(start..end).collect())
41.unwrap_or_default()
42}
43fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
44if let Some(v) = s.vehicles.get_mut(entity_idx) {
45for (i, item) in items.into_iter().enumerate() {
46v.visits.insert(pos + i, item);
47}
48}
49}
50
51// Swap segments of size 1..=3 between routes
52let selector = SublistSwapMoveSelector::<Solution, i32, _>::new(
53FromSolutionEntitySelector::new(0),
541, 3,
55list_len,
56sublist_remove,
57sublist_insert,
58"visits",
590,
60);
61```
62*/
63
64use std::fmt::Debug;
65use std::marker::PhantomData;
66
67use solverforge_core::domain::PlanningSolution;
68use solverforge_scoring::Director;
69
70use crate::heuristic::r#move::SublistSwapMove;
71use crate::list_placement::{selected_segment_allows, OwnerRestriction, SelectedOwnerRestrictions};
72
73use super::entity::EntitySelector;
74use super::list_support::{collect_selected_entities, ordered_index};
75use super::move_selector::{
76    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor, MoveSelector, MoveStreamContext,
77};
78use super::precedence_route::{PrecedenceRouteGraph, PrecedenceRouteHooks};
79use super::sublist_support::{count_intra_sublist_swap_moves_for_len, count_sublist_segments};
80
81/// A move selector that generates sublist swap moves.
82///
83/// For each pair of segments (which may span different entities), generates
84/// a swap move. Intra-entity swaps require non-overlapping segments.
85///
86/// # Type Parameters
87/// * `S` - The solution type
88/// * `V` - The list element type
89/// * `ES` - The entity selector type
90pub struct SublistSwapMoveSelector<S, V, ES> {
91    entity_selector: ES,
92    // Minimum segment size (inclusive).
93    min_sublist_size: usize,
94    // Maximum segment size (inclusive).
95    max_sublist_size: usize,
96    list_len: fn(&S, usize) -> usize,
97    list_get: fn(&S, usize, usize) -> Option<V>,
98    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
99    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
100    element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
101    precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
102    variable_name: &'static str,
103    descriptor_index: usize,
104    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
105}
106
107#[derive(Clone, Copy)]
108struct ListSegment {
109    start: usize,
110    end: usize,
111}
112
113#[derive(Clone)]
114struct SublistSegmentCursor {
115    entity: usize,
116    route_len: usize,
117    min_seg: usize,
118    max_seg: usize,
119    context: MoveStreamContext,
120    descriptor_index: usize,
121    start_offset: usize,
122    size_offset: usize,
123    current_start: Option<usize>,
124    current_size_count: usize,
125}
126
127impl SublistSegmentCursor {
128    fn new(
129        entity: usize,
130        route_len: usize,
131        min_seg: usize,
132        max_seg: usize,
133        context: MoveStreamContext,
134        descriptor_index: usize,
135    ) -> Self {
136        Self {
137            entity,
138            route_len,
139            min_seg,
140            max_seg,
141            context,
142            descriptor_index,
143            start_offset: 0,
144            size_offset: 0,
145            current_start: None,
146            current_size_count: 0,
147        }
148    }
149}
150
151impl Iterator for SublistSegmentCursor {
152    type Item = ListSegment;
153
154    fn next(&mut self) -> Option<Self::Item> {
155        if self.route_len < self.min_seg {
156            return None;
157        }
158
159        loop {
160            if let Some(start) = self.current_start {
161                if self.size_offset < self.current_size_count {
162                    let segment_size = self.min_seg
163                        + ordered_index(
164                            self.size_offset,
165                            self.current_size_count,
166                            self.context,
167                            0x5B15_75A0_9000_0003 ^ self.entity as u64 ^ start as u64,
168                        );
169                    self.size_offset += 1;
170                    return Some(ListSegment {
171                        start,
172                        end: start + segment_size,
173                    });
174                }
175                self.current_start = None;
176            }
177
178            if self.start_offset >= self.route_len {
179                return None;
180            }
181
182            let start = ordered_index(
183                self.start_offset,
184                self.route_len,
185                self.context,
186                0x5B15_75A0_9000_0002 ^ self.entity as u64 ^ self.descriptor_index as u64,
187            );
188            self.start_offset += 1;
189            let max_valid = self.max_seg.min(self.route_len - start);
190            if max_valid < self.min_seg {
191                continue;
192            }
193            self.current_start = Some(start);
194            self.current_size_count = max_valid - self.min_seg + 1;
195            self.size_offset = 0;
196        }
197    }
198}
199
200pub struct SublistSwapMoveCursor<S, V>
201where
202    S: PlanningSolution,
203    V: Clone + PartialEq + Send + Sync + Debug + 'static,
204{
205    store: CandidateStore<S, SublistSwapMove<S, V>>,
206    entities: Vec<usize>,
207    route_lens: Vec<usize>,
208    context: MoveStreamContext,
209    element_owners: Option<Vec<Vec<OwnerRestriction>>>,
210    fixed_to_current_entity: bool,
211    precedence_route_graph: Option<PrecedenceRouteGraph>,
212    entity_a_idx: usize,
213    segment_a_cursor: Option<SublistSegmentCursor>,
214    first_segment: Option<ListSegment>,
215    entity_b_idx: usize,
216    segment_b_cursor: Option<SublistSegmentCursor>,
217    min_seg: usize,
218    max_seg: usize,
219    list_len: fn(&S, usize) -> usize,
220    list_get: fn(&S, usize, usize) -> Option<V>,
221    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
222    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
223    variable_name: &'static str,
224    descriptor_index: usize,
225}
226
227impl<S, V> SublistSwapMoveCursor<S, V>
228where
229    S: PlanningSolution,
230    V: Clone + PartialEq + Send + Sync + Debug + 'static,
231{
232    #[allow(clippy::too_many_arguments)]
233    fn new(
234        entities: Vec<usize>,
235        route_lens: Vec<usize>,
236        context: MoveStreamContext,
237        element_owners: Option<Vec<Vec<OwnerRestriction>>>,
238        fixed_to_current_entity: bool,
239        precedence_route_graph: Option<PrecedenceRouteGraph>,
240        min_seg: usize,
241        max_seg: usize,
242        list_len: fn(&S, usize) -> usize,
243        list_get: fn(&S, usize, usize) -> Option<V>,
244        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
245        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
246        variable_name: &'static str,
247        descriptor_index: usize,
248    ) -> Self {
249        Self {
250            store: CandidateStore::new(),
251            entities,
252            route_lens,
253            context,
254            element_owners,
255            fixed_to_current_entity,
256            precedence_route_graph,
257            entity_a_idx: 0,
258            segment_a_cursor: None,
259            first_segment: None,
260            entity_b_idx: 0,
261            segment_b_cursor: None,
262            min_seg,
263            max_seg,
264            list_len,
265            list_get,
266            sublist_remove,
267            sublist_insert,
268            variable_name,
269            descriptor_index,
270        }
271    }
272
273    pub(crate) fn with_precedence_route_graph(
274        mut self,
275        precedence_route_graph: Option<PrecedenceRouteGraph>,
276    ) -> Self {
277        self.precedence_route_graph = precedence_route_graph;
278        self
279    }
280
281    fn push_move(
282        &mut self,
283        entity_a: usize,
284        first: ListSegment,
285        entity_b: usize,
286        second: ListSegment,
287    ) -> CandidateId {
288        self.store.push(SublistSwapMove::new(
289            entity_a,
290            first.start,
291            first.end,
292            entity_b,
293            second.start,
294            second.end,
295            self.list_len,
296            self.list_get,
297            self.sublist_remove,
298            self.sublist_insert,
299            self.variable_name,
300            self.descriptor_index,
301        ))
302    }
303
304    fn segment_cursor(&self, entity_idx: usize) -> SublistSegmentCursor {
305        SublistSegmentCursor::new(
306            self.entities[entity_idx],
307            self.route_lens[entity_idx],
308            self.min_seg,
309            self.max_seg,
310            self.context,
311            self.descriptor_index,
312        )
313    }
314
315    fn segment_owner_allows(
316        &self,
317        entity_idx: usize,
318        segment: ListSegment,
319        dst_entity: usize,
320    ) -> bool {
321        self.element_owners.as_ref().is_none_or(|owners| {
322            selected_segment_allows(owners, entity_idx, segment.start, segment.end, dst_entity)
323        })
324    }
325
326    fn owner_allows_swap(
327        &self,
328        entity_a_idx: usize,
329        first: ListSegment,
330        entity_a: usize,
331        entity_b_idx: usize,
332        second: ListSegment,
333        entity_b: usize,
334    ) -> bool {
335        if entity_a == entity_b {
336            self.segment_owner_allows(entity_a_idx, first, entity_a)
337                && self.segment_owner_allows(entity_b_idx, second, entity_a)
338        } else {
339            self.segment_owner_allows(entity_a_idx, first, entity_b)
340                && self.segment_owner_allows(entity_b_idx, second, entity_a)
341        }
342    }
343
344    fn next_first_segment(&mut self) -> Option<ListSegment> {
345        loop {
346            if self.entity_a_idx >= self.entities.len() {
347                return None;
348            }
349            if self.segment_a_cursor.is_none() {
350                self.segment_a_cursor = Some(self.segment_cursor(self.entity_a_idx));
351            }
352            if let Some(first) = self
353                .segment_a_cursor
354                .as_mut()
355                .and_then(SublistSegmentCursor::next)
356            {
357                self.first_segment = Some(first);
358                self.entity_b_idx = self.entity_a_idx;
359                self.segment_b_cursor = None;
360                return Some(first);
361            }
362
363            self.entity_a_idx += 1;
364            self.segment_a_cursor = None;
365            self.first_segment = None;
366            self.entity_b_idx = self.entity_a_idx;
367            self.segment_b_cursor = None;
368        }
369    }
370}
371
372impl<S, V> MoveCursor<S, SublistSwapMove<S, V>> for SublistSwapMoveCursor<S, V>
373where
374    S: PlanningSolution,
375    V: Clone + PartialEq + Send + Sync + Debug + 'static,
376{
377    fn next_candidate(&mut self) -> Option<CandidateId> {
378        loop {
379            if self.entity_a_idx >= self.entities.len() {
380                return None;
381            }
382
383            let first = match self.first_segment {
384                Some(first) => first,
385                None => self.next_first_segment()?,
386            };
387            let entity_a = self.entities[self.entity_a_idx];
388            if self.entity_b_idx < self.entity_a_idx {
389                self.entity_b_idx = self.entity_a_idx;
390                self.segment_b_cursor = None;
391            }
392
393            while self.entity_b_idx < self.entities.len() {
394                if self.fixed_to_current_entity && self.entity_b_idx != self.entity_a_idx {
395                    break;
396                }
397                let entity_b = self.entities[self.entity_b_idx];
398                if self.segment_b_cursor.is_none() {
399                    self.segment_b_cursor = Some(self.segment_cursor(self.entity_b_idx));
400                }
401                while let Some(second) = self
402                    .segment_b_cursor
403                    .as_mut()
404                    .and_then(SublistSegmentCursor::next)
405                {
406                    if self.entity_a_idx == self.entity_b_idx {
407                        if second.start < first.end {
408                            continue;
409                        }
410                        if first.start == second.start && first.end == second.end {
411                            continue;
412                        }
413                    }
414                    if self.element_owners.is_some()
415                        && !self.owner_allows_swap(
416                            self.entity_a_idx,
417                            first,
418                            entity_a,
419                            self.entity_b_idx,
420                            second,
421                            entity_b,
422                        )
423                    {
424                        continue;
425                    }
426                    if entity_a == entity_b
427                        && self.precedence_route_graph.as_ref().is_some_and(|graph| {
428                            graph.intra_sublist_swap_introduces_cycle(
429                                entity_a,
430                                first.start,
431                                first.end,
432                                second.start,
433                                second.end,
434                            )
435                        })
436                    {
437                        continue;
438                    }
439                    return Some(self.push_move(entity_a, first, entity_b, second));
440                }
441                self.entity_b_idx += 1;
442                self.segment_b_cursor = None;
443            }
444
445            self.first_segment = None;
446            self.entity_b_idx = self.entity_a_idx;
447            self.segment_b_cursor = None;
448        }
449    }
450
451    fn candidate(&self, id: CandidateId) -> Option<MoveCandidateRef<'_, S, SublistSwapMove<S, V>>> {
452        self.store.candidate(id)
453    }
454
455    fn take_candidate(&mut self, id: CandidateId) -> SublistSwapMove<S, V> {
456        self.store.take_candidate(id)
457    }
458}
459
460impl<S, V> Iterator for SublistSwapMoveCursor<S, V>
461where
462    S: PlanningSolution,
463    V: Clone + PartialEq + Send + Sync + Debug + 'static,
464{
465    type Item = SublistSwapMove<S, V>;
466
467    fn next(&mut self) -> Option<Self::Item> {
468        let id = self.next_candidate()?;
469        Some(self.take_candidate(id))
470    }
471}
472
473fn sublist_segments_for_entity(
474    entity: usize,
475    route_len: usize,
476    min_seg: usize,
477    max_seg: usize,
478    context: MoveStreamContext,
479    descriptor_index: usize,
480) -> impl Iterator<Item = ListSegment> {
481    SublistSegmentCursor::new(
482        entity,
483        route_len,
484        min_seg,
485        max_seg,
486        context,
487        descriptor_index,
488    )
489}
490
491impl<S, V: Debug, ES: Debug> Debug for SublistSwapMoveSelector<S, V, ES> {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        f.debug_struct("SublistSwapMoveSelector")
494            .field("entity_selector", &self.entity_selector)
495            .field("min_sublist_size", &self.min_sublist_size)
496            .field("max_sublist_size", &self.max_sublist_size)
497            .field("variable_name", &self.variable_name)
498            .field("descriptor_index", &self.descriptor_index)
499            .finish()
500    }
501}
502
503impl<S, V, ES> SublistSwapMoveSelector<S, V, ES> {
504    /* Creates a new sublist swap move selector.
505
506    # Arguments
507    * `entity_selector` - Selects entities to consider for swaps
508    * `min_sublist_size` - Minimum segment length (must be ≥ 1)
509    * `max_sublist_size` - Maximum segment length
510    * `list_len` - Function to get list length for an entity
511    * `sublist_remove` - Function to drain range `[start, end)`, returning elements
512    * `sublist_insert` - Function to insert elements at a position
513    * `variable_name` - Name of the list variable
514    * `descriptor_index` - Entity descriptor index
515
516    # Panics
517    Panics if `min_sublist_size == 0` or `max_sublist_size < min_sublist_size`.
518    */
519    #[allow(clippy::too_many_arguments)]
520    pub fn new(
521        entity_selector: ES,
522        min_sublist_size: usize,
523        max_sublist_size: usize,
524        list_len: fn(&S, usize) -> usize,
525        list_get: fn(&S, usize, usize) -> Option<V>,
526        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
527        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
528        variable_name: &'static str,
529        descriptor_index: usize,
530    ) -> Self {
531        assert!(min_sublist_size >= 1, "min_sublist_size must be at least 1");
532        assert!(
533            max_sublist_size >= min_sublist_size,
534            "max_sublist_size must be >= min_sublist_size"
535        );
536        Self {
537            entity_selector,
538            min_sublist_size,
539            max_sublist_size,
540            list_len,
541            list_get,
542            sublist_remove,
543            sublist_insert,
544            element_owner_fn: None,
545            precedence_route_hooks: None,
546            variable_name,
547            descriptor_index,
548            _phantom: PhantomData,
549        }
550    }
551
552    pub fn with_element_owner_fn(
553        mut self,
554        element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
555    ) -> Self {
556        self.element_owner_fn = element_owner_fn;
557        self
558    }
559
560    pub(crate) fn with_precedence_route_hooks(
561        mut self,
562        precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
563    ) -> Self {
564        self.precedence_route_hooks = precedence_route_hooks;
565        self
566    }
567
568    pub(crate) fn precedence_route_hooks(&self) -> Option<PrecedenceRouteHooks<S, V>> {
569        self.precedence_route_hooks
570    }
571}
572
573impl<S, V, ES> MoveSelector<S, SublistSwapMove<S, V>> for SublistSwapMoveSelector<S, V, ES>
574where
575    S: PlanningSolution,
576    V: Clone + PartialEq + Send + Sync + Debug + 'static,
577    ES: EntitySelector<S>,
578{
579    type Cursor<'a>
580        = SublistSwapMoveCursor<S, V>
581    where
582        Self: 'a;
583
584    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
585        self.open_cursor_with_context(score_director, MoveStreamContext::default())
586    }
587
588    fn open_cursor_with_context<'a, D: Director<S>>(
589        &'a self,
590        score_director: &D,
591        context: MoveStreamContext,
592    ) -> Self::Cursor<'a> {
593        let min_seg = self.min_sublist_size;
594        let max_seg = self.max_sublist_size;
595
596        let mut selected =
597            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
598        selected.apply_stream_order(
599            context,
600            0x5B15_75A0_9000_0001 ^ self.descriptor_index as u64,
601        );
602        let owner_restrictions = crate::list_placement::selected_owner_restrictions(
603            self.element_owner_fn,
604            score_director.working_solution(),
605            score_director
606                .entity_count(self.descriptor_index)
607                .unwrap_or(0),
608            &selected.entities,
609            &selected.route_lens,
610            self.list_get,
611        );
612        let fixed_to_current_entity = owner_restrictions
613            .as_ref()
614            .is_some_and(SelectedOwnerRestrictions::is_fixed_to_current);
615        let element_owners = owner_restrictions.and_then(SelectedOwnerRestrictions::into_mixed);
616
617        SublistSwapMoveCursor::new(
618            selected.entities,
619            selected.route_lens,
620            context,
621            element_owners,
622            fixed_to_current_entity,
623            None,
624            min_seg,
625            max_seg,
626            self.list_len,
627            self.list_get,
628            self.sublist_remove,
629            self.sublist_insert,
630            self.variable_name,
631            self.descriptor_index,
632        )
633    }
634
635    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
636        let selected =
637            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
638        let Some(owner_restrictions) = crate::list_placement::selected_owner_restrictions(
639            self.element_owner_fn,
640            score_director.working_solution(),
641            score_director
642                .entity_count(self.descriptor_index)
643                .unwrap_or(0),
644            &selected.entities,
645            &selected.route_lens,
646            self.list_get,
647        ) else {
648            return unfiltered_sublist_swap_size(
649                &selected.route_lens,
650                self.min_sublist_size,
651                self.max_sublist_size,
652            );
653        };
654
655        if owner_restrictions.is_fixed_to_current() {
656            return selected
657                .route_lens
658                .iter()
659                .map(|&route_len| {
660                    count_intra_sublist_swap_moves_for_len(
661                        route_len,
662                        self.min_sublist_size,
663                        self.max_sublist_size,
664                    )
665                })
666                .sum();
667        }
668        let element_owners = owner_restrictions
669            .mixed()
670            .expect("non-fixed owner restrictions must retain mixed owner matrix");
671
672        let mut count = 0;
673        for (left_idx, &left_entity) in selected.entities.iter().enumerate() {
674            for left_segment in sublist_segments_for_entity(
675                left_entity,
676                selected.route_lens[left_idx],
677                self.min_sublist_size,
678                self.max_sublist_size,
679                MoveStreamContext::default(),
680                self.descriptor_index,
681            ) {
682                for (right_idx, &right_entity) in
683                    selected.entities.iter().enumerate().skip(left_idx)
684                {
685                    for right_segment in sublist_segments_for_entity(
686                        right_entity,
687                        selected.route_lens[right_idx],
688                        self.min_sublist_size,
689                        self.max_sublist_size,
690                        MoveStreamContext::default(),
691                        self.descriptor_index,
692                    ) {
693                        if left_idx == right_idx {
694                            if right_segment.start < left_segment.end {
695                                continue;
696                            }
697                            if left_segment.start == right_segment.start
698                                && left_segment.end == right_segment.end
699                            {
700                                continue;
701                            }
702                        }
703
704                        let allowed = if left_entity == right_entity {
705                            selected_segment_allows(
706                                element_owners,
707                                left_idx,
708                                left_segment.start,
709                                left_segment.end,
710                                left_entity,
711                            ) && selected_segment_allows(
712                                element_owners,
713                                right_idx,
714                                right_segment.start,
715                                right_segment.end,
716                                left_entity,
717                            )
718                        } else {
719                            selected_segment_allows(
720                                element_owners,
721                                left_idx,
722                                left_segment.start,
723                                left_segment.end,
724                                right_entity,
725                            ) && selected_segment_allows(
726                                element_owners,
727                                right_idx,
728                                right_segment.start,
729                                right_segment.end,
730                                left_entity,
731                            )
732                        };
733                        if allowed {
734                            count += 1;
735                        }
736                    }
737                }
738            }
739        }
740        count
741    }
742}
743
744fn unfiltered_sublist_swap_size(
745    route_lens: &[usize],
746    min_sublist_size: usize,
747    max_sublist_size: usize,
748) -> usize {
749    let segment_counts: Vec<usize> = route_lens
750        .iter()
751        .map(|&route_len| count_sublist_segments(route_len, min_sublist_size, max_sublist_size))
752        .collect();
753    let intra: usize = route_lens
754        .iter()
755        .map(|&route_len| {
756            count_intra_sublist_swap_moves_for_len(route_len, min_sublist_size, max_sublist_size)
757        })
758        .sum();
759    let inter: usize = (0..route_lens.len())
760        .flat_map(|left| (left + 1..route_lens.len()).map(move |right| (left, right)))
761        .map(|(left, right)| segment_counts[left] * segment_counts[right])
762        .sum();
763    intra + inter
764}