Skip to main content

solverforge_solver/heuristic/selector/
list_swap.rs

1/* List swap move selector for element exchange.
2
3Generates `ListSwapMove`s that swap elements within or between list variables.
4Useful for inter-route rebalancing in vehicle routing problems.
5
6# Complexity
7
8For n entities with average route length m:
9- Intra-entity swaps: O(n * m * (m-1) / 2)
10- Inter-entity swaps: O(n² * m²)
11- Total: O(n² * m²) worst case (triangular optimization halves constant)
12
13# Example
14
15```
16use solverforge_solver::heuristic::selector::list_swap::ListSwapMoveSelector;
17use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
18use solverforge_solver::heuristic::selector::MoveSelector;
19use solverforge_core::domain::PlanningSolution;
20use solverforge_core::score::SoftScore;
21
22#[derive(Clone, Debug)]
23struct Vehicle { visits: Vec<i32> }
24
25#[derive(Clone, Debug)]
26struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
27
28impl PlanningSolution for Solution {
29type Score = SoftScore;
30fn score(&self) -> Option<Self::Score> { self.score }
31fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
32}
33
34fn list_len(s: &Solution, entity_idx: usize) -> usize {
35s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
36}
37fn list_get(s: &Solution, entity_idx: usize, pos: usize) -> Option<i32> {
38s.vehicles.get(entity_idx).and_then(|v| v.visits.get(pos).copied())
39}
40fn list_set(s: &mut Solution, entity_idx: usize, pos: usize, val: i32) {
41if let Some(v) = s.vehicles.get_mut(entity_idx) {
42if let Some(elem) = v.visits.get_mut(pos) { *elem = val; }
43}
44}
45
46let selector = ListSwapMoveSelector::<Solution, i32, _>::new(
47FromSolutionEntitySelector::new(0),
48list_len,
49list_get,
50list_set,
51"visits",
520,
53);
54```
55*/
56
57use std::fmt::Debug;
58use std::marker::PhantomData;
59
60use solverforge_core::domain::PlanningSolution;
61use solverforge_scoring::Director;
62
63use crate::heuristic::r#move::ListSwapMove;
64use crate::list_placement::{selected_owner_allows, OwnerRestriction, SelectedOwnerRestrictions};
65
66use super::entity::EntitySelector;
67use super::list_support::{collect_selected_entities, ordered_index};
68use super::move_selector::{
69    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor, MoveSelector, MoveStreamContext,
70};
71use super::precedence_route::{PrecedenceRouteGraph, PrecedenceRouteHooks};
72
73/// A move selector that generates list swap moves.
74///
75/// Enumerates all valid (entity_a, pos_a, entity_b, pos_b) pairs for swapping
76/// elements within or between list variables. Intra-entity swaps use a
77/// triangular iteration to avoid duplicate pairs.
78///
79/// # Type Parameters
80/// * `S` - The solution type
81/// * `V` - The list element type
82/// * `ES` - The entity selector type
83pub struct ListSwapMoveSelector<S, V, ES> {
84    entity_selector: ES,
85    list_len: fn(&S, usize) -> usize,
86    list_get: fn(&S, usize, usize) -> Option<V>,
87    list_set: fn(&mut S, usize, usize, V),
88    element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
89    precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
90    variable_name: &'static str,
91    descriptor_index: usize,
92    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
93}
94
95enum ListSwapStage {
96    Intra,
97    Inter,
98}
99
100pub struct ListSwapMoveCursor<S, V>
101where
102    S: PlanningSolution,
103    V: Clone + PartialEq + Send + Sync + Debug + 'static,
104{
105    store: CandidateStore<S, ListSwapMove<S, V>>,
106    entities: Vec<usize>,
107    route_lens: Vec<usize>,
108    context: MoveStreamContext,
109    entity_idx: usize,
110    stage: ListSwapStage,
111    pos_a_offset: usize,
112    pos_b_offset: usize,
113    dst_idx: usize,
114    inter_pos_a_offset: usize,
115    inter_pos_b_offset: usize,
116    list_len: fn(&S, usize) -> usize,
117    list_get: fn(&S, usize, usize) -> Option<V>,
118    list_set: fn(&mut S, usize, usize, V),
119    element_owners: Option<Vec<Vec<OwnerRestriction>>>,
120    fixed_to_current_entity: bool,
121    precedence_route_graph: Option<PrecedenceRouteGraph>,
122    variable_name: &'static str,
123    descriptor_index: usize,
124}
125
126impl<S, V> ListSwapMoveCursor<S, V>
127where
128    S: PlanningSolution,
129    V: Clone + PartialEq + Send + Sync + Debug + 'static,
130{
131    #[allow(clippy::too_many_arguments)]
132    fn new(
133        entities: Vec<usize>,
134        route_lens: Vec<usize>,
135        context: MoveStreamContext,
136        list_len: fn(&S, usize) -> usize,
137        list_get: fn(&S, usize, usize) -> Option<V>,
138        list_set: fn(&mut S, usize, usize, V),
139        element_owners: Option<Vec<Vec<OwnerRestriction>>>,
140        fixed_to_current_entity: bool,
141        precedence_route_graph: Option<PrecedenceRouteGraph>,
142        variable_name: &'static str,
143        descriptor_index: usize,
144    ) -> Self {
145        Self {
146            store: CandidateStore::new(),
147            entities,
148            route_lens,
149            context,
150            entity_idx: 0,
151            stage: ListSwapStage::Intra,
152            pos_a_offset: 0,
153            pos_b_offset: 0,
154            dst_idx: 1,
155            inter_pos_a_offset: 0,
156            inter_pos_b_offset: 0,
157            list_len,
158            list_get,
159            list_set,
160            element_owners,
161            fixed_to_current_entity,
162            precedence_route_graph,
163            variable_name,
164            descriptor_index,
165        }
166    }
167
168    pub(crate) fn with_precedence_route_graph(
169        mut self,
170        precedence_route_graph: Option<PrecedenceRouteGraph>,
171    ) -> Self {
172        self.precedence_route_graph = precedence_route_graph;
173        self
174    }
175
176    fn push_move(
177        &mut self,
178        entity_a: usize,
179        pos_a: usize,
180        entity_b: usize,
181        pos_b: usize,
182    ) -> CandidateId {
183        self.store.push(ListSwapMove::new(
184            entity_a,
185            pos_a,
186            entity_b,
187            pos_b,
188            self.list_len,
189            self.list_get,
190            self.list_set,
191            self.variable_name,
192            self.descriptor_index,
193        ))
194    }
195
196    fn restriction_at(&self, entity_idx: usize, pos: usize) -> Option<OwnerRestriction> {
197        self.element_owners
198            .as_ref()?
199            .get(entity_idx)?
200            .get(pos)
201            .copied()
202    }
203
204    fn owner_allows_swap(
205        &self,
206        entity_a_idx: usize,
207        pos_a: usize,
208        entity_a: usize,
209        entity_b_idx: usize,
210        pos_b: usize,
211        entity_b: usize,
212    ) -> bool {
213        self.element_owners.as_ref().is_none_or(|_| {
214            self.restriction_at(entity_a_idx, pos_a)
215                .is_some_and(|restriction| restriction.allows(entity_b))
216                && self
217                    .restriction_at(entity_b_idx, pos_b)
218                    .is_some_and(|restriction| restriction.allows(entity_a))
219        })
220    }
221
222    fn advance_entity(&mut self) {
223        self.entity_idx += 1;
224        self.stage = ListSwapStage::Intra;
225        self.pos_a_offset = 0;
226        self.pos_b_offset = 0;
227        self.dst_idx = self.entity_idx + 1;
228        self.inter_pos_a_offset = 0;
229        self.inter_pos_b_offset = 0;
230    }
231}
232
233impl<S, V> MoveCursor<S, ListSwapMove<S, V>> for ListSwapMoveCursor<S, V>
234where
235    S: PlanningSolution,
236    V: Clone + PartialEq + Send + Sync + Debug + 'static,
237{
238    fn next_candidate(&mut self) -> Option<CandidateId> {
239        loop {
240            if self.entity_idx >= self.entities.len() {
241                return None;
242            }
243
244            let entity_a = self.entities[self.entity_idx];
245            let len_a = self.route_lens[self.entity_idx];
246            if len_a == 0 {
247                self.advance_entity();
248                continue;
249            }
250
251            match self.stage {
252                ListSwapStage::Intra => {
253                    while self.pos_a_offset < len_a {
254                        let pos_a = ordered_index(
255                            self.pos_a_offset,
256                            len_a,
257                            self.context,
258                            0x1157_5A09_0000_0002 ^ entity_a as u64 ^ self.descriptor_index as u64,
259                        );
260                        let pos_b_count = len_a.saturating_sub(pos_a + 1);
261                        if self.pos_b_offset < pos_b_count {
262                            let pos_b = pos_a
263                                + 1
264                                + ordered_index(
265                                    self.pos_b_offset,
266                                    pos_b_count,
267                                    self.context,
268                                    0x1157_5A09_0000_0003 ^ entity_a as u64 ^ pos_a as u64,
269                                );
270                            self.pos_b_offset += 1;
271                            if self.element_owners.is_some()
272                                && (!self
273                                    .restriction_at(self.entity_idx, pos_a)
274                                    .is_some_and(|restriction| restriction.allows(entity_a))
275                                    || !self
276                                        .restriction_at(self.entity_idx, pos_b)
277                                        .is_some_and(|restriction| restriction.allows(entity_a)))
278                            {
279                                continue;
280                            }
281                            if self.precedence_route_graph.as_ref().is_some_and(|graph| {
282                                graph.intra_list_swap_introduces_cycle(entity_a, pos_a, pos_b)
283                            }) {
284                                continue;
285                            }
286                            return Some(self.push_move(entity_a, pos_a, entity_a, pos_b));
287                        }
288                        self.pos_a_offset += 1;
289                        self.pos_b_offset = 0;
290                    }
291                    if self.fixed_to_current_entity {
292                        self.advance_entity();
293                        continue;
294                    }
295                    self.stage = ListSwapStage::Inter;
296                    self.dst_idx = self.entity_idx + 1;
297                    self.inter_pos_a_offset = 0;
298                    self.inter_pos_b_offset = 0;
299                }
300                ListSwapStage::Inter => {
301                    while self.dst_idx < self.entities.len() {
302                        let entity_b = self.entities[self.dst_idx];
303                        let len_b = self.route_lens[self.dst_idx];
304                        if len_b == 0 {
305                            self.dst_idx += 1;
306                            continue;
307                        }
308
309                        while self.inter_pos_a_offset < len_a {
310                            let pos_a = ordered_index(
311                                self.inter_pos_a_offset,
312                                len_a,
313                                self.context,
314                                0x1157_5A09_0000_0004 ^ entity_a as u64 ^ entity_b as u64,
315                            );
316                            if self.inter_pos_b_offset < len_b {
317                                let pos_b = ordered_index(
318                                    self.inter_pos_b_offset,
319                                    len_b,
320                                    self.context,
321                                    0x1157_5A09_0000_0005
322                                        ^ entity_a as u64
323                                        ^ entity_b as u64
324                                        ^ pos_a as u64,
325                                );
326                                self.inter_pos_b_offset += 1;
327                                if self.element_owners.is_some()
328                                    && !self.owner_allows_swap(
329                                        self.entity_idx,
330                                        pos_a,
331                                        entity_a,
332                                        self.dst_idx,
333                                        pos_b,
334                                        entity_b,
335                                    )
336                                {
337                                    continue;
338                                }
339                                return Some(self.push_move(entity_a, pos_a, entity_b, pos_b));
340                            }
341                            self.inter_pos_a_offset += 1;
342                            self.inter_pos_b_offset = 0;
343                        }
344                        self.dst_idx += 1;
345                        self.inter_pos_a_offset = 0;
346                        self.inter_pos_b_offset = 0;
347                    }
348                    self.advance_entity();
349                }
350            }
351        }
352    }
353
354    fn candidate(&self, id: CandidateId) -> Option<MoveCandidateRef<'_, S, ListSwapMove<S, V>>> {
355        self.store.candidate(id)
356    }
357
358    fn take_candidate(&mut self, id: CandidateId) -> ListSwapMove<S, V> {
359        self.store.take_candidate(id)
360    }
361}
362
363impl<S, V> Iterator for ListSwapMoveCursor<S, V>
364where
365    S: PlanningSolution,
366    V: Clone + PartialEq + Send + Sync + Debug + 'static,
367{
368    type Item = ListSwapMove<S, V>;
369
370    fn next(&mut self) -> Option<Self::Item> {
371        let id = self.next_candidate()?;
372        Some(self.take_candidate(id))
373    }
374}
375
376impl<S, V: Debug, ES: Debug> Debug for ListSwapMoveSelector<S, V, ES> {
377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        f.debug_struct("ListSwapMoveSelector")
379            .field("entity_selector", &self.entity_selector)
380            .field("variable_name", &self.variable_name)
381            .field("descriptor_index", &self.descriptor_index)
382            .finish()
383    }
384}
385
386impl<S, V, ES> ListSwapMoveSelector<S, V, ES> {
387    /// Creates a new list swap move selector.
388    ///
389    /// # Arguments
390    /// * `entity_selector` - Selects entities to consider for swaps
391    /// * `list_len` - Function to get list length for an entity
392    /// * `list_get` - Function to get element at position
393    /// * `list_set` - Function to set element at position
394    /// * `variable_name` - Name of the list variable
395    /// * `descriptor_index` - Entity descriptor index
396    pub fn new(
397        entity_selector: ES,
398        list_len: fn(&S, usize) -> usize,
399        list_get: fn(&S, usize, usize) -> Option<V>,
400        list_set: fn(&mut S, usize, usize, V),
401        variable_name: &'static str,
402        descriptor_index: usize,
403    ) -> Self {
404        Self {
405            entity_selector,
406            list_len,
407            list_get,
408            list_set,
409            element_owner_fn: None,
410            precedence_route_hooks: None,
411            variable_name,
412            descriptor_index,
413            _phantom: PhantomData,
414        }
415    }
416
417    pub fn with_element_owner_fn(
418        mut self,
419        element_owner_fn: Option<fn(&S, &V) -> Option<usize>>,
420    ) -> Self {
421        self.element_owner_fn = element_owner_fn;
422        self
423    }
424
425    pub(crate) fn with_precedence_route_hooks(
426        mut self,
427        precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
428    ) -> Self {
429        self.precedence_route_hooks = precedence_route_hooks;
430        self
431    }
432
433    pub(crate) fn precedence_route_hooks(&self) -> Option<PrecedenceRouteHooks<S, V>> {
434        self.precedence_route_hooks
435    }
436}
437
438impl<S, V, ES> MoveSelector<S, ListSwapMove<S, V>> for ListSwapMoveSelector<S, V, ES>
439where
440    S: PlanningSolution,
441    V: Clone + PartialEq + Send + Sync + Debug + 'static,
442    ES: EntitySelector<S>,
443{
444    type Cursor<'a>
445        = ListSwapMoveCursor<S, V>
446    where
447        Self: 'a;
448
449    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
450        self.open_cursor_with_context(score_director, MoveStreamContext::default())
451    }
452
453    fn open_cursor_with_context<'a, D: Director<S>>(
454        &'a self,
455        score_director: &D,
456        context: MoveStreamContext,
457    ) -> Self::Cursor<'a> {
458        let mut selected =
459            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
460        selected.apply_stream_order(
461            context,
462            0x1157_5A09_0000_0001 ^ self.descriptor_index as u64,
463        );
464        let owner_restrictions = crate::list_placement::selected_owner_restrictions(
465            self.element_owner_fn,
466            score_director.working_solution(),
467            score_director
468                .entity_count(self.descriptor_index)
469                .unwrap_or(0),
470            &selected.entities,
471            &selected.route_lens,
472            self.list_get,
473        );
474        let fixed_to_current_entity = owner_restrictions
475            .as_ref()
476            .is_some_and(SelectedOwnerRestrictions::is_fixed_to_current);
477        let element_owners = owner_restrictions.and_then(SelectedOwnerRestrictions::into_mixed);
478        ListSwapMoveCursor::new(
479            selected.entities,
480            selected.route_lens,
481            context,
482            self.list_len,
483            self.list_get,
484            self.list_set,
485            element_owners,
486            fixed_to_current_entity,
487            None,
488            self.variable_name,
489            self.descriptor_index,
490        )
491    }
492
493    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
494        let selected =
495            collect_selected_entities(&self.entity_selector, score_director, self.list_len);
496        let Some(owner_restrictions) = crate::list_placement::selected_owner_restrictions(
497            self.element_owner_fn,
498            score_director.working_solution(),
499            score_director
500                .entity_count(self.descriptor_index)
501                .unwrap_or(0),
502            &selected.entities,
503            &selected.route_lens,
504            self.list_get,
505        ) else {
506            return selected.list_swap_move_capacity();
507        };
508
509        if owner_restrictions.is_fixed_to_current() {
510            return selected
511                .route_lens
512                .iter()
513                .map(|&len| len * len.saturating_sub(1) / 2)
514                .sum();
515        }
516        let element_owners = owner_restrictions
517            .mixed()
518            .expect("non-fixed owner restrictions must retain mixed owner matrix");
519
520        let mut count = 0;
521        for (left_idx, (&left_entity, &left_len)) in selected
522            .entities
523            .iter()
524            .zip(selected.route_lens.iter())
525            .enumerate()
526        {
527            for left_pos in 0..left_len {
528                for right_pos in left_pos + 1..left_len {
529                    if selected_owner_allows(element_owners, left_idx, left_pos, left_entity)
530                        && selected_owner_allows(element_owners, left_idx, right_pos, left_entity)
531                    {
532                        count += 1;
533                    }
534                }
535                for (right_idx, (&right_entity, &right_len)) in selected
536                    .entities
537                    .iter()
538                    .zip(selected.route_lens.iter())
539                    .enumerate()
540                    .skip(left_idx + 1)
541                {
542                    for right_pos in 0..right_len {
543                        if selected_owner_allows(element_owners, left_idx, left_pos, right_entity)
544                            && selected_owner_allows(
545                                element_owners,
546                                right_idx,
547                                right_pos,
548                                left_entity,
549                            )
550                        {
551                            count += 1;
552                        }
553                    }
554                }
555            }
556        }
557        count
558    }
559}