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
115pub struct KOptMove<S, V> {
131 cuts: [CutPoint; 5],
133 cut_count: u8,
135 reconnection: &'static KOptReconnection,
137 list_len: fn(&S, usize) -> usize,
139 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
141 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
143 variable_name: &'static str,
145 descriptor_index: usize,
147 entity_index: usize,
149 _phantom: PhantomData<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 #[allow(clippy::too_many_arguments)]
202 pub fn new(
203 cuts: &[CutPoint],
204 reconnection: &'static 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 let entity_index = cuts[0].entity_index;
220
221 Self {
222 cuts: cut_array,
223 cut_count: cuts.len() as u8,
224 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 #[inline]
237 pub fn k(&self) -> usize {
238 self.cut_count as usize
239 }
240
241 #[inline]
243 pub fn cuts(&self) -> &[CutPoint] {
244 &self.cuts[..self.cut_count as usize]
245 }
246
247 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 if k < 2 {
267 return false;
268 }
269
270 if self.reconnection.k() != k {
272 return false;
273 }
274
275 let len = (self.list_len)(solution, self.entity_index);
277
278 for cut in &self.cuts[..k] {
280 if cut.position > len {
281 return false;
282 }
283 }
284
285 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 }
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 score_director.before_variable_changed(self.descriptor_index, entity, self.variable_name);
305
306 let solution = score_director.working_solution_mut();
320 let len = (self.list_len)(solution, entity);
321
322 let all_elements = (self.sublist_remove)(solution, entity, 0, len);
324
325 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 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 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 = segments[seg_idx].clone();
346 if self.reconnection.should_reverse(seg_idx) {
347 seg.reverse();
348 }
349 new_elements.extend(seg);
350 }
351
352 (self.sublist_insert)(
354 score_director.working_solution_mut(),
355 entity,
356 0,
357 new_elements.clone(),
358 );
359
360 score_director.after_variable_changed(self.descriptor_index, entity, self.variable_name);
362
363 let sublist_remove = self.sublist_remove;
365 let sublist_insert = self.sublist_insert;
366
367 score_director.register_undo(Box::new(move |s: &mut S| {
368 let current_len = new_elements.len();
370 let _ = sublist_remove(s, entity, 0, current_len);
371 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)]
390mod tests {
391 use super::*;
392 use crate::heuristic::r#move::k_opt_reconnection::THREE_OPT_RECONNECTIONS;
393 use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
394 use solverforge_core::score::SimpleScore;
395 use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
396 use std::any::TypeId;
397
398 #[derive(Clone, Debug)]
399 struct Tour {
400 cities: Vec<i32>,
401 }
402
403 #[derive(Clone, Debug)]
404 struct TspSolution {
405 tours: Vec<Tour>,
406 score: Option<SimpleScore>,
407 }
408
409 impl PlanningSolution for TspSolution {
410 type Score = SimpleScore;
411 fn score(&self) -> Option<Self::Score> {
412 self.score
413 }
414 fn set_score(&mut self, score: Option<Self::Score>) {
415 self.score = score;
416 }
417 }
418
419 fn get_tours(s: &TspSolution) -> &Vec<Tour> {
420 &s.tours
421 }
422 fn get_tours_mut(s: &mut TspSolution) -> &mut Vec<Tour> {
423 &mut s.tours
424 }
425
426 fn list_len(s: &TspSolution, entity_idx: usize) -> usize {
427 s.tours.get(entity_idx).map_or(0, |t| t.cities.len())
428 }
429 fn sublist_remove(
430 s: &mut TspSolution,
431 entity_idx: usize,
432 start: usize,
433 end: usize,
434 ) -> Vec<i32> {
435 s.tours
436 .get_mut(entity_idx)
437 .map(|t| t.cities.drain(start..end).collect())
438 .unwrap_or_default()
439 }
440 fn sublist_insert(s: &mut TspSolution, entity_idx: usize, pos: usize, items: Vec<i32>) {
441 if let Some(t) = s.tours.get_mut(entity_idx) {
442 for (i, item) in items.into_iter().enumerate() {
443 t.cities.insert(pos + i, item);
444 }
445 }
446 }
447
448 fn create_director(
449 tours: Vec<Tour>,
450 ) -> SimpleScoreDirector<TspSolution, impl Fn(&TspSolution) -> SimpleScore> {
451 let solution = TspSolution { tours, score: None };
452 let extractor = Box::new(TypedEntityExtractor::new(
453 "Tour",
454 "tours",
455 get_tours,
456 get_tours_mut,
457 ));
458 let entity_desc =
459 EntityDescriptor::new("Tour", TypeId::of::<Tour>(), "tours").with_extractor(extractor);
460 let descriptor = SolutionDescriptor::new("TspSolution", TypeId::of::<TspSolution>())
461 .with_entity(entity_desc);
462 SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
463 }
464
465 #[test]
466 fn three_opt_swap_segments() {
467 let tours = vec![Tour {
477 cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
478 }];
479 let mut director = create_director(tours);
480
481 let cuts = [
482 CutPoint::new(0, 2),
483 CutPoint::new(0, 4),
484 CutPoint::new(0, 6),
485 ];
486 let reconnection = &THREE_OPT_RECONNECTIONS[3]; let m = KOptMove::<TspSolution, i32>::new(
489 &cuts,
490 reconnection,
491 list_len,
492 sublist_remove,
493 sublist_insert,
494 "cities",
495 0,
496 );
497
498 assert!(m.is_doable(&director));
499 assert_eq!(m.k(), 3);
500
501 {
502 let mut recording = RecordingScoreDirector::new(&mut director);
503 m.do_move(&mut recording);
504
505 let cities = &recording.working_solution().tours[0].cities;
506 assert_eq!(cities, &[1, 2, 5, 6, 3, 4, 7, 8]);
507
508 recording.undo_changes();
509 }
510
511 let cities = &director.working_solution().tours[0].cities;
512 assert_eq!(cities, &[1, 2, 3, 4, 5, 6, 7, 8]);
513 }
514
515 #[test]
516 fn three_opt_reverse_segment() {
517 let tours = vec![Tour {
528 cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
529 }];
530 let mut director = create_director(tours);
531
532 let cuts = [
533 CutPoint::new(0, 2),
534 CutPoint::new(0, 4),
535 CutPoint::new(0, 6),
536 ];
537 let reconnection = &THREE_OPT_RECONNECTIONS[0]; let m = KOptMove::<TspSolution, i32>::new(
540 &cuts,
541 reconnection,
542 list_len,
543 sublist_remove,
544 sublist_insert,
545 "cities",
546 0,
547 );
548
549 assert!(m.is_doable(&director));
550
551 {
552 let mut recording = RecordingScoreDirector::new(&mut director);
553 m.do_move(&mut recording);
554
555 let cities = &recording.working_solution().tours[0].cities;
556 assert_eq!(cities, &[1, 2, 4, 3, 5, 6, 7, 8]);
557
558 recording.undo_changes();
559 }
560
561 let cities = &director.working_solution().tours[0].cities;
562 assert_eq!(cities, &[1, 2, 3, 4, 5, 6, 7, 8]);
563 }
564
565 #[test]
566 fn invalid_cuts_not_doable() {
567 let tours = vec![Tour {
568 cities: vec![1, 2, 3],
569 }];
570 let director = create_director(tours);
571
572 let cuts = [
574 CutPoint::new(0, 2),
575 CutPoint::new(0, 4),
576 CutPoint::new(0, 10), ];
578 let reconnection = &THREE_OPT_RECONNECTIONS[0];
579
580 let m = KOptMove::<TspSolution, i32>::new(
581 &cuts,
582 reconnection,
583 list_len,
584 sublist_remove,
585 sublist_insert,
586 "cities",
587 0,
588 );
589
590 assert!(!m.is_doable(&director));
591 }
592
593 #[test]
594 fn cuts_not_sorted_not_doable() {
595 let tours = vec![Tour {
596 cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
597 }];
598 let director = create_director(tours);
599
600 let cuts = [
602 CutPoint::new(0, 4),
603 CutPoint::new(0, 2), CutPoint::new(0, 6),
605 ];
606 let reconnection = &THREE_OPT_RECONNECTIONS[0];
607
608 let m = KOptMove::<TspSolution, i32>::new(
609 &cuts,
610 reconnection,
611 list_len,
612 sublist_remove,
613 sublist_insert,
614 "cities",
615 0,
616 );
617
618 assert!(!m.is_doable(&director));
619 }
620}