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;