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::{ListMoveImpl, SubListChangeMove};
74
75use super::entity::EntitySelector;
76use super::list_support::collect_selected_entities;
77use super::move_selector::MoveSelector;
78use super::sublist_support::count_sublist_change_moves_for_len;
79
80/// A move selector that generates sublist change (Or-opt) moves.
81///
82/// For each source entity and each valid segment `[start, start+len)`, generates
83/// moves that insert the segment at every valid destination position in every
84/// entity (including the source entity for intra-route relocation).
85///
86/// # Type Parameters
87/// * `S` - The solution type
88/// * `V` - The list element type
89/// * `ES` - The entity selector type
90pub struct SubListChangeMoveSelector<S, V, ES> {
91    entity_selector: ES,
92    // Minimum segment size (inclusive). Usually 1.
93    min_sublist_size: usize,
94    // Maximum segment size (inclusive). Usually 3-5.
95    max_sublist_size: usize,
96    list_len: fn(&S, usize) -> usize,
97    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
98    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
99    variable_name: &'static str,
100    descriptor_index: usize,
101    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
102}
103
104impl<S, V: Debug, ES: Debug> Debug for SubListChangeMoveSelector<S, V, ES> {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("SubListChangeMoveSelector")
107            .field("entity_selector", &self.entity_selector)
108            .field("min_sublist_size", &self.min_sublist_size)
109            .field("max_sublist_size", &self.max_sublist_size)
110            .field("variable_name", &self.variable_name)
111            .field("descriptor_index", &self.descriptor_index)
112            .finish()
113    }
114}
115
116impl<S, V, ES> SubListChangeMoveSelector<S, V, ES> {
117    /* Creates a new sublist change move selector.
118
119    # Arguments
120    * `entity_selector` - Selects entities to generate moves for
121    * `min_sublist_size` - Minimum segment length (must be ≥ 1)
122    * `max_sublist_size` - Maximum segment length
123    * `list_len` - Function to get list length
124    * `sublist_remove` - Function to drain a range `[start, end)`, returning removed elements
125    * `sublist_insert` - Function to insert a slice at a position
126    * `variable_name` - Name of the list variable
127    * `descriptor_index` - Entity descriptor index
128
129    # Panics
130    Panics if `min_sublist_size == 0` or `max_sublist_size < min_sublist_size`.
131    */
132    #[allow(clippy::too_many_arguments)]
133    pub fn new(
134        entity_selector: ES,
135        min_sublist_size: usize,
136        max_sublist_size: usize,
137        list_len: fn(&S, usize) -> usize,
138        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
139        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
140        variable_name: &'static str,
141        descriptor_index: usize,
142    ) -> Self {
143        assert!(min_sublist_size >= 1, "min_sublist_size must be at least 1");
144        assert!(
145            max_sublist_size >= min_sublist_size,
146            "max_sublist_size must be >= min_sublist_size"
147        );
148        Self {
149            entity_selector,
150            min_sublist_size,
151            max_sublist_size,
152            list_len,
153            sublist_remove,
154            sublist_insert,
155            variable_name,
156            descriptor_index,
157            _phantom: PhantomData,
158        }
159    }
160}
161
162impl<S, V, ES> MoveSelector<S, SubListChangeMove<S, V>> for SubListChangeMoveSelector<S, V, ES>
163where
164    S: PlanningSolution,
165    V: Clone + PartialEq + Send + Sync + Debug + 'static,
166    ES: EntitySelector<S>,
167{
168    fn open_cursor<'a, D: Director<S>>(
169        &'a self,
170        score_director: &D,
171    ) -> impl Iterator<Item = SubListChangeMove<S, V>> + 'a {
172        let list_len = self.list_len;
173        let sublist_remove = self.sublist_remove;
174        let sublist_insert = self.sublist_insert;
175        let variable_name = self.variable_name;
176        let descriptor_index = self.descriptor_index;
177        let min_seg = self.min_sublist_size;
178        let max_seg = self.max_sublist_size;
179
180        let selected = collect_selected_entities(&self.entity_selector, score_director, list_len);
181        let entities = selected.entities;
182        let route_lens = selected.route_lens;
183        let mut moves = Vec::new();
184
185        for (src_idx, &src_entity) in entities.iter().enumerate() {
186            let src_len = route_lens[src_idx];
187
188            for seg_start in 0..src_len {
189                for seg_size in min_seg..=max_seg {
190                    let seg_end = seg_start + seg_size;
191                    if seg_end > src_len {
192                        break;
193                    }
194
195                    let post_removal_len = src_len - seg_size;
196                    for dst_pos in 0..=post_removal_len {
197                        if dst_pos == seg_start {
198                            continue;
199                        }
200                        moves.push(SubListChangeMove::new(
201                            src_entity,
202                            seg_start,
203                            seg_end,
204                            src_entity,
205                            dst_pos,
206                            list_len,
207                            sublist_remove,
208                            sublist_insert,
209                            variable_name,
210                            descriptor_index,
211                        ));
212                    }
213
214                    for (dst_idx, &dst_entity) in entities.iter().enumerate() {
215                        if dst_idx == src_idx {
216                            continue;
217                        }
218                        let dst_len = route_lens[dst_idx];
219                        for dst_pos in 0..=dst_len {
220                            moves.push(SubListChangeMove::new(
221                                src_entity,
222                                seg_start,
223                                seg_end,
224                                dst_entity,
225                                dst_pos,
226                                list_len,
227                                sublist_remove,
228                                sublist_insert,
229                                variable_name,
230                                descriptor_index,
231                            ));
232                        }
233                    }
234                }
235            }
236        }
237
238        moves.into_iter()
239    }
240
241    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
242        let selected =
243            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
244        let total_elements = selected.route_lens.iter().sum::<usize>();
245        let entity_count = selected.entities.len();
246
247        selected
248            .route_lens
249            .iter()
250            .map(|&route_len| {
251                let inter_destinations =
252                    total_elements.saturating_sub(route_len) + entity_count.saturating_sub(1);
253                count_sublist_change_moves_for_len(
254                    route_len,
255                    inter_destinations,
256                    self.min_sublist_size,
257                    self.max_sublist_size,
258                )
259            })
260            .sum()
261    }
262}
263
264/// Wraps a `SubListChangeMoveSelector` to yield `ListMoveImpl::SubListChange`.
265pub struct ListMoveSubListChangeSelector<S, V, ES> {
266    inner: SubListChangeMoveSelector<S, V, ES>,
267}
268
269impl<S, V: Debug, ES: Debug> Debug for ListMoveSubListChangeSelector<S, V, ES> {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        f.debug_struct("ListMoveSubListChangeSelector")
272            .field("inner", &self.inner)
273            .finish()
274    }
275}
276
277impl<S, V, ES> ListMoveSubListChangeSelector<S, V, ES> {
278    /// Wraps an existing [`SubListChangeMoveSelector`].
279    pub fn new(inner: SubListChangeMoveSelector<S, V, ES>) -> Self {
280        Self { inner }
281    }
282}
283
284impl<S, V, ES> MoveSelector<S, ListMoveImpl<S, V>> for ListMoveSubListChangeSelector<S, V, ES>
285where
286    S: PlanningSolution,
287    V: Clone + PartialEq + Send + Sync + Debug + 'static,
288    ES: EntitySelector<S>,
289{
290    fn open_cursor<'a, D: Director<S>>(
291        &'a self,
292        score_director: &D,
293    ) -> impl Iterator<Item = ListMoveImpl<S, V>> + 'a {
294        self.inner
295            .open_cursor(score_director)
296            .map(ListMoveImpl::SubListChange)
297    }
298
299    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
300        self.inner.size(score_director)
301    }
302}