Skip to main content

solverforge_solver/heuristic/move/
k_opt.rs

1/* K-opt move for tour optimization.
2
3K-opt removes k edges from a tour and reconnects the resulting segments
4in a different order, potentially reversing some segments. This is a
5fundamental move for TSP and VRP optimization.
6
7# Zero-Erasure Design
8
9- Fixed arrays for cut points (no SmallVec for static data)
10- Reconnection pattern stored by value (`KOptReconnection` is `Copy`)
11- Typed function pointers for all list operations
12
13# Example
14
15```
16use solverforge_solver::heuristic::r#move::{KOptMove, CutPoint};
17use solverforge_solver::heuristic::r#move::k_opt_reconnection::THREE_OPT_RECONNECTIONS;
18use solverforge_core::domain::PlanningSolution;
19use solverforge_core::score::SoftScore;
20
21#[derive(Clone, Debug)]
22struct Tour { cities: Vec<i32>, score: Option<SoftScore> }
23
24impl PlanningSolution for Tour {
25type Score = SoftScore;
26fn score(&self) -> Option<Self::Score> { self.score }
27fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
28}
29
30fn list_len(s: &Tour, _: usize) -> usize { s.cities.len() }
31fn sublist_remove(s: &mut Tour, _: usize, start: usize, end: usize) -> Vec<i32> {
32s.cities.drain(start..end).collect()
33}
34fn sublist_insert(s: &mut Tour, _: usize, pos: usize, items: Vec<i32>) {
35for (i, item) in items.into_iter().enumerate() {
36s.cities.insert(pos + i, item);
37}
38}
39
40// Create a 3-opt move with cuts at positions 2, 4, 6
41// This creates 4 segments: [0..2), [2..4), [4..6), [6..)
42let cuts = [
43CutPoint::new(0, 2),
44CutPoint::new(0, 4),
45CutPoint::new(0, 6),
46];
47let reconnection = &THREE_OPT_RECONNECTIONS[3]; // Swap middle segments
48
49let m = KOptMove::<Tour, i32>::new(
50&cuts,
51reconnection,
52list_len,
53sublist_remove,
54sublist_insert,
55"cities",
560,
57);
58```
59*/
60
61use std::fmt::Debug;
62use std::marker::PhantomData;
63
64use solverforge_core::domain::PlanningSolution;
65use solverforge_scoring::Director;
66
67use super::k_opt_reconnection::KOptReconnection;
68use super::Move;
69
70/* A cut point in a route, defining where an edge is removed.
71
72For k-opt, we have k cut points which divide the route into k+1 segments.
73
74# Example
75
76```
77use solverforge_solver::heuristic::r#move::CutPoint;
78
79// Cut at position 5 in entity 0
80let cut = CutPoint::new(0, 5);
81assert_eq!(cut.entity_index(), 0);
82assert_eq!(cut.position(), 5);
83```
84*/
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86pub struct CutPoint {
87    // Entity (route/vehicle) index.
88    entity_index: usize,
89    // Position in the route where the cut occurs.
90    // The edge between position-1 and position is removed.
91    position: usize,
92}
93
94impl CutPoint {
95    #[inline]
96    pub const fn new(entity_index: usize, position: usize) -> Self {
97        Self {
98            entity_index,
99            position,
100        }
101    }
102
103    #[inline]
104    pub const fn entity_index(&self) -> usize {
105        self.entity_index
106    }
107
108    #[inline]
109    pub const fn position(&self) -> usize {
110        self.position
111    }
112}
113
114/// A k-opt move that removes k edges and reconnects segments.
115///
116/// This is the generalized k-opt move supporting k=2,3,4,5.
117/// For k=2, this is equivalent to a 2-opt (segment reversal).
118///
119/// # Zero-Erasure Design
120///
121/// - Fixed array `[CutPoint; 5]` for up to 5 cuts (5-opt)
122/// - `KOptReconnection` stored by value (`Copy` type, no heap allocation)
123/// - Typed function pointers for list operations
124///
125/// # Type Parameters
126///
127/// * `S` - The planning solution type
128/// * `V` - The list element value type
129pub struct KOptMove<S, V> {
130    // Cut points (up to 5 for 5-opt).
131    cuts: [CutPoint; 5],
132    // Number of actual cuts (k value).
133    cut_count: u8,
134    // Reconnection pattern to apply (stored by value — `KOptReconnection` is `Copy`).
135    reconnection: KOptReconnection,
136    list_len: fn(&S, usize) -> usize,
137    // Remove sublist [start, end), returns removed elements.
138    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
139    // Insert elements at position.
140    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
141    // Variable name.
142    variable_name: &'static str,
143    // Descriptor index.
144    descriptor_index: usize,
145    // Entity index (for intra-route moves).
146    entity_index: usize,
147    _phantom: PhantomData<fn() -> V>,
148}
149
150impl<S, V> Clone for KOptMove<S, V> {
151    fn clone(&self) -> Self {
152        Self {
153            cuts: self.cuts,
154            cut_count: self.cut_count,
155            reconnection: self.reconnection,
156            list_len: self.list_len,
157            sublist_remove: self.sublist_remove,
158            sublist_insert: self.sublist_insert,
159            variable_name: self.variable_name,
160            descriptor_index: self.descriptor_index,
161            entity_index: self.entity_index,
162            _phantom: PhantomData,
163        }
164    }
165}
166
167impl<S, V: Debug> Debug for KOptMove<S, V> {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        let cuts: Vec<_> = self.cuts[..self.cut_count as usize]
170            .iter()
171            .map(|c| c.position)
172            .collect();
173        f.debug_struct("KOptMove")
174            .field("k", &self.cut_count)
175            .field("entity", &self.entity_index)
176            .field("cuts", &cuts)
177            .field("reconnection", &self.reconnection)
178            .field("variable_name", &self.variable_name)
179            .finish()
180    }
181}
182
183impl<S, V> KOptMove<S, V> {
184    /* Creates a new k-opt move.
185
186    # Arguments
187
188    * `cuts` - Slice of cut points (must be sorted by position for intra-route)
189    * `reconnection` - How to reconnect the segments
190    * `list_len` - Function to get list length
191    * `sublist_remove` - Function to remove a range
192    * `sublist_insert` - Function to insert elements
193    * `variable_name` - Name of the list variable
194    * `descriptor_index` - Entity descriptor index
195
196    # Panics
197
198    Panics if cuts is empty or has more than 5 elements.
199    */
200    #[allow(clippy::too_many_arguments)]
201    pub fn new(
202        cuts: &[CutPoint],
203        reconnection: &KOptReconnection,
204        list_len: fn(&S, usize) -> usize,
205        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
206        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
207        variable_name: &'static str,
208        descriptor_index: usize,
209    ) -> Self {
210        assert!(!cuts.is_empty() && cuts.len() <= 5, "k must be 1-5");
211
212        let mut cut_array = [CutPoint::default(); 5];
213        for (i, cut) in cuts.iter().enumerate() {
214            cut_array[i] = *cut;
215        }
216
217        // For now, assume intra-route (all cuts on same entity)
218        let entity_index = cuts[0].entity_index;
219
220        Self {
221            cuts: cut_array,
222            cut_count: cuts.len() as u8,
223            reconnection: *reconnection,
224            list_len,
225            sublist_remove,
226            sublist_insert,
227            variable_name,
228            descriptor_index,
229            entity_index,
230            _phantom: PhantomData,
231        }
232    }
233
234    #[inline]
235    pub fn k(&self) -> usize {
236        self.cut_count as usize
237    }
238
239    #[inline]
240    pub fn cuts(&self) -> &[CutPoint] {
241        &self.cuts[..self.cut_count as usize]
242    }
243
244    pub fn is_intra_route(&self) -> bool {
245        let first = self.cuts[0].entity_index;
246        self.cuts[..self.cut_count as usize]
247            .iter()
248            .all(|c| c.entity_index == first)
249    }
250}
251
252impl<S, V> Move<S> for KOptMove<S, V>
253where
254    S: PlanningSolution,
255    V: Clone + Send + Sync + Debug + 'static,
256{
257    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
258        let solution = score_director.working_solution();
259        let k = self.cut_count as usize;
260
261        // Must have at least 2 cuts for meaningful k-opt
262        if k < 2 {
263            return false;
264        }
265
266        // Verify reconnection pattern matches k
267        if self.reconnection.k() != k {
268            return false;
269        }
270
271        // For intra-route, verify cuts are sorted and within bounds
272        let len = (self.list_len)(solution, self.entity_index);
273
274        // Check cuts are valid positions
275        for cut in &self.cuts[..k] {
276            if cut.position > len {
277                return false;
278            }
279        }
280
281        // For intra-route, cuts must be strictly increasing
282        if self.is_intra_route() {
283            for i in 1..k {
284                if self.cuts[i].position <= self.cuts[i - 1].position {
285                    return false;
286                }
287            }
288            // Need at least 1 element between cuts for meaningful segments
289            // Actually, empty segments are allowed in some cases
290        }
291
292        true
293    }
294
295    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
296        let k = self.cut_count as usize;
297        let entity = self.entity_index;
298
299        // Notify before change
300        score_director.before_variable_changed(self.descriptor_index, entity);
301
302        /* For intra-route k-opt, we need to:
303        1. Extract all segments
304        2. Reorder according to reconnection pattern
305        3. Reverse segments as needed
306        4. Rebuild the list
307        */
308
309        /* Calculate segment boundaries (segments are between consecutive cuts)
310        For k cuts at positions [p0, p1, ..., pk-1], we have k+1 segments:
311        Segment 0: [0, p0)
312        Segment 1: [p0, p1)
313        ...
314        Segment k: [pk-1, len)
315        */
316
317        let solution = score_director.working_solution_mut();
318        let len = (self.list_len)(solution, entity);
319
320        // Extract all elements
321        let all_elements = (self.sublist_remove)(solution, entity, 0, len);
322
323        // Build segment boundaries
324        let mut boundaries = Vec::with_capacity(k + 2);
325        boundaries.push(0);
326        for i in 0..k {
327            boundaries.push(self.cuts[i].position);
328        }
329        boundaries.push(len);
330
331        // Extract segments
332        let mut segments: Vec<Vec<V>> = Vec::with_capacity(k + 1);
333        for i in 0..=k {
334            let start = boundaries[i];
335            let end = boundaries[i + 1];
336            segments.push(all_elements[start..end].to_vec());
337        }
338
339        // Reorder and reverse according to reconnection pattern
340        let mut new_elements = Vec::with_capacity(len);
341        for pos in 0..self.reconnection.segment_count() {
342            let seg_idx = self.reconnection.segment_at(pos);
343            let mut seg = std::mem::take(&mut segments[seg_idx]);
344            if self.reconnection.should_reverse(seg_idx) {
345                seg.reverse();
346            }
347            new_elements.extend(seg);
348        }
349
350        // Insert reordered elements back
351        let new_len = new_elements.len();
352        (self.sublist_insert)(
353            score_director.working_solution_mut(),
354            entity,
355            0,
356            new_elements,
357        );
358
359        // Notify after change
360        score_director.after_variable_changed(self.descriptor_index, entity);
361
362        // Register undo - need to restore original order
363        let sublist_remove = self.sublist_remove;
364        let sublist_insert = self.sublist_insert;
365
366        score_director.register_undo(Box::new(move |s: &mut S| {
367            // Remove current elements
368            let _ = sublist_remove(s, entity, 0, new_len);
369            // Insert original elements
370            sublist_insert(s, entity, 0, all_elements);
371        }));
372    }
373
374    fn descriptor_index(&self) -> usize {
375        self.descriptor_index
376    }
377
378    fn entity_indices(&self) -> &[usize] {
379        std::slice::from_ref(&self.entity_index)
380    }
381
382    fn variable_name(&self) -> &str {
383        self.variable_name
384    }
385}
386
387#[cfg(test)]
388#[path = "k_opt_tests.rs"]
389mod tests;