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;
71
72use super::entity::EntitySelector;
73use super::list_support::{collect_selected_entities, ordered_index};
74use super::move_selector::{
75    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor, MoveSelector, MoveStreamContext,
76};
77use super::sublist_support::{count_intra_sublist_swap_moves_for_len, count_sublist_segments};
78
79/// A move selector that generates sublist swap moves.
80///
81/// For each pair of segments (which may span different entities), generates
82/// a swap move. Intra-entity swaps require non-overlapping segments.
83///
84/// # Type Parameters
85/// * `S` - The solution type
86/// * `V` - The list element type
87/// * `ES` - The entity selector type
88pub struct SublistSwapMoveSelector<S, V, ES> {
89    entity_selector: ES,
90    // Minimum segment size (inclusive).
91    min_sublist_size: usize,
92    // Maximum segment size (inclusive).
93    max_sublist_size: usize,
94    list_len: fn(&S, usize) -> usize,
95    list_get: fn(&S, usize, usize) -> Option<V>,
96    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
97    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
98    variable_name: &'static str,
99    descriptor_index: usize,
100    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
101}
102
103#[derive(Clone, Copy)]
104struct ListSegment {
105    start: usize,
106    end: usize,
107}
108
109pub struct SublistSwapMoveCursor<S, V>
110where
111    S: PlanningSolution,
112    V: Clone + PartialEq + Send + Sync + Debug + 'static,
113{
114    store: CandidateStore<S, SublistSwapMove<S, V>>,
115    entities: Vec<usize>,
116    segments_by_entity: Vec<Vec<ListSegment>>,
117    entity_a_idx: usize,
118    segment_a_idx: usize,
119    entity_b_idx: usize,
120    segment_b_idx: usize,
121    list_len: fn(&S, usize) -> usize,
122    list_get: fn(&S, usize, usize) -> Option<V>,
123    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
124    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
125    variable_name: &'static str,
126    descriptor_index: usize,
127}
128
129impl<S, V> SublistSwapMoveCursor<S, V>
130where
131    S: PlanningSolution,
132    V: Clone + PartialEq + Send + Sync + Debug + 'static,
133{
134    #[allow(clippy::too_many_arguments)]
135    fn new(
136        entities: Vec<usize>,
137        segments_by_entity: Vec<Vec<ListSegment>>,
138        list_len: fn(&S, usize) -> usize,
139        list_get: fn(&S, usize, usize) -> Option<V>,
140        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
141        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
142        variable_name: &'static str,
143        descriptor_index: usize,
144    ) -> Self {
145        Self {
146            store: CandidateStore::new(),
147            entities,
148            segments_by_entity,
149            entity_a_idx: 0,
150            segment_a_idx: 0,
151            entity_b_idx: 0,
152            segment_b_idx: 0,
153            list_len,
154            list_get,
155            sublist_remove,
156            sublist_insert,
157            variable_name,
158            descriptor_index,
159        }
160    }
161
162    fn push_move(
163        &mut self,
164        entity_a: usize,
165        first: ListSegment,
166        entity_b: usize,
167        second: ListSegment,
168    ) -> CandidateId {
169        self.store.push(SublistSwapMove::new(
170            entity_a,
171            first.start,
172            first.end,
173            entity_b,
174            second.start,
175            second.end,
176            self.list_len,
177            self.list_get,
178            self.sublist_remove,
179            self.sublist_insert,
180            self.variable_name,
181            self.descriptor_index,
182        ))
183    }
184}
185
186impl<S, V> MoveCursor<S, SublistSwapMove<S, V>> for SublistSwapMoveCursor<S, V>
187where
188    S: PlanningSolution,
189    V: Clone + PartialEq + Send + Sync + Debug + 'static,
190{
191    fn next_candidate(&mut self) -> Option<CandidateId> {
192        loop {
193            if self.entity_a_idx >= self.entities.len() {
194                return None;
195            }
196            if self.segment_a_idx >= self.segments_by_entity[self.entity_a_idx].len() {
197                self.entity_a_idx += 1;
198                self.segment_a_idx = 0;
199                self.entity_b_idx = self.entity_a_idx;
200                self.segment_b_idx = 0;
201                continue;
202            }
203
204            let entity_a = self.entities[self.entity_a_idx];
205            let first = self.segments_by_entity[self.entity_a_idx][self.segment_a_idx];
206            if self.entity_b_idx < self.entity_a_idx {
207                self.entity_b_idx = self.entity_a_idx;
208                self.segment_b_idx = 0;
209            }
210
211            while self.entity_b_idx < self.entities.len() {
212                let entity_b = self.entities[self.entity_b_idx];
213                while self.segment_b_idx < self.segments_by_entity[self.entity_b_idx].len() {
214                    let second = self.segments_by_entity[self.entity_b_idx][self.segment_b_idx];
215                    self.segment_b_idx += 1;
216                    if self.entity_a_idx == self.entity_b_idx {
217                        if second.start < first.end {
218                            continue;
219                        }
220                        if first.start == second.start && first.end == second.end {
221                            continue;
222                        }
223                    }
224                    return Some(self.push_move(entity_a, first, entity_b, second));
225                }
226                self.entity_b_idx += 1;
227                self.segment_b_idx = 0;
228            }
229
230            self.segment_a_idx += 1;
231            self.entity_b_idx = self.entity_a_idx;
232            self.segment_b_idx = 0;
233        }
234    }
235
236    fn candidate(&self, id: CandidateId) -> Option<MoveCandidateRef<'_, S, SublistSwapMove<S, V>>> {
237        self.store.candidate(id)
238    }
239
240    fn take_candidate(&mut self, id: CandidateId) -> SublistSwapMove<S, V> {
241        self.store.take_candidate(id)
242    }
243}
244
245impl<S, V> Iterator for SublistSwapMoveCursor<S, V>
246where
247    S: PlanningSolution,
248    V: Clone + PartialEq + Send + Sync + Debug + 'static,
249{
250    type Item = SublistSwapMove<S, V>;
251
252    fn next(&mut self) -> Option<Self::Item> {
253        let id = self.next_candidate()?;
254        Some(self.take_candidate(id))
255    }
256}
257
258fn build_segments_for_entity(
259    entity: usize,
260    route_len: usize,
261    min_seg: usize,
262    max_seg: usize,
263    context: MoveStreamContext,
264    descriptor_index: usize,
265) -> Vec<ListSegment> {
266    if route_len < min_seg {
267        return Vec::new();
268    }
269
270    let mut segments = Vec::new();
271    for start_offset in 0..route_len {
272        let start = ordered_index(
273            start_offset,
274            route_len,
275            context,
276            0x5B15_75A0_9000_0002 ^ entity as u64 ^ descriptor_index as u64,
277        );
278        let max_valid = max_seg.min(route_len - start);
279        if max_valid < min_seg {
280            continue;
281        }
282        let size_count = max_valid - min_seg + 1;
283        for size_offset in 0..size_count {
284            let segment_size = min_seg
285                + ordered_index(
286                    size_offset,
287                    size_count,
288                    context,
289                    0x5B15_75A0_9000_0003 ^ entity as u64 ^ start as u64,
290                );
291            segments.push(ListSegment {
292                start,
293                end: start + segment_size,
294            });
295        }
296    }
297    segments
298}
299
300impl<S, V: Debug, ES: Debug> Debug for SublistSwapMoveSelector<S, V, ES> {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        f.debug_struct("SublistSwapMoveSelector")
303            .field("entity_selector", &self.entity_selector)
304            .field("min_sublist_size", &self.min_sublist_size)
305            .field("max_sublist_size", &self.max_sublist_size)
306            .field("variable_name", &self.variable_name)
307            .field("descriptor_index", &self.descriptor_index)
308            .finish()
309    }
310}
311
312impl<S, V, ES> SublistSwapMoveSelector<S, V, ES> {
313    /* Creates a new sublist swap move selector.
314
315    # Arguments
316    * `entity_selector` - Selects entities to consider for swaps
317    * `min_sublist_size` - Minimum segment length (must be ≥ 1)
318    * `max_sublist_size` - Maximum segment length
319    * `list_len` - Function to get list length for an entity
320    * `sublist_remove` - Function to drain range `[start, end)`, returning elements
321    * `sublist_insert` - Function to insert elements at a position
322    * `variable_name` - Name of the list variable
323    * `descriptor_index` - Entity descriptor index
324
325    # Panics
326    Panics if `min_sublist_size == 0` or `max_sublist_size < min_sublist_size`.
327    */
328    #[allow(clippy::too_many_arguments)]
329    pub fn new(
330        entity_selector: ES,
331        min_sublist_size: usize,
332        max_sublist_size: usize,
333        list_len: fn(&S, usize) -> usize,
334        list_get: fn(&S, usize, usize) -> Option<V>,
335        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
336        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
337        variable_name: &'static str,
338        descriptor_index: usize,
339    ) -> Self {
340        assert!(min_sublist_size >= 1, "min_sublist_size must be at least 1");
341        assert!(
342            max_sublist_size >= min_sublist_size,
343            "max_sublist_size must be >= min_sublist_size"
344        );
345        Self {
346            entity_selector,
347            min_sublist_size,
348            max_sublist_size,
349            list_len,
350            list_get,
351            sublist_remove,
352            sublist_insert,
353            variable_name,
354            descriptor_index,
355            _phantom: PhantomData,
356        }
357    }
358}
359
360impl<S, V, ES> MoveSelector<S, SublistSwapMove<S, V>> for SublistSwapMoveSelector<S, V, ES>
361where
362    S: PlanningSolution,
363    V: Clone + PartialEq + Send + Sync + Debug + 'static,
364    ES: EntitySelector<S>,
365{
366    type Cursor<'a>
367        = SublistSwapMoveCursor<S, V>
368    where
369        Self: 'a;
370
371    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
372        self.open_cursor_with_context(score_director, MoveStreamContext::default())
373    }
374
375    fn open_cursor_with_context<'a, D: Director<S>>(
376        &'a self,
377        score_director: &D,
378        context: MoveStreamContext,
379    ) -> Self::Cursor<'a> {
380        let min_seg = self.min_sublist_size;
381        let max_seg = self.max_sublist_size;
382
383        let mut selected =
384            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
385        selected.apply_stream_order(
386            context,
387            0x5B15_75A0_9000_0001 ^ self.descriptor_index as u64,
388        );
389        let segments_by_entity = selected
390            .route_lens
391            .iter()
392            .enumerate()
393            .map(|(entity_offset, &route_len)| {
394                let entity = selected.entities[entity_offset];
395                build_segments_for_entity(
396                    entity,
397                    route_len,
398                    min_seg,
399                    max_seg,
400                    context,
401                    self.descriptor_index,
402                )
403            })
404            .collect();
405
406        SublistSwapMoveCursor::new(
407            selected.entities,
408            segments_by_entity,
409            self.list_len,
410            self.list_get,
411            self.sublist_remove,
412            self.sublist_insert,
413            self.variable_name,
414            self.descriptor_index,
415        )
416    }
417
418    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
419        let selected =
420            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
421        let segment_counts: Vec<usize> = selected
422            .route_lens
423            .iter()
424            .map(|&route_len| {
425                count_sublist_segments(route_len, self.min_sublist_size, self.max_sublist_size)
426            })
427            .collect();
428        let intra: usize = selected
429            .route_lens
430            .iter()
431            .map(|&route_len| {
432                count_intra_sublist_swap_moves_for_len(
433                    route_len,
434                    self.min_sublist_size,
435                    self.max_sublist_size,
436                )
437            })
438            .sum();
439        let inter: usize = (0..selected.route_lens.len())
440            .flat_map(|left| (left + 1..selected.route_lens.len()).map(move |right| (left, right)))
441            .map(|(left, right)| segment_counts[left] * segment_counts[right])
442            .sum();
443        intra + inter
444    }
445}