Skip to main content

solverforge_solver/heuristic/move/
k_opt.rs

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