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    type Undo = Vec<V>;
266
267    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
268        let solution = score_director.working_solution();
269        let k = self.cut_count as usize;
270
271        // Must have at least 2 cuts for meaningful k-opt
272        if k < 2 {
273            return false;
274        }
275
276        // Verify reconnection pattern matches k
277        if self.reconnection.k() != k {
278            return false;
279        }
280
281        // For intra-route, verify cuts are sorted and within bounds
282        let len = (self.list_len)(solution, self.entity_index);
283
284        // Check cuts are valid positions
285        for cut in &self.cuts[..k] {
286            if cut.position > len {
287                return false;
288            }
289        }
290
291        // For intra-route, cuts must be strictly increasing
292        if self.is_intra_route() {
293            for i in 1..k {
294                if self.cuts[i].position <= self.cuts[i - 1].position {
295                    return false;
296                }
297            }
298            // Need at least 1 element between cuts for meaningful segments
299            // Actually, empty segments are allowed in some cases
300        }
301
302        true
303    }
304
305    fn do_move<D: Director<S>>(&self, score_director: &mut D) -> Self::Undo {
306        let k = self.cut_count as usize;
307        let entity = self.entity_index;
308
309        // Notify before change
310        score_director.before_variable_changed(self.descriptor_index, entity);
311
312        /* For intra-route k-opt, we need to:
313        1. Extract all segments
314        2. Reorder according to reconnection pattern
315        3. Reverse segments as needed
316        4. Rebuild the list
317        */
318
319        /* Calculate segment boundaries (segments are between consecutive cuts)
320        For k cuts at positions [p0, p1, ..., pk-1], we have k+1 segments:
321        Segment 0: [0, p0)
322        Segment 1: [p0, p1)
323        ...
324        Segment k: [pk-1, len)
325        */
326
327        let solution = score_director.working_solution_mut();
328        let len = (self.list_len)(solution, entity);
329
330        // Extract all elements
331        let all_elements = (self.sublist_remove)(solution, entity, 0, len);
332
333        // Build segment boundaries
334        let mut boundaries = Vec::with_capacity(k + 2);
335        boundaries.push(0);
336        for i in 0..k {
337            boundaries.push(self.cuts[i].position);
338        }
339        boundaries.push(len);
340
341        // Extract segments
342        let mut segments: Vec<Vec<V>> = Vec::with_capacity(k + 1);
343        for i in 0..=k {
344            let start = boundaries[i];
345            let end = boundaries[i + 1];
346            segments.push(all_elements[start..end].to_vec());
347        }
348
349        // Reorder and reverse according to reconnection pattern
350        let mut new_elements = Vec::with_capacity(len);
351        for pos in 0..self.reconnection.segment_count() {
352            let seg_idx = self.reconnection.segment_at(pos);
353            let mut seg = std::mem::take(&mut segments[seg_idx]);
354            if self.reconnection.should_reverse(seg_idx) {
355                seg.reverse();
356            }
357            new_elements.extend(seg);
358        }
359
360        // Insert reordered elements back
361        (self.sublist_insert)(
362            score_director.working_solution_mut(),
363            entity,
364            0,
365            new_elements,
366        );
367
368        // Notify after change
369        score_director.after_variable_changed(self.descriptor_index, entity);
370
371        all_elements
372    }
373
374    fn undo_move<D: Director<S>>(&self, score_director: &mut D, undo: Self::Undo) {
375        score_director.before_variable_changed(self.descriptor_index, self.entity_index);
376        let len = (self.list_len)(score_director.working_solution(), self.entity_index);
377        let _ = (self.sublist_remove)(
378            score_director.working_solution_mut(),
379            self.entity_index,
380            0,
381            len,
382        );
383        (self.sublist_insert)(
384            score_director.working_solution_mut(),
385            self.entity_index,
386            0,
387            undo,
388        );
389        score_director.after_variable_changed(self.descriptor_index, self.entity_index);
390    }
391
392    fn descriptor_index(&self) -> usize {
393        self.descriptor_index
394    }
395
396    fn entity_indices(&self) -> &[usize] {
397        std::slice::from_ref(&self.entity_index)
398    }
399
400    fn variable_name(&self) -> &str {
401        self.variable_name
402    }
403
404    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
405        let mut touched_value_ids: SmallVec<[u64; 2]> = SmallVec::new();
406        let len = (self.list_len)(score_director.working_solution(), self.entity_index);
407        let k = self.cut_count as usize;
408        let first_pos = self.cuts[0].position;
409        let last_pos = self.cuts[k - 1].position;
410        for pos in first_pos..len.min(last_pos.max(first_pos)) {
411            let value = (self.list_get)(score_director.working_solution(), self.entity_index, pos);
412            touched_value_ids.push(encode_option_debug(value.as_ref()));
413        }
414
415        let entity_id = encode_usize(self.entity_index);
416        let variable_id = hash_str(self.variable_name);
417        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
418        let destination_value_tokens: SmallVec<[ScopedValueTabuToken; 2]> = touched_value_ids
419            .iter()
420            .copied()
421            .map(|value_id| scope.value_token(value_id))
422            .collect();
423        let mut move_id = smallvec![
424            encode_usize(self.descriptor_index),
425            variable_id,
426            entity_id,
427            encode_usize(k)
428        ];
429        for cut in &self.cuts[..k] {
430            move_id.push(encode_usize(cut.position));
431        }
432        for segment in self.reconnection.segment_order() {
433            move_id.push(u64::from(*segment));
434        }
435        for idx in 0..self.reconnection.segment_count() {
436            move_id.push(if self.reconnection.should_reverse(idx) {
437                1
438            } else {
439                0
440            });
441        }
442        move_id.extend(touched_value_ids.iter().copied());
443
444        let mut inverse_order = vec![0u8; self.reconnection.segment_count()];
445        for (pos, segment) in self
446            .reconnection
447            .segment_order()
448            .iter()
449            .copied()
450            .enumerate()
451        {
452            inverse_order[segment as usize] = pos as u8;
453        }
454        let mut undo_move_id = smallvec![
455            encode_usize(self.descriptor_index),
456            variable_id,
457            entity_id,
458            encode_usize(k)
459        ];
460        for cut in &self.cuts[..k] {
461            undo_move_id.push(encode_usize(cut.position));
462        }
463        for segment in inverse_order {
464            undo_move_id.push(u64::from(segment));
465        }
466        for idx in 0..self.reconnection.segment_count() {
467            undo_move_id.push(if self.reconnection.should_reverse(idx) {
468                1
469            } else {
470                0
471            });
472        }
473        undo_move_id.extend(touched_value_ids.iter().copied());
474
475        MoveTabuSignature::new(scope, move_id, undo_move_id)
476            .with_entity_tokens([scope.entity_token(entity_id)])
477            .with_destination_value_tokens(destination_value_tokens)
478    }
479}