1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub struct CutPoint {
85 entity_index: usize,
87 position: usize,
90}
91
92impl CutPoint {
93 #[inline]
95 pub const fn new(entity_index: usize, position: usize) -> Self {
96 Self {
97 entity_index,
98 position,
99 }
100 }
101
102 #[inline]
104 pub const fn entity_index(&self) -> usize {
105 self.entity_index
106 }
107
108 #[inline]
110 pub const fn position(&self) -> usize {
111 self.position
112 }
113}
114
115#[derive(Clone)]
131pub struct KOptMove<S, V> {
132 cuts: [CutPoint; 5],
134 cut_count: u8,
136 reconnection: &'static KOptReconnection,
138 list_len: fn(&S, usize) -> usize,
140 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
142 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
144 variable_name: &'static str,
146 descriptor_index: usize,
148 entity_index: usize,
150 _phantom: PhantomData<V>,
151}
152
153impl<S, V: Debug> Debug for KOptMove<S, V> {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 let cuts: Vec<_> = self.cuts[..self.cut_count as usize]
156 .iter()
157 .map(|c| c.position)
158 .collect();
159 f.debug_struct("KOptMove")
160 .field("k", &self.cut_count)
161 .field("entity", &self.entity_index)
162 .field("cuts", &cuts)
163 .field("reconnection", &self.reconnection)
164 .field("variable_name", &self.variable_name)
165 .finish()
166 }
167}
168
169impl<S, V> KOptMove<S, V> {
170 #[allow(clippy::too_many_arguments)]
186 pub fn new(
187 cuts: &[CutPoint],
188 reconnection: &'static KOptReconnection,
189 list_len: fn(&S, usize) -> usize,
190 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
191 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
192 variable_name: &'static str,
193 descriptor_index: usize,
194 ) -> Self {
195 assert!(!cuts.is_empty() && cuts.len() <= 5, "k must be 1-5");
196
197 let mut cut_array = [CutPoint::default(); 5];
198 for (i, cut) in cuts.iter().enumerate() {
199 cut_array[i] = *cut;
200 }
201
202 let entity_index = cuts[0].entity_index;
204
205 Self {
206 cuts: cut_array,
207 cut_count: cuts.len() as u8,
208 reconnection,
209 list_len,
210 sublist_remove,
211 sublist_insert,
212 variable_name,
213 descriptor_index,
214 entity_index,
215 _phantom: PhantomData,
216 }
217 }
218
219 #[inline]
221 pub fn k(&self) -> usize {
222 self.cut_count as usize
223 }
224
225 #[inline]
227 pub fn cuts(&self) -> &[CutPoint] {
228 &self.cuts[..self.cut_count as usize]
229 }
230
231 pub fn is_intra_route(&self) -> bool {
233 let first = self.cuts[0].entity_index;
234 self.cuts[..self.cut_count as usize]
235 .iter()
236 .all(|c| c.entity_index == first)
237 }
238}
239
240impl<S, V> Move<S> for KOptMove<S, V>
241where
242 S: PlanningSolution,
243 V: Clone + Send + Sync + Debug + 'static,
244{
245 fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
246 let solution = score_director.working_solution();
247 let k = self.cut_count as usize;
248
249 if k < 2 {
251 return false;
252 }
253
254 if self.reconnection.k() != k {
256 return false;
257 }
258
259 let len = (self.list_len)(solution, self.entity_index);
261
262 for cut in &self.cuts[..k] {
264 if cut.position > len {
265 return false;
266 }
267 }
268
269 if self.is_intra_route() {
271 for i in 1..k {
272 if self.cuts[i].position <= self.cuts[i - 1].position {
273 return false;
274 }
275 }
276 }
279
280 true
281 }
282
283 fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
284 let k = self.cut_count as usize;
285 let entity = self.entity_index;
286
287 score_director.before_variable_changed(self.descriptor_index, entity, self.variable_name);
289
290 let solution = score_director.working_solution_mut();
304 let len = (self.list_len)(solution, entity);
305
306 let all_elements = (self.sublist_remove)(solution, entity, 0, len);
308
309 let mut boundaries = Vec::with_capacity(k + 2);
311 boundaries.push(0);
312 for i in 0..k {
313 boundaries.push(self.cuts[i].position);
314 }
315 boundaries.push(len);
316
317 let mut segments: Vec<Vec<V>> = Vec::with_capacity(k + 1);
319 for i in 0..=k {
320 let start = boundaries[i];
321 let end = boundaries[i + 1];
322 segments.push(all_elements[start..end].to_vec());
323 }
324
325 let mut new_elements = Vec::with_capacity(len);
327 for pos in 0..self.reconnection.segment_count() {
328 let seg_idx = self.reconnection.segment_at(pos);
329 let mut seg = segments[seg_idx].clone();
330 if self.reconnection.should_reverse(seg_idx) {
331 seg.reverse();
332 }
333 new_elements.extend(seg);
334 }
335
336 (self.sublist_insert)(
338 score_director.working_solution_mut(),
339 entity,
340 0,
341 new_elements.clone(),
342 );
343
344 score_director.after_variable_changed(self.descriptor_index, entity, self.variable_name);
346
347 let sublist_remove = self.sublist_remove;
349 let sublist_insert = self.sublist_insert;
350
351 score_director.register_undo(Box::new(move |s: &mut S| {
352 let current_len = new_elements.len();
354 let _ = sublist_remove(s, entity, 0, current_len);
355 sublist_insert(s, entity, 0, all_elements);
357 }));
358 }
359
360 fn descriptor_index(&self) -> usize {
361 self.descriptor_index
362 }
363
364 fn entity_indices(&self) -> &[usize] {
365 std::slice::from_ref(&self.entity_index)
366 }
367
368 fn variable_name(&self) -> &str {
369 self.variable_name
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::heuristic::r#move::k_opt_reconnection::THREE_OPT_RECONNECTIONS;
377 use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
378 use solverforge_core::score::SimpleScore;
379 use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
380 use std::any::TypeId;
381
382 #[derive(Clone, Debug)]
383 struct Tour {
384 cities: Vec<i32>,
385 }
386
387 #[derive(Clone, Debug)]
388 struct TspSolution {
389 tours: Vec<Tour>,
390 score: Option<SimpleScore>,
391 }
392
393 impl PlanningSolution for TspSolution {
394 type Score = SimpleScore;
395 fn score(&self) -> Option<Self::Score> {
396 self.score
397 }
398 fn set_score(&mut self, score: Option<Self::Score>) {
399 self.score = score;
400 }
401 }
402
403 fn get_tours(s: &TspSolution) -> &Vec<Tour> {
404 &s.tours
405 }
406 fn get_tours_mut(s: &mut TspSolution) -> &mut Vec<Tour> {
407 &mut s.tours
408 }
409
410 fn list_len(s: &TspSolution, entity_idx: usize) -> usize {
411 s.tours.get(entity_idx).map_or(0, |t| t.cities.len())
412 }
413 fn sublist_remove(
414 s: &mut TspSolution,
415 entity_idx: usize,
416 start: usize,
417 end: usize,
418 ) -> Vec<i32> {
419 s.tours
420 .get_mut(entity_idx)
421 .map(|t| t.cities.drain(start..end).collect())
422 .unwrap_or_default()
423 }
424 fn sublist_insert(s: &mut TspSolution, entity_idx: usize, pos: usize, items: Vec<i32>) {
425 if let Some(t) = s.tours.get_mut(entity_idx) {
426 for (i, item) in items.into_iter().enumerate() {
427 t.cities.insert(pos + i, item);
428 }
429 }
430 }
431
432 fn create_director(
433 tours: Vec<Tour>,
434 ) -> SimpleScoreDirector<TspSolution, impl Fn(&TspSolution) -> SimpleScore> {
435 let solution = TspSolution { tours, score: None };
436 let extractor = Box::new(TypedEntityExtractor::new(
437 "Tour",
438 "tours",
439 get_tours,
440 get_tours_mut,
441 ));
442 let entity_desc =
443 EntityDescriptor::new("Tour", TypeId::of::<Tour>(), "tours").with_extractor(extractor);
444 let descriptor = SolutionDescriptor::new("TspSolution", TypeId::of::<TspSolution>())
445 .with_entity(entity_desc);
446 SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
447 }
448
449 #[test]
450 fn three_opt_swap_segments() {
451 let tours = vec![Tour {
461 cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
462 }];
463 let mut director = create_director(tours);
464
465 let cuts = [
466 CutPoint::new(0, 2),
467 CutPoint::new(0, 4),
468 CutPoint::new(0, 6),
469 ];
470 let reconnection = &THREE_OPT_RECONNECTIONS[3]; let m = KOptMove::<TspSolution, i32>::new(
473 &cuts,
474 reconnection,
475 list_len,
476 sublist_remove,
477 sublist_insert,
478 "cities",
479 0,
480 );
481
482 assert!(m.is_doable(&director));
483 assert_eq!(m.k(), 3);
484
485 {
486 let mut recording = RecordingScoreDirector::new(&mut director);
487 m.do_move(&mut recording);
488
489 let cities = &recording.working_solution().tours[0].cities;
490 assert_eq!(cities, &[1, 2, 5, 6, 3, 4, 7, 8]);
491
492 recording.undo_changes();
493 }
494
495 let cities = &director.working_solution().tours[0].cities;
496 assert_eq!(cities, &[1, 2, 3, 4, 5, 6, 7, 8]);
497 }
498
499 #[test]
500 fn three_opt_reverse_segment() {
501 let tours = vec![Tour {
512 cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
513 }];
514 let mut director = create_director(tours);
515
516 let cuts = [
517 CutPoint::new(0, 2),
518 CutPoint::new(0, 4),
519 CutPoint::new(0, 6),
520 ];
521 let reconnection = &THREE_OPT_RECONNECTIONS[0]; let m = KOptMove::<TspSolution, i32>::new(
524 &cuts,
525 reconnection,
526 list_len,
527 sublist_remove,
528 sublist_insert,
529 "cities",
530 0,
531 );
532
533 assert!(m.is_doable(&director));
534
535 {
536 let mut recording = RecordingScoreDirector::new(&mut director);
537 m.do_move(&mut recording);
538
539 let cities = &recording.working_solution().tours[0].cities;
540 assert_eq!(cities, &[1, 2, 4, 3, 5, 6, 7, 8]);
541
542 recording.undo_changes();
543 }
544
545 let cities = &director.working_solution().tours[0].cities;
546 assert_eq!(cities, &[1, 2, 3, 4, 5, 6, 7, 8]);
547 }
548
549 #[test]
550 fn invalid_cuts_not_doable() {
551 let tours = vec![Tour {
552 cities: vec![1, 2, 3],
553 }];
554 let director = create_director(tours);
555
556 let cuts = [
558 CutPoint::new(0, 2),
559 CutPoint::new(0, 4),
560 CutPoint::new(0, 10), ];
562 let reconnection = &THREE_OPT_RECONNECTIONS[0];
563
564 let m = KOptMove::<TspSolution, i32>::new(
565 &cuts,
566 reconnection,
567 list_len,
568 sublist_remove,
569 sublist_insert,
570 "cities",
571 0,
572 );
573
574 assert!(!m.is_doable(&director));
575 }
576
577 #[test]
578 fn cuts_not_sorted_not_doable() {
579 let tours = vec![Tour {
580 cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
581 }];
582 let director = create_director(tours);
583
584 let cuts = [
586 CutPoint::new(0, 4),
587 CutPoint::new(0, 2), CutPoint::new(0, 6),
589 ];
590 let reconnection = &THREE_OPT_RECONNECTIONS[0];
591
592 let m = KOptMove::<TspSolution, i32>::new(
593 &cuts,
594 reconnection,
595 list_len,
596 sublist_remove,
597 sublist_insert,
598 "cities",
599 0,
600 );
601
602 assert!(!m.is_doable(&director));
603 }
604}