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- Concrete 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 smallvec::{smallvec, SmallVec};
65use solverforge_core::domain::PlanningSolution;
66use solverforge_scoring::Director;
67
68use super::k_opt_reconnection::KOptReconnection;
69use super::metadata::{
70    encode_option_debug, encode_usize, hash_str, MoveTabuScope, ScopedValueTabuToken,
71};
72use super::{Move, MoveTabuSignature};
73
74/* A cut point in a route, defining where an edge is removed.
75
76For k-opt, we have k cut points which divide the route into k+1 segments.
77
78# Example
79
80```
81use solverforge_solver::heuristic::r#move::CutPoint;
82
83// Cut at position 5 in entity 0
84let cut = CutPoint::new(0, 5);
85assert_eq!(cut.entity_index(), 0);
86assert_eq!(cut.position(), 5);
87```
88*/
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90pub struct CutPoint {
91    // Entity (route/vehicle) index.
92    entity_index: usize,
93    // Position in the route where the cut occurs.
94    // The edge between position-1 and position is removed.
95    position: usize,
96}
97
98impl CutPoint {
99    #[inline]
100    pub const fn new(entity_index: usize, position: usize) -> Self {
101        Self {
102            entity_index,
103            position,
104        }
105    }
106
107    #[inline]
108    pub const fn entity_index(&self) -> usize {
109        self.entity_index
110    }
111
112    #[inline]
113    pub const fn position(&self) -> usize {
114        self.position
115    }
116}
117
118/// A k-opt move that removes k edges and reconnects segments.
119///
120/// This is the generalized k-opt move supporting k=2,3,4,5.
121/// For k=2, this is equivalent to a 2-opt (segment reversal).
122///
123/// # Zero-Erasure Design
124///
125/// - Fixed array `[CutPoint; 5]` for up to 5 cuts (5-opt)
126/// - `KOptReconnection` stored by value (`Copy` type, no heap allocation)
127/// - Concrete function pointers for list operations
128///
129/// # Type Parameters
130///
131/// * `S` - The planning solution type
132/// * `V` - The list element value type
133pub struct KOptMove<S, V> {
134    // Cut points (up to 5 for 5-opt).
135    cuts: [CutPoint; 5],
136    // Number of actual cuts (k value).
137    cut_count: u8,
138    // Reconnection pattern to apply (stored by value — `KOptReconnection` is `Copy`).
139    reconnection: KOptReconnection,
140    list_len: fn(&S, usize) -> usize,
141    list_get: fn(&S, usize, usize) -> Option<V>,
142    // Remove sublist [start, end), returns removed elements.
143    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
144    // Insert elements at position.
145    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
146    // Variable name.
147    variable_name: &'static str,
148    // Descriptor index.
149    descriptor_index: usize,
150    // Entity index (for intra-route moves).
151    entity_index: usize,
152    _phantom: PhantomData<fn() -> V>,
153}
154
155impl<S, V> Clone for KOptMove<S, V> {
156    fn clone(&self) -> Self {
157        Self {
158            cuts: self.cuts,
159            cut_count: self.cut_count,
160            reconnection: self.reconnection,
161            list_len: self.list_len,
162            list_get: self.list_get,
163            sublist_remove: self.sublist_remove,
164            sublist_insert: self.sublist_insert,
165            variable_name: self.variable_name,
166            descriptor_index: self.descriptor_index,
167            entity_index: self.entity_index,
168            _phantom: PhantomData,
169        }
170    }
171}
172
173impl<S, V: Debug> Debug for KOptMove<S, V> {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        let cuts: Vec<_> = self.cuts[..self.cut_count as usize]
176            .iter()
177            .map(|c| c.position)
178            .collect();
179        f.debug_struct("KOptMove")
180            .field("k", &self.cut_count)
181            .field("entity", &self.entity_index)
182            .field("cuts", &cuts)
183            .field("reconnection", &self.reconnection)
184            .field("variable_name", &self.variable_name)
185            .finish()
186    }
187}
188
189impl<S, V> KOptMove<S, V> {
190    /* Creates a new k-opt move.
191
192    # Arguments
193
194    * `cuts` - Slice of cut points (must be sorted by position for intra-route)
195    * `reconnection` - How to reconnect the segments
196    * `list_len` - Function to get list length
197    * `sublist_remove` - Function to remove a range
198    * `sublist_insert` - Function to insert elements
199    * `variable_name` - Name of the list variable
200    * `descriptor_index` - Entity descriptor index
201
202    # Panics
203
204    Panics if cuts is empty or has more than 5 elements.
205    */
206    #[allow(clippy::too_many_arguments)]
207    pub fn new(
208        cuts: &[CutPoint],
209        reconnection: &KOptReconnection,
210        list_len: fn(&S, usize) -> usize,
211        list_get: fn(&S, usize, usize) -> Option<V>,
212        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
213        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
214        variable_name: &'static str,
215        descriptor_index: usize,
216    ) -> Self {
217        assert!(!cuts.is_empty() && cuts.len() <= 5, "k must be 1-5");
218
219        let mut cut_array = [CutPoint::default(); 5];
220        for (i, cut) in cuts.iter().enumerate() {
221            cut_array[i] = *cut;
222        }
223
224        // For now, assume intra-route (all cuts on same entity)
225        let entity_index = cuts[0].entity_index;
226
227        Self {
228            cuts: cut_array,
229            cut_count: cuts.len() as u8,
230            reconnection: *reconnection,
231            list_len,
232            list_get,
233            sublist_remove,
234            sublist_insert,
235            variable_name,
236            descriptor_index,
237            entity_index,
238            _phantom: PhantomData,
239        }
240    }
241
242    #[inline]
243    pub fn k(&self) -> usize {
244        self.cut_count as usize
245    }
246
247    #[inline]
248    pub fn cuts(&self) -> &[CutPoint] {
249        &self.cuts[..self.cut_count as usize]
250    }
251
252    pub fn is_intra_route(&self) -> bool {
253        let first = self.cuts[0].entity_index;
254        self.cuts[..self.cut_count as usize]
255            .iter()
256            .all(|c| c.entity_index == first)
257    }
258}
259
260impl<S, V> Move<S> for KOptMove<S, V>
261where
262    S: PlanningSolution,
263    V: Clone + Send + Sync + Debug + 'static,
264{
265    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
266        let solution = score_director.working_solution();
267        let k = self.cut_count as usize;
268
269        // Must have at least 2 cuts for meaningful k-opt
270        if k < 2 {
271            return false;
272        }
273
274        // Verify reconnection pattern matches k
275        if self.reconnection.k() != k {
276            return false;
277        }
278
279        // For intra-route, verify cuts are sorted and within bounds
280        let len = (self.list_len)(solution, self.entity_index);
281
282        // Check cuts are valid positions
283        for cut in &self.cuts[..k] {
284            if cut.position > len {
285                return false;
286            }
287        }
288
289        // For intra-route, cuts must be strictly increasing
290        if self.is_intra_route() {
291            for i in 1..k {
292                if self.cuts[i].position <= self.cuts[i - 1].position {
293                    return false;
294                }
295            }
296            // Need at least 1 element between cuts for meaningful segments
297            // Actually, empty segments are allowed in some cases
298        }
299
300        true
301    }
302
303    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
304        let k = self.cut_count as usize;
305        let entity = self.entity_index;
306
307        // Notify before change
308        score_director.before_variable_changed(self.descriptor_index, entity);
309
310        /* For intra-route k-opt, we need to:
311        1. Extract all segments
312        2. Reorder according to reconnection pattern
313        3. Reverse segments as needed
314        4. Rebuild the list
315        */
316
317        /* Calculate segment boundaries (segments are between consecutive cuts)
318        For k cuts at positions [p0, p1, ..., pk-1], we have k+1 segments:
319        Segment 0: [0, p0)
320        Segment 1: [p0, p1)
321        ...
322        Segment k: [pk-1, len)
323        */
324
325        let solution = score_director.working_solution_mut();
326        let len = (self.list_len)(solution, entity);
327
328        // Extract all elements
329        let all_elements = (self.sublist_remove)(solution, entity, 0, len);
330
331        // Build segment boundaries
332        let mut boundaries = Vec::with_capacity(k + 2);
333        boundaries.push(0);
334        for i in 0..k {
335            boundaries.push(self.cuts[i].position);
336        }
337        boundaries.push(len);
338
339        // Extract segments
340        let mut segments: Vec<Vec<V>> = Vec::with_capacity(k + 1);
341        for i in 0..=k {
342            let start = boundaries[i];
343            let end = boundaries[i + 1];
344            segments.push(all_elements[start..end].to_vec());
345        }
346
347        // Reorder and reverse according to reconnection pattern
348        let mut new_elements = Vec::with_capacity(len);
349        for pos in 0..self.reconnection.segment_count() {
350            let seg_idx = self.reconnection.segment_at(pos);
351            let mut seg = std::mem::take(&mut segments[seg_idx]);
352            if self.reconnection.should_reverse(seg_idx) {
353                seg.reverse();
354            }
355            new_elements.extend(seg);
356        }
357
358        // Insert reordered elements back
359        let new_len = new_elements.len();
360        (self.sublist_insert)(
361            score_director.working_solution_mut(),
362            entity,
363            0,
364            new_elements,
365        );
366
367        // Notify after change
368        score_director.after_variable_changed(self.descriptor_index, entity);
369
370        // Register undo - need to restore original order
371        let sublist_remove = self.sublist_remove;
372        let sublist_insert = self.sublist_insert;
373
374        score_director.register_undo(Box::new(move |s: &mut S| {
375            // Remove current elements
376            let _ = sublist_remove(s, entity, 0, new_len);
377            // Insert original elements
378            sublist_insert(s, entity, 0, all_elements);
379        }));
380    }
381
382    fn descriptor_index(&self) -> usize {
383        self.descriptor_index
384    }
385
386    fn entity_indices(&self) -> &[usize] {
387        std::slice::from_ref(&self.entity_index)
388    }
389
390    fn variable_name(&self) -> &str {
391        self.variable_name
392    }
393
394    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
395        let mut touched_value_ids: SmallVec<[u64; 2]> = SmallVec::new();
396        let len = (self.list_len)(score_director.working_solution(), self.entity_index);
397        let k = self.cut_count as usize;
398        let first_pos = self.cuts[0].position;
399        let last_pos = self.cuts[k - 1].position;
400        for pos in first_pos..len.min(last_pos.max(first_pos)) {
401            let value = (self.list_get)(score_director.working_solution(), self.entity_index, pos);
402            touched_value_ids.push(encode_option_debug(value.as_ref()));
403        }
404
405        let entity_id = encode_usize(self.entity_index);
406        let variable_id = hash_str(self.variable_name);
407        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
408        let destination_value_tokens: SmallVec<[ScopedValueTabuToken; 2]> = touched_value_ids
409            .iter()
410            .copied()
411            .map(|value_id| scope.value_token(value_id))
412            .collect();
413        let mut move_id = smallvec![
414            encode_usize(self.descriptor_index),
415            variable_id,
416            entity_id,
417            encode_usize(k)
418        ];
419        for cut in &self.cuts[..k] {
420            move_id.push(encode_usize(cut.position));
421        }
422        for segment in self.reconnection.segment_order() {
423            move_id.push(u64::from(*segment));
424        }
425        for idx in 0..self.reconnection.segment_count() {
426            move_id.push(if self.reconnection.should_reverse(idx) {
427                1
428            } else {
429                0
430            });
431        }
432        move_id.extend(touched_value_ids.iter().copied());
433
434        let mut inverse_order = vec![0u8; self.reconnection.segment_count()];
435        for (pos, segment) in self
436            .reconnection
437            .segment_order()
438            .iter()
439            .copied()
440            .enumerate()
441        {
442            inverse_order[segment as usize] = pos as u8;
443        }
444        let mut undo_move_id = smallvec![
445            encode_usize(self.descriptor_index),
446            variable_id,
447            entity_id,
448            encode_usize(k)
449        ];
450        for cut in &self.cuts[..k] {
451            undo_move_id.push(encode_usize(cut.position));
452        }
453        for segment in inverse_order {
454            undo_move_id.push(u64::from(segment));
455        }
456        for idx in 0..self.reconnection.segment_count() {
457            undo_move_id.push(if self.reconnection.should_reverse(idx) {
458                1
459            } else {
460                0
461            });
462        }
463        undo_move_id.extend(touched_value_ids.iter().copied());
464
465        MoveTabuSignature::new(scope, move_id, undo_move_id)
466            .with_entity_tokens([scope.entity_token(entity_id)])
467            .with_destination_value_tokens(destination_value_tokens)
468    }
469}