solverforge_solver/heuristic/selector/
k_opt.rs

1//! K-opt move selector for tour optimization.
2//!
3//! Generates k-opt moves by enumerating all valid cut point combinations
4//! within selected entities and applying reconnection patterns.
5//!
6//! # Complexity
7//!
8//! For a route of length n and k-opt:
9//! - Full enumeration: O(n^k) cut combinations × reconnection patterns
10//! - Use `NearbyKOptMoveSelector` to reduce to O(n × m^(k-1)) with nearby selection
11//!
12//! # Example
13//!
14//! ```
15//! use solverforge_solver::heuristic::selector::k_opt::{KOptMoveSelector, KOptConfig};
16//! use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
17//! use solverforge_core::domain::PlanningSolution;
18//! use solverforge_core::score::SimpleScore;
19//!
20//! #[derive(Clone, Debug)]
21//! struct Tour { cities: Vec<i32>, score: Option<SimpleScore> }
22//!
23//! impl PlanningSolution for Tour {
24//!     type Score = SimpleScore;
25//!     fn score(&self) -> Option<Self::Score> { self.score }
26//!     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
27//! }
28//!
29//! fn list_len(s: &Tour, _: usize) -> usize { s.cities.len() }
30//! fn sublist_remove(s: &mut Tour, _: usize, start: usize, end: usize) -> Vec<i32> {
31//!     s.cities.drain(start..end).collect()
32//! }
33//! fn sublist_insert(s: &mut Tour, _: usize, pos: usize, items: Vec<i32>) {
34//!     for (i, item) in items.into_iter().enumerate() {
35//!         s.cities.insert(pos + i, item);
36//!     }
37//! }
38//!
39//! let config = KOptConfig::new(3); // 3-opt
40//! let entity_selector = Box::new(FromSolutionEntitySelector::new(0));
41//!
42//! let selector = KOptMoveSelector::<Tour, i32>::new(
43//!     entity_selector,
44//!     config,
45//!     list_len,
46//!     sublist_remove,
47//!     sublist_insert,
48//!     "cities",
49//!     0,
50//! );
51//! ```
52
53use std::fmt::Debug;
54use std::marker::PhantomData;
55
56use solverforge_core::domain::PlanningSolution;
57use solverforge_scoring::ScoreDirector;
58
59use crate::heuristic::r#move::k_opt_reconnection::{
60    enumerate_reconnections, KOptReconnection, THREE_OPT_RECONNECTIONS,
61};
62use crate::heuristic::r#move::{CutPoint, KOptMove};
63
64use super::entity::EntitySelector;
65use super::typed_move_selector::MoveSelector;
66
67/// Configuration for k-opt move generation.
68#[derive(Debug, Clone)]
69pub struct KOptConfig {
70    /// The k value (2-5).
71    pub k: usize,
72    /// Minimum segment length between cuts (default: 1).
73    pub min_segment_len: usize,
74    /// Whether to use only a subset of reconnection patterns.
75    pub limited_patterns: bool,
76}
77
78impl KOptConfig {
79    /// Creates a new k-opt configuration.
80    ///
81    /// # Panics
82    ///
83    /// Panics if k < 2 or k > 5.
84    pub fn new(k: usize) -> Self {
85        assert!((2..=5).contains(&k), "k must be between 2 and 5");
86        Self {
87            k,
88            min_segment_len: 1,
89            limited_patterns: false,
90        }
91    }
92
93    /// Sets minimum segment length between cuts.
94    pub fn with_min_segment_len(mut self, len: usize) -> Self {
95        self.min_segment_len = len;
96        self
97    }
98
99    /// Enables limited pattern mode (faster but less thorough).
100    pub fn with_limited_patterns(mut self, limited: bool) -> Self {
101        self.limited_patterns = limited;
102        self
103    }
104}
105
106/// A move selector that generates k-opt moves.
107///
108/// Enumerates all valid cut point combinations for each selected entity
109/// and generates moves for each reconnection pattern.
110pub struct KOptMoveSelector<S, V> {
111    /// Selects entities (routes) to apply k-opt to.
112    entity_selector: Box<dyn EntitySelector<S>>,
113    /// K-opt configuration.
114    config: KOptConfig,
115    /// Reconnection patterns to use.
116    patterns: Vec<&'static KOptReconnection>,
117    /// Get list length for an entity.
118    list_len: fn(&S, usize) -> usize,
119    /// Remove sublist [start, end).
120    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
121    /// Insert elements at position.
122    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
123    /// Variable name.
124    variable_name: &'static str,
125    /// Descriptor index.
126    descriptor_index: usize,
127    _phantom: PhantomData<V>,
128}
129
130impl<S, V: Debug> Debug for KOptMoveSelector<S, V> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("KOptMoveSelector")
133            .field("entity_selector", &self.entity_selector)
134            .field("config", &self.config)
135            .field("pattern_count", &self.patterns.len())
136            .field("variable_name", &self.variable_name)
137            .finish()
138    }
139}
140
141impl<S: PlanningSolution, V> KOptMoveSelector<S, V> {
142    /// Creates a new k-opt move selector.
143    #[allow(clippy::too_many_arguments)]
144    pub fn new(
145        entity_selector: Box<dyn EntitySelector<S>>,
146        config: KOptConfig,
147        list_len: fn(&S, usize) -> usize,
148        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
149        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
150        variable_name: &'static str,
151        descriptor_index: usize,
152    ) -> Self {
153        // Get static patterns for k=3, generate for others
154        let patterns: Vec<&'static KOptReconnection> = if config.k == 3 {
155            THREE_OPT_RECONNECTIONS.iter().collect()
156        } else {
157            // For other k values, we need to leak the patterns to get 'static lifetime
158            // This is a one-time allocation per selector creation
159            let generated = enumerate_reconnections(config.k);
160            let leaked: &'static [KOptReconnection] = Box::leak(generated.into_boxed_slice());
161            leaked.iter().collect()
162        };
163
164        Self {
165            entity_selector,
166            config,
167            patterns,
168            list_len,
169            sublist_remove,
170            sublist_insert,
171            variable_name,
172            descriptor_index,
173            _phantom: PhantomData,
174        }
175    }
176}
177
178impl<S, V> MoveSelector<S, KOptMove<S, V>> for KOptMoveSelector<S, V>
179where
180    S: PlanningSolution,
181    V: Clone + Send + Sync + Debug + 'static,
182{
183    fn iter_moves<'a>(
184        &'a self,
185        score_director: &'a dyn ScoreDirector<S>,
186    ) -> Box<dyn Iterator<Item = KOptMove<S, V>> + 'a> {
187        let k = self.config.k;
188        let min_seg = self.config.min_segment_len;
189        let patterns = &self.patterns;
190        let list_len = self.list_len;
191        let sublist_remove = self.sublist_remove;
192        let sublist_insert = self.sublist_insert;
193        let variable_name = self.variable_name;
194        let descriptor_index = self.descriptor_index;
195
196        let iter = self
197            .entity_selector
198            .iter(score_director)
199            .flat_map(move |entity_ref| {
200                let entity_idx = entity_ref.entity_index;
201                let solution = score_director.working_solution();
202                let len = list_len(solution, entity_idx);
203
204                // Generate all valid cut combinations
205                let cuts_iter = CutCombinationIterator::new(k, len, min_seg, entity_idx);
206
207                cuts_iter.flat_map(move |cuts| {
208                    // For each cut combination, generate moves for each pattern
209                    patterns.iter().map(move |&pattern| {
210                        KOptMove::new(
211                            &cuts,
212                            pattern,
213                            list_len,
214                            sublist_remove,
215                            sublist_insert,
216                            variable_name,
217                            descriptor_index,
218                        )
219                    })
220                })
221            });
222
223        Box::new(iter)
224    }
225
226    fn size(&self, score_director: &dyn ScoreDirector<S>) -> usize {
227        let k = self.config.k;
228        let min_seg = self.config.min_segment_len;
229        let pattern_count = self.patterns.len();
230
231        self.entity_selector
232            .iter(score_director)
233            .map(|entity_ref| {
234                let solution = score_director.working_solution();
235                let len = (self.list_len)(solution, entity_ref.entity_index);
236                count_cut_combinations(k, len, min_seg) * pattern_count
237            })
238            .sum()
239    }
240}
241
242/// Iterator over all valid k-cut combinations for a route of given length.
243struct CutCombinationIterator {
244    k: usize,
245    len: usize,
246    min_seg: usize,
247    entity_idx: usize,
248    /// Current cut positions.
249    positions: Vec<usize>,
250    /// Whether we've exhausted all combinations.
251    done: bool,
252}
253
254impl CutCombinationIterator {
255    fn new(k: usize, len: usize, min_seg: usize, entity_idx: usize) -> Self {
256        // Minimum length required: k cuts need k+1 segments of min_seg each
257        let min_len = (k + 1) * min_seg;
258
259        if len < min_len {
260            return Self {
261                k,
262                len,
263                min_seg,
264                entity_idx,
265                positions: vec![],
266                done: true,
267            };
268        }
269
270        // Initialize with first valid combination
271        // Cuts must be at positions that leave min_seg elements between them
272        let mut positions = Vec::with_capacity(k);
273        for i in 0..k {
274            positions.push(min_seg * (i + 1));
275        }
276
277        Self {
278            k,
279            len,
280            min_seg,
281            entity_idx,
282            positions,
283            done: false,
284        }
285    }
286
287    fn advance(&mut self) -> bool {
288        if self.done || self.positions.is_empty() {
289            return false;
290        }
291
292        // Find the rightmost position that can be incremented
293        let k = self.k;
294        let len = self.len;
295        let min_seg = self.min_seg;
296
297        for i in (0..k).rev() {
298            // Maximum position for cut i:
299            // Need to leave room for (k - i - 1) more cuts after this one,
300            // each separated by min_seg, plus min_seg at the end
301            let max_pos = len - min_seg * (k - i);
302
303            if self.positions[i] < max_pos {
304                self.positions[i] += 1;
305                // Reset all positions after i
306                for j in (i + 1)..k {
307                    self.positions[j] = self.positions[j - 1] + min_seg;
308                }
309                return true;
310            }
311        }
312
313        self.done = true;
314        false
315    }
316}
317
318impl Iterator for CutCombinationIterator {
319    type Item = Vec<CutPoint>;
320
321    fn next(&mut self) -> Option<Self::Item> {
322        if self.done {
323            return None;
324        }
325
326        let cuts: Vec<CutPoint> = self
327            .positions
328            .iter()
329            .map(|&pos| CutPoint::new(self.entity_idx, pos))
330            .collect();
331
332        self.advance();
333
334        Some(cuts)
335    }
336}
337
338/// Counts the number of valid k-cut combinations for a route of length len.
339fn count_cut_combinations(k: usize, len: usize, min_seg: usize) -> usize {
340    // This is equivalent to C(n - (k+1)*min_seg + k, k)
341    // where we're choosing k positions from the "free" slots
342    let min_len = (k + 1) * min_seg;
343    if len < min_len {
344        return 0;
345    }
346
347    let free_slots = len - min_len + k;
348    binomial(free_slots, k)
349}
350
351/// Compute binomial coefficient C(n, k).
352fn binomial(n: usize, k: usize) -> usize {
353    if k > n {
354        return 0;
355    }
356    if k == 0 || k == n {
357        return 1;
358    }
359
360    let k = k.min(n - k); // Use symmetry
361    let mut result = 1;
362    for i in 0..k {
363        result = result * (n - i) / (i + 1);
364    }
365    result
366}
367
368/// A distance meter for list element positions.
369///
370/// Measures distance between elements at two positions in a list.
371/// Used by `NearbyKOptMoveSelector` to limit k-opt search space.
372pub trait ListPositionDistanceMeter<S>: Send + Sync + Debug {
373    /// Measures distance between elements at two positions in the same entity.
374    fn distance(&self, solution: &S, entity_idx: usize, pos_a: usize, pos_b: usize) -> f64;
375}
376
377/// A k-opt move selector with nearby selection for improved performance.
378///
379/// Instead of enumerating all O(n^k) cut combinations, uses distance-based
380/// pruning to reduce to O(n * m^(k-1)) where m = max_nearby_size.
381///
382/// # How It Works
383///
384/// 1. First cut: all positions in the route
385/// 2. Second cut: only positions nearby (by element distance) to first cut
386/// 3. Third cut: only positions nearby to second cut
387/// 4. etc.
388///
389/// This dramatically reduces the search space for large routes.
390pub struct NearbyKOptMoveSelector<S, V, D: ListPositionDistanceMeter<S>> {
391    /// Selects entities (routes) to apply k-opt to.
392    entity_selector: Box<dyn EntitySelector<S>>,
393    /// Distance meter for nearby selection.
394    distance_meter: D,
395    /// Maximum nearby positions to consider.
396    max_nearby: usize,
397    /// K-opt configuration.
398    config: KOptConfig,
399    /// Reconnection patterns.
400    patterns: Vec<&'static KOptReconnection>,
401    /// Get list length for an entity.
402    list_len: fn(&S, usize) -> usize,
403    /// Remove sublist.
404    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
405    /// Insert sublist.
406    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
407    /// Variable name.
408    variable_name: &'static str,
409    /// Descriptor index.
410    descriptor_index: usize,
411    _phantom: PhantomData<V>,
412}
413
414impl<S, V: Debug, D: ListPositionDistanceMeter<S>> Debug for NearbyKOptMoveSelector<S, V, D> {
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        f.debug_struct("NearbyKOptMoveSelector")
417            .field("entity_selector", &self.entity_selector)
418            .field("max_nearby", &self.max_nearby)
419            .field("config", &self.config)
420            .field("pattern_count", &self.patterns.len())
421            .finish()
422    }
423}
424
425impl<S: PlanningSolution, V, D: ListPositionDistanceMeter<S>> NearbyKOptMoveSelector<S, V, D> {
426    /// Creates a new nearby k-opt move selector.
427    #[allow(clippy::too_many_arguments)]
428    pub fn new(
429        entity_selector: Box<dyn EntitySelector<S>>,
430        distance_meter: D,
431        max_nearby: usize,
432        config: KOptConfig,
433        list_len: fn(&S, usize) -> usize,
434        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
435        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
436        variable_name: &'static str,
437        descriptor_index: usize,
438    ) -> Self {
439        let patterns: Vec<&'static KOptReconnection> = if config.k == 3 {
440            THREE_OPT_RECONNECTIONS.iter().collect()
441        } else {
442            let generated = enumerate_reconnections(config.k);
443            let leaked: &'static [KOptReconnection] = Box::leak(generated.into_boxed_slice());
444            leaked.iter().collect()
445        };
446
447        Self {
448            entity_selector,
449            distance_meter,
450            max_nearby,
451            config,
452            patterns,
453            list_len,
454            sublist_remove,
455            sublist_insert,
456            variable_name,
457            descriptor_index,
458            _phantom: PhantomData,
459        }
460    }
461
462    /// Finds the m nearest positions to a given position.
463    fn nearby_positions(
464        &self,
465        solution: &S,
466        entity_idx: usize,
467        origin: usize,
468        len: usize,
469    ) -> Vec<usize> {
470        let mut positions: Vec<(usize, f64)> = (0..len)
471            .filter(|&p| p != origin)
472            .map(|p| {
473                let dist = self
474                    .distance_meter
475                    .distance(solution, entity_idx, origin, p);
476                (p, dist)
477            })
478            .collect();
479
480        positions.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
481        positions.truncate(self.max_nearby);
482        positions.into_iter().map(|(p, _)| p).collect()
483    }
484}
485
486impl<S, V, D> MoveSelector<S, KOptMove<S, V>> for NearbyKOptMoveSelector<S, V, D>
487where
488    S: PlanningSolution,
489    V: Clone + Send + Sync + Debug + 'static,
490    D: ListPositionDistanceMeter<S> + 'static,
491{
492    fn iter_moves<'a>(
493        &'a self,
494        score_director: &'a dyn ScoreDirector<S>,
495    ) -> Box<dyn Iterator<Item = KOptMove<S, V>> + 'a> {
496        let k = self.config.k;
497        let min_seg = self.config.min_segment_len;
498        let patterns = &self.patterns;
499        let list_len_fn = self.list_len;
500        let sublist_remove = self.sublist_remove;
501        let sublist_insert = self.sublist_insert;
502        let variable_name = self.variable_name;
503        let descriptor_index = self.descriptor_index;
504
505        let iter = self
506            .entity_selector
507            .iter(score_director)
508            .flat_map(move |entity_ref| {
509                let entity_idx = entity_ref.entity_index;
510                let solution = score_director.working_solution();
511                let len = list_len_fn(solution, entity_idx);
512
513                // Generate nearby cut combinations
514                let cuts_iter = NearbyCutIterator::new(self, solution, entity_idx, k, len, min_seg);
515
516                cuts_iter.flat_map(move |cuts| {
517                    patterns.iter().map(move |&pattern| {
518                        // Validate cuts are sorted for intra-route
519                        let mut sorted_cuts = cuts.clone();
520                        sorted_cuts.sort_by_key(|c| c.position());
521
522                        KOptMove::new(
523                            &sorted_cuts,
524                            pattern,
525                            list_len_fn,
526                            sublist_remove,
527                            sublist_insert,
528                            variable_name,
529                            descriptor_index,
530                        )
531                    })
532                })
533            });
534
535        Box::new(iter)
536    }
537
538    fn size(&self, score_director: &dyn ScoreDirector<S>) -> usize {
539        // Approximate size: n * m^(k-1) * patterns
540        let k = self.config.k;
541        let m = self.max_nearby;
542        let pattern_count = self.patterns.len();
543
544        self.entity_selector
545            .iter(score_director)
546            .map(|entity_ref| {
547                let solution = score_director.working_solution();
548                let len = (self.list_len)(solution, entity_ref.entity_index);
549                if len < (k + 1) * self.config.min_segment_len {
550                    0
551                } else {
552                    // Approximate: first cut has ~len choices, others have ~m choices
553                    len.saturating_sub(k) * m.pow((k - 1) as u32) * pattern_count
554                }
555            })
556            .sum()
557    }
558}
559
560/// Iterator for nearby k-cut combinations.
561struct NearbyCutIterator<'a, S, V, D: ListPositionDistanceMeter<S>> {
562    selector: &'a NearbyKOptMoveSelector<S, V, D>,
563    solution: &'a S,
564    entity_idx: usize,
565    k: usize,
566    len: usize,
567    min_seg: usize,
568    /// Stack of (position, nearby_iterator_index)
569    stack: Vec<(usize, usize)>,
570    /// Nearby positions for each level
571    nearby_cache: Vec<Vec<usize>>,
572    done: bool,
573}
574
575impl<'a, S: PlanningSolution, V, D: ListPositionDistanceMeter<S>> NearbyCutIterator<'a, S, V, D> {
576    fn new(
577        selector: &'a NearbyKOptMoveSelector<S, V, D>,
578        solution: &'a S,
579        entity_idx: usize,
580        k: usize,
581        len: usize,
582        min_seg: usize,
583    ) -> Self {
584        let min_len = (k + 1) * min_seg;
585        if len < min_len {
586            return Self {
587                selector,
588                solution,
589                entity_idx,
590                k,
591                len,
592                min_seg,
593                stack: vec![],
594                nearby_cache: vec![],
595                done: true,
596            };
597        }
598
599        // Start with first valid position
600        let mut iter = Self {
601            selector,
602            solution,
603            entity_idx,
604            k,
605            len,
606            min_seg,
607            stack: vec![(min_seg, 0)],
608            nearby_cache: vec![vec![]],
609            done: false,
610        };
611
612        // Build initial stack to depth k
613        iter.extend_stack();
614        iter
615    }
616
617    fn extend_stack(&mut self) {
618        while self.stack.len() < self.k && !self.done {
619            let (last_pos, _) = *self.stack.last().unwrap();
620
621            // Get nearby positions for next cut
622            let nearby =
623                self.selector
624                    .nearby_positions(self.solution, self.entity_idx, last_pos, self.len);
625
626            // Filter to valid positions (must leave room for remaining cuts)
627            let remaining_cuts = self.k - self.stack.len();
628            let min_pos = last_pos + self.min_seg;
629            let max_pos = self.len - self.min_seg * remaining_cuts;
630
631            let valid: Vec<usize> = nearby
632                .into_iter()
633                .filter(|&p| p >= min_pos && p <= max_pos)
634                .collect();
635
636            if valid.is_empty() {
637                // No valid positions, backtrack
638                if !self.backtrack() {
639                    self.done = true;
640                    return;
641                }
642            } else {
643                self.nearby_cache.push(valid);
644                let next_pos = self.nearby_cache.last().unwrap()[0];
645                self.stack.push((next_pos, 0));
646            }
647        }
648    }
649
650    fn backtrack(&mut self) -> bool {
651        while let Some((_, _idx)) = self.stack.pop() {
652            self.nearby_cache.pop();
653
654            if let Some((_, last_idx)) = self.stack.last_mut() {
655                let cache_idx = self.nearby_cache.len();
656                if cache_idx > 0 {
657                    let cache = &self.nearby_cache[cache_idx - 1];
658                    let next_idx = *last_idx + 1;
659                    if next_idx < cache.len() {
660                        *last_idx = next_idx;
661                        let (pos, _) = self.stack.last().unwrap();
662                        let new_pos = cache[next_idx];
663                        if new_pos > *pos {
664                            self.stack.pop();
665                            self.stack.push((new_pos, next_idx));
666                            return true;
667                        }
668                    }
669                }
670            } else {
671                // We're at the first level
672                let first_pos = self.stack.first().map(|(p, _)| *p).unwrap_or(0);
673                let next_first = first_pos + 1;
674                let max_first = self.len - self.min_seg * self.k;
675                if next_first <= max_first {
676                    self.stack.clear();
677                    self.nearby_cache.clear();
678                    self.stack.push((next_first, 0));
679                    self.nearby_cache.push(vec![]);
680                    return true;
681                }
682            }
683        }
684        false
685    }
686
687    fn advance(&mut self) {
688        if self.done || self.stack.is_empty() {
689            self.done = true;
690            return;
691        }
692
693        // Try to advance at current depth
694        if let Some((_, idx)) = self.stack.last_mut() {
695            let cache_idx = self.nearby_cache.len() - 1;
696            let cache = &self.nearby_cache[cache_idx];
697            let next_idx = *idx + 1;
698            if next_idx < cache.len() {
699                *idx = next_idx;
700                let new_pos = cache[next_idx];
701                self.stack.pop();
702                self.stack.push((new_pos, next_idx));
703                return;
704            }
705        }
706
707        // Backtrack and extend
708        if self.backtrack() {
709            self.extend_stack();
710        } else {
711            self.done = true;
712        }
713    }
714}
715
716impl<'a, S: PlanningSolution, V, D: ListPositionDistanceMeter<S>> Iterator
717    for NearbyCutIterator<'a, S, V, D>
718{
719    type Item = Vec<CutPoint>;
720
721    fn next(&mut self) -> Option<Self::Item> {
722        if self.done || self.stack.len() != self.k {
723            return None;
724        }
725
726        let cuts: Vec<CutPoint> = self
727            .stack
728            .iter()
729            .map(|(pos, _)| CutPoint::new(self.entity_idx, *pos))
730            .collect();
731
732        self.advance();
733
734        Some(cuts)
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use crate::heuristic::r#move::Move;
742    use crate::heuristic::selector::entity::FromSolutionEntitySelector;
743    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
744    use solverforge_core::score::SimpleScore;
745    use solverforge_scoring::SimpleScoreDirector;
746    use std::any::TypeId;
747
748    #[derive(Clone, Debug)]
749    struct Tour {
750        cities: Vec<i32>,
751    }
752
753    #[derive(Clone, Debug)]
754    struct TspSolution {
755        tours: Vec<Tour>,
756        score: Option<SimpleScore>,
757    }
758
759    impl PlanningSolution for TspSolution {
760        type Score = SimpleScore;
761        fn score(&self) -> Option<Self::Score> {
762            self.score
763        }
764        fn set_score(&mut self, score: Option<Self::Score>) {
765            self.score = score;
766        }
767    }
768
769    fn get_tours(s: &TspSolution) -> &Vec<Tour> {
770        &s.tours
771    }
772    fn get_tours_mut(s: &mut TspSolution) -> &mut Vec<Tour> {
773        &mut s.tours
774    }
775    fn list_len(s: &TspSolution, entity_idx: usize) -> usize {
776        s.tours.get(entity_idx).map_or(0, |t| t.cities.len())
777    }
778    fn sublist_remove(
779        s: &mut TspSolution,
780        entity_idx: usize,
781        start: usize,
782        end: usize,
783    ) -> Vec<i32> {
784        s.tours
785            .get_mut(entity_idx)
786            .map(|t| t.cities.drain(start..end).collect())
787            .unwrap_or_default()
788    }
789    fn sublist_insert(s: &mut TspSolution, entity_idx: usize, pos: usize, items: Vec<i32>) {
790        if let Some(t) = s.tours.get_mut(entity_idx) {
791            for (i, item) in items.into_iter().enumerate() {
792                t.cities.insert(pos + i, item);
793            }
794        }
795    }
796
797    fn create_director(
798        tours: Vec<Tour>,
799    ) -> SimpleScoreDirector<TspSolution, impl Fn(&TspSolution) -> SimpleScore> {
800        let solution = TspSolution { tours, score: None };
801        let extractor = Box::new(TypedEntityExtractor::new(
802            "Tour",
803            "tours",
804            get_tours,
805            get_tours_mut,
806        ));
807        let entity_desc =
808            EntityDescriptor::new("Tour", TypeId::of::<Tour>(), "tours").with_extractor(extractor);
809        let descriptor = SolutionDescriptor::new("TspSolution", TypeId::of::<TspSolution>())
810            .with_entity(entity_desc);
811        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
812    }
813
814    #[test]
815    fn cut_combination_iterator_basic() {
816        // For k=3, len=8, min_seg=1:
817        // We need 4 segments of length >= 1
818        // Cuts can be at positions 1-7 (not 0 or 8)
819        // First combination: [1, 2, 3]
820        let mut iter = CutCombinationIterator::new(3, 8, 1, 0);
821
822        let first = iter.next().unwrap();
823        assert_eq!(first.len(), 3);
824        assert_eq!(first[0].position(), 1);
825        assert_eq!(first[1].position(), 2);
826        assert_eq!(first[2].position(), 3);
827
828        // Count total combinations
829        let count = 1 + iter.count(); // +1 for first we already took
830                                      // C(8 - 4 + 3, 3) = C(7, 3) = 35
831        assert_eq!(count, 35);
832    }
833
834    #[test]
835    fn cut_combination_too_short() {
836        // Route too short for 3 cuts with min_seg=2
837        // Need 4 segments * 2 = 8 elements minimum
838        let mut iter = CutCombinationIterator::new(3, 6, 2, 0);
839        assert!(iter.next().is_none());
840    }
841
842    #[test]
843    fn binomial_coefficient() {
844        assert_eq!(binomial(5, 2), 10);
845        assert_eq!(binomial(7, 3), 35);
846        assert_eq!(binomial(10, 5), 252);
847    }
848
849    #[test]
850    fn selector_generates_moves() {
851        let tours = vec![Tour {
852            cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
853        }];
854        let director = create_director(tours);
855
856        let config = KOptConfig::new(3);
857        let entity_selector = Box::new(FromSolutionEntitySelector::new(0));
858
859        let selector = KOptMoveSelector::<TspSolution, i32>::new(
860            entity_selector,
861            config,
862            list_len,
863            sublist_remove,
864            sublist_insert,
865            "cities",
866            0,
867        );
868
869        let moves: Vec<_> = selector.iter_moves(&director).collect();
870
871        // 35 cut combinations × 7 patterns = 245 moves
872        assert_eq!(moves.len(), 245);
873
874        // All moves should be doable
875        for m in &moves {
876            assert!(m.is_doable(&director), "Move not doable: {:?}", m);
877        }
878    }
879
880    #[test]
881    fn selector_size_matches_iteration() {
882        let tours = vec![Tour {
883            cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
884        }];
885        let director = create_director(tours);
886
887        let config = KOptConfig::new(3);
888        let entity_selector = Box::new(FromSolutionEntitySelector::new(0));
889
890        let selector = KOptMoveSelector::<TspSolution, i32>::new(
891            entity_selector,
892            config,
893            list_len,
894            sublist_remove,
895            sublist_insert,
896            "cities",
897            0,
898        );
899
900        let size = selector.size(&director);
901        let actual_count = selector.iter_moves(&director).count();
902
903        assert_eq!(size, actual_count);
904    }
905}