Skip to main content

solverforge_solver/heuristic/selector/
list_change.rs

1/* List change move selector for element relocation.
2
3Generates `ListChangeMove`s that relocate elements within or between list variables.
4Essential for vehicle routing and scheduling problems.
5
6# Example
7
8```
9use solverforge_solver::heuristic::selector::list_change::ListChangeMoveSelector;
10use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
11use solverforge_solver::heuristic::selector::MoveSelector;
12use solverforge_core::domain::PlanningSolution;
13use solverforge_core::score::SoftScore;
14
15#[derive(Clone, Debug)]
16struct Vehicle { visits: Vec<i32> }
17
18#[derive(Clone, Debug)]
19struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
20
21impl PlanningSolution for Solution {
22type Score = SoftScore;
23fn score(&self) -> Option<Self::Score> { self.score }
24fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
25}
26
27fn list_len(s: &Solution, entity_idx: usize) -> usize {
28s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
29}
30fn list_remove(s: &mut Solution, entity_idx: usize, pos: usize) -> Option<i32> {
31s.vehicles.get_mut(entity_idx).map(|v| v.visits.remove(pos))
32}
33fn list_insert(s: &mut Solution, entity_idx: usize, pos: usize, val: i32) {
34if let Some(v) = s.vehicles.get_mut(entity_idx) { v.visits.insert(pos, val); }
35}
36
37let selector = ListChangeMoveSelector::<Solution, i32, _>::new(
38FromSolutionEntitySelector::new(0),
39list_len,
40list_remove,
41list_insert,
42"visits",
430,
44);
45```
46*/
47
48use std::fmt::Debug;
49use std::marker::PhantomData;
50
51use solverforge_core::domain::PlanningSolution;
52use solverforge_scoring::Director;
53
54use crate::heuristic::r#move::ListChangeMove;
55
56use super::entity::EntitySelector;
57use super::list_support::collect_selected_entities;
58use super::move_selector::MoveSelector;
59
60/// A move selector that generates list change moves.
61///
62/// Enumerates all valid (source_entity, source_pos, dest_entity, dest_pos)
63/// combinations for relocating elements within or between list variables.
64///
65/// # Type Parameters
66/// * `S` - The solution type
67/// * `V` - The list element type
68///
69/// # Complexity
70///
71/// For n entities with average route length m:
72/// - Intra-entity moves: O(n * m * m)
73/// - Inter-entity moves: O(n * n * m * m)
74/// - Total: O(n² * m²) worst case
75///
76/// Use with a forager that quits early for better performance.
77pub struct ListChangeMoveSelector<S, V, ES> {
78    // Selects entities (vehicles) for moves.
79    entity_selector: ES,
80    list_len: fn(&S, usize) -> usize,
81    // Remove element at position.
82    list_remove: fn(&mut S, usize, usize) -> Option<V>,
83    // Insert element at position.
84    list_insert: fn(&mut S, usize, usize, V),
85    // Variable name for notifications.
86    variable_name: &'static str,
87    // Entity descriptor index.
88    descriptor_index: usize,
89    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
90}
91
92impl<S, V: Debug, ES: Debug> Debug for ListChangeMoveSelector<S, V, ES> {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        f.debug_struct("ListChangeMoveSelector")
95            .field("entity_selector", &self.entity_selector)
96            .field("variable_name", &self.variable_name)
97            .field("descriptor_index", &self.descriptor_index)
98            .finish()
99    }
100}
101
102impl<S, V, ES> ListChangeMoveSelector<S, V, ES> {
103    /// Creates a new list change move selector.
104    ///
105    /// # Arguments
106    /// * `entity_selector` - Selects entities to consider for moves
107    /// * `list_len` - Function to get list length for an entity
108    /// * `list_remove` - Function to remove element at position
109    /// * `list_insert` - Function to insert element at position
110    /// * `variable_name` - Name of the list variable
111    /// * `descriptor_index` - Entity descriptor index
112    pub fn new(
113        entity_selector: ES,
114        list_len: fn(&S, usize) -> usize,
115        list_remove: fn(&mut S, usize, usize) -> Option<V>,
116        list_insert: fn(&mut S, usize, usize, V),
117        variable_name: &'static str,
118        descriptor_index: usize,
119    ) -> Self {
120        Self {
121            entity_selector,
122            list_len,
123            list_remove,
124            list_insert,
125            variable_name,
126            descriptor_index,
127            _phantom: PhantomData,
128        }
129    }
130}
131
132impl<S, V, ES> MoveSelector<S, ListChangeMove<S, V>> for ListChangeMoveSelector<S, V, ES>
133where
134    S: PlanningSolution,
135    V: Clone + PartialEq + Send + Sync + Debug + 'static,
136    ES: EntitySelector<S>,
137{
138    fn open_cursor<'a, D: Director<S>>(
139        &'a self,
140        score_director: &D,
141    ) -> impl Iterator<Item = ListChangeMove<S, V>> + 'a {
142        let list_len = self.list_len;
143        let list_remove = self.list_remove;
144        let list_insert = self.list_insert;
145        let variable_name = self.variable_name;
146        let descriptor_index = self.descriptor_index;
147
148        let selected = collect_selected_entities(&self.entity_selector, score_director, list_len);
149        let move_capacity = selected.list_change_move_capacity();
150        let entities = selected.entities;
151        let route_lens = selected.route_lens;
152
153        let mut moves = Vec::with_capacity(move_capacity);
154
155        for (src_idx, &src_entity) in entities.iter().enumerate() {
156            let src_len = route_lens[src_idx];
157            if src_len == 0 {
158                continue;
159            }
160
161            for src_pos in 0..src_len {
162                // Intra-entity moves
163                for dst_pos in 0..src_len {
164                    /* Skip no-op moves:
165                    - Same position is obviously a no-op
166                    - Forward by 1 is a no-op due to index adjustment during do_move
167                    */
168                    if src_pos == dst_pos || dst_pos == src_pos + 1 {
169                        continue;
170                    }
171
172                    moves.push(ListChangeMove::new(
173                        src_entity,
174                        src_pos,
175                        src_entity,
176                        dst_pos,
177                        list_len,
178                        list_remove,
179                        list_insert,
180                        variable_name,
181                        descriptor_index,
182                    ));
183                }
184
185                // Inter-entity moves
186                for (dst_idx, &dst_entity) in entities.iter().enumerate() {
187                    if dst_idx == src_idx {
188                        continue;
189                    }
190
191                    let dst_len = route_lens[dst_idx];
192                    // Can insert at any position from 0 to dst_len inclusive
193                    for dst_pos in 0..=dst_len {
194                        moves.push(ListChangeMove::new(
195                            src_entity,
196                            src_pos,
197                            dst_entity,
198                            dst_pos,
199                            list_len,
200                            list_remove,
201                            list_insert,
202                            variable_name,
203                            descriptor_index,
204                        ));
205                    }
206                }
207            }
208        }
209
210        moves.into_iter()
211    }
212
213    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
214        collect_selected_entities(&self.entity_selector, score_director, self.list_len)
215            .list_change_move_capacity()
216    }
217}