Skip to main content

solverforge_solver/heuristic/selector/
list_reverse.rs

1/* List reverse move selector for 2-opt optimization.
2
3Generates `ListReverseMove`s that reverse contiguous segments within a single
4list. This is the fundamental 2-opt move for TSP and VRP: reversing a segment
5of the tour can eliminate crossing edges and reduce total distance.
6
7For VRP, 2-opt is applied independently within each route (intra-route 2-opt).
8Cross-route 2-opt would require inter-entity reversal, which is a different
9operation modeled by `SublistSwapMove` with same-size segments.
10
11# Complexity
12
13For n entities with average route length m:
14O(n * m²) — all (start, end) pairs per entity where end > start + 1.
15
16# Example
17
18```
19use solverforge_solver::heuristic::selector::list_reverse::ListReverseMoveSelector;
20use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
21use solverforge_solver::heuristic::selector::MoveSelector;
22use solverforge_core::domain::PlanningSolution;
23use solverforge_core::score::SoftScore;
24
25#[derive(Clone, Debug)]
26struct Vehicle { visits: Vec<i32> }
27
28#[derive(Clone, Debug)]
29struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
30
31impl PlanningSolution for Solution {
32type Score = SoftScore;
33fn score(&self) -> Option<Self::Score> { self.score }
34fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
35}
36
37fn list_len(s: &Solution, entity_idx: usize) -> usize {
38s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
39}
40fn list_reverse(s: &mut Solution, entity_idx: usize, start: usize, end: usize) {
41if let Some(v) = s.vehicles.get_mut(entity_idx) {
42v.visits[start..end].reverse();
43}
44}
45
46let selector = ListReverseMoveSelector::<Solution, i32, _>::new(
47FromSolutionEntitySelector::new(0),
48list_len,
49list_reverse,
50"visits",
510,
52);
53```
54*/
55
56use std::fmt::Debug;
57use std::marker::PhantomData;
58
59use solverforge_core::domain::PlanningSolution;
60use solverforge_scoring::Director;
61
62use crate::heuristic::r#move::ListReverseMove;
63
64use super::entity::EntitySelector;
65use super::list_support::ordered_index;
66use super::move_selector::{
67    CandidateId, CandidateStore, MoveCandidateRef, MoveCursor, MoveSelector, MoveStreamContext,
68};
69use super::precedence_route::{PrecedenceRouteGraph, PrecedenceRouteHooks};
70
71/// A move selector that generates 2-opt segment reversal moves.
72///
73/// For each entity, enumerates all valid (start, end) pairs where
74/// `end > start + 1` (at least 2 elements in the reversed segment).
75///
76/// # Type Parameters
77/// * `S` - The solution type
78/// * `V` - The list element type (phantom — only used for type safety)
79/// * `ES` - The entity selector type
80pub struct ListReverseMoveSelector<S, V, ES> {
81    entity_selector: ES,
82    list_len: fn(&S, usize) -> usize,
83    list_get: fn(&S, usize, usize) -> Option<V>,
84    list_reverse: fn(&mut S, usize, usize, usize),
85    precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
86    variable_name: &'static str,
87    descriptor_index: usize,
88    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
89}
90
91pub struct ListReverseMoveCursor<S, V>
92where
93    S: PlanningSolution,
94    V: Clone + Send + Sync + Debug + 'static,
95{
96    store: CandidateStore<S, ListReverseMove<S, V>>,
97    entities: Vec<usize>,
98    route_lens: Vec<usize>,
99    context: MoveStreamContext,
100    entity_idx: usize,
101    start_offset: usize,
102    end_offset: usize,
103    list_len: fn(&S, usize) -> usize,
104    list_get: fn(&S, usize, usize) -> Option<V>,
105    list_reverse: fn(&mut S, usize, usize, usize),
106    precedence_route_graph: Option<PrecedenceRouteGraph>,
107    variable_name: &'static str,
108    descriptor_index: usize,
109}
110
111impl<S, V> ListReverseMoveCursor<S, V>
112where
113    S: PlanningSolution,
114    V: Clone + Send + Sync + Debug + 'static,
115{
116    #[allow(clippy::too_many_arguments)]
117    fn new(
118        entities: Vec<usize>,
119        route_lens: Vec<usize>,
120        context: MoveStreamContext,
121        list_len: fn(&S, usize) -> usize,
122        list_get: fn(&S, usize, usize) -> Option<V>,
123        list_reverse: fn(&mut S, usize, usize, usize),
124        precedence_route_graph: Option<PrecedenceRouteGraph>,
125        variable_name: &'static str,
126        descriptor_index: usize,
127    ) -> Self {
128        Self {
129            store: CandidateStore::new(),
130            entities,
131            route_lens,
132            context,
133            entity_idx: 0,
134            start_offset: 0,
135            end_offset: 0,
136            list_len,
137            list_get,
138            list_reverse,
139            precedence_route_graph,
140            variable_name,
141            descriptor_index,
142        }
143    }
144
145    pub(crate) fn with_precedence_route_graph(
146        mut self,
147        precedence_route_graph: Option<PrecedenceRouteGraph>,
148    ) -> Self {
149        self.precedence_route_graph = precedence_route_graph;
150        self
151    }
152
153    fn push_move(&mut self, entity: usize, start: usize, end: usize) -> CandidateId {
154        self.store.push(ListReverseMove::new(
155            entity,
156            start,
157            end,
158            self.list_len,
159            self.list_get,
160            self.list_reverse,
161            self.variable_name,
162            self.descriptor_index,
163        ))
164    }
165}
166
167impl<S, V> MoveCursor<S, ListReverseMove<S, V>> for ListReverseMoveCursor<S, V>
168where
169    S: PlanningSolution,
170    V: Clone + Send + Sync + Debug + 'static,
171{
172    fn next_candidate(&mut self) -> Option<CandidateId> {
173        loop {
174            let entity = *self.entities.get(self.entity_idx)?;
175            let len = self.route_lens[self.entity_idx];
176            if len < 2 {
177                self.entity_idx += 1;
178                self.start_offset = 0;
179                self.end_offset = 0;
180                continue;
181            }
182
183            while self.start_offset < len {
184                let start = ordered_index(
185                    self.start_offset,
186                    len,
187                    self.context,
188                    0x1157_2A07_0000_0002 ^ entity as u64 ^ self.descriptor_index as u64,
189                );
190                let end_count = len.saturating_sub(start + 1);
191                if self.end_offset < end_count {
192                    let end = start
193                        + 2
194                        + ordered_index(
195                            self.end_offset,
196                            end_count,
197                            self.context,
198                            0x1157_2A07_0000_0003 ^ entity as u64 ^ start as u64,
199                        );
200                    self.end_offset += 1;
201                    if self.precedence_route_graph.as_ref().is_some_and(|graph| {
202                        graph.intra_list_reverse_introduces_cycle(entity, start, end)
203                    }) {
204                        continue;
205                    }
206                    return Some(self.push_move(entity, start, end));
207                }
208                self.start_offset += 1;
209                self.end_offset = 0;
210            }
211
212            self.entity_idx += 1;
213            self.start_offset = 0;
214            self.end_offset = 0;
215        }
216    }
217
218    fn candidate(&self, id: CandidateId) -> Option<MoveCandidateRef<'_, S, ListReverseMove<S, V>>> {
219        self.store.candidate(id)
220    }
221
222    fn take_candidate(&mut self, id: CandidateId) -> ListReverseMove<S, V> {
223        self.store.take_candidate(id)
224    }
225}
226
227impl<S, V> Iterator for ListReverseMoveCursor<S, V>
228where
229    S: PlanningSolution,
230    V: Clone + Send + Sync + Debug + 'static,
231{
232    type Item = ListReverseMove<S, V>;
233
234    fn next(&mut self) -> Option<Self::Item> {
235        let id = self.next_candidate()?;
236        Some(self.take_candidate(id))
237    }
238}
239
240impl<S, V: Debug, ES: Debug> Debug for ListReverseMoveSelector<S, V, ES> {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        f.debug_struct("ListReverseMoveSelector")
243            .field("entity_selector", &self.entity_selector)
244            .field("variable_name", &self.variable_name)
245            .field("descriptor_index", &self.descriptor_index)
246            .finish()
247    }
248}
249
250impl<S, V, ES> ListReverseMoveSelector<S, V, ES> {
251    /// Creates a new list reverse move selector.
252    ///
253    /// # Arguments
254    /// * `entity_selector` - Selects entities (routes) to apply 2-opt to
255    /// * `list_len` - Function to get route length
256    /// * `list_reverse` - Function to reverse `[start, end)` in-place
257    /// * `variable_name` - Name of the list variable
258    /// * `descriptor_index` - Entity descriptor index
259    pub fn new(
260        entity_selector: ES,
261        list_len: fn(&S, usize) -> usize,
262        list_get: fn(&S, usize, usize) -> Option<V>,
263        list_reverse: fn(&mut S, usize, usize, usize),
264        variable_name: &'static str,
265        descriptor_index: usize,
266    ) -> Self {
267        Self {
268            entity_selector,
269            list_len,
270            list_get,
271            list_reverse,
272            precedence_route_hooks: None,
273            variable_name,
274            descriptor_index,
275            _phantom: PhantomData,
276        }
277    }
278
279    pub(crate) fn with_precedence_route_hooks(
280        mut self,
281        precedence_route_hooks: Option<PrecedenceRouteHooks<S, V>>,
282    ) -> Self {
283        self.precedence_route_hooks = precedence_route_hooks;
284        self
285    }
286
287    pub(crate) fn precedence_route_hooks(&self) -> Option<PrecedenceRouteHooks<S, V>> {
288        self.precedence_route_hooks
289    }
290}
291
292impl<S, V, ES> MoveSelector<S, ListReverseMove<S, V>> for ListReverseMoveSelector<S, V, ES>
293where
294    S: PlanningSolution,
295    V: Clone + Send + Sync + Debug + 'static,
296    ES: EntitySelector<S>,
297{
298    type Cursor<'a>
299        = ListReverseMoveCursor<S, V>
300    where
301        Self: 'a;
302
303    fn open_cursor<'a, D: Director<S>>(&'a self, score_director: &D) -> Self::Cursor<'a> {
304        self.open_cursor_with_context(score_director, MoveStreamContext::default())
305    }
306
307    fn open_cursor_with_context<'a, D: Director<S>>(
308        &'a self,
309        score_director: &D,
310        context: MoveStreamContext,
311    ) -> Self::Cursor<'a> {
312        let mut entities: Vec<usize> = self
313            .entity_selector
314            .iter(score_director)
315            .map(|r| r.entity_index)
316            .collect();
317        let entity_start = context.start_offset(
318            entities.len(),
319            0x1157_2A07_0000_0001 ^ self.descriptor_index as u64,
320        );
321        entities.rotate_left(entity_start);
322
323        let solution = score_director.working_solution();
324        let route_lens = entities
325            .iter()
326            .map(|&entity| (self.list_len)(solution, entity))
327            .collect();
328
329        ListReverseMoveCursor::new(
330            entities,
331            route_lens,
332            context,
333            self.list_len,
334            self.list_get,
335            self.list_reverse,
336            None,
337            self.variable_name,
338            self.descriptor_index,
339        )
340    }
341
342    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
343        let solution = score_director.working_solution();
344        let list_len = self.list_len;
345
346        self.entity_selector
347            .iter(score_director)
348            .map(|r| {
349                let m = list_len(solution, r.entity_index);
350                // Number of valid (start, end) pairs: m*(m-1)/2 - m = m*(m-1)/2 - m
351                // For start in 0..m, end in start+2..=m: sum = m*(m-1)/2
352                if m >= 2 {
353                    m * (m - 1) / 2
354                } else {
355                    0
356                }
357            })
358            .sum()
359    }
360}