1use crate::error::TspResult;
6use crate::instance::TspInstance;
7use crate::solver::{Budget, TspSolution, TspSolver};
8use rand::prelude::*;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
13pub struct TabuSolver {
14 pub tenure: usize,
16 pub max_neighbors: usize,
18 pub use_aspiration: bool,
20 seed: Option<u64>,
22}
23
24impl Default for TabuSolver {
25 fn default() -> Self {
26 Self {
27 tenure: 20,
28 max_neighbors: 100,
29 use_aspiration: true,
30 seed: None,
31 }
32 }
33}
34
35impl TabuSolver {
36 pub fn new() -> Self {
38 Self::default()
39 }
40
41 pub fn with_tenure(mut self, tenure: usize) -> Self {
43 self.tenure = tenure;
44 self
45 }
46
47 pub fn with_max_neighbors(mut self, max_neighbors: usize) -> Self {
49 self.max_neighbors = max_neighbors;
50 self
51 }
52
53 pub fn with_aspiration(mut self, use_aspiration: bool) -> Self {
55 self.use_aspiration = use_aspiration;
56 self
57 }
58
59 pub fn with_seed(mut self, seed: u64) -> Self {
61 self.seed = Some(seed);
62 self
63 }
64
65 fn nearest_neighbor_tour(instance: &TspInstance, start: usize) -> Vec<usize> {
67 let n = instance.num_cities();
68 let mut tour = Vec::with_capacity(n);
69 let mut visited = vec![false; n];
70
71 tour.push(start);
72 visited[start] = true;
73
74 while tour.len() < n {
75 let current = tour[tour.len() - 1];
76 let mut best_next = 0;
77 let mut best_dist = f64::INFINITY;
78
79 for (j, &is_visited) in visited.iter().enumerate() {
80 if !is_visited && instance.distance(current, j) < best_dist {
81 best_dist = instance.distance(current, j);
82 best_next = j;
83 }
84 }
85
86 tour.push(best_next);
87 visited[best_next] = true;
88 }
89
90 tour
91 }
92
93 fn two_opt_delta(tour: &[usize], instance: &TspInstance, i: usize, j: usize) -> f64 {
95 let n = tour.len();
96
97 let i_next = (i + 1) % n;
99 let j_next = (j + 1) % n;
100
101 let d_removed =
102 instance.distance(tour[i], tour[i_next]) + instance.distance(tour[j], tour[j_next]);
103
104 let d_added =
106 instance.distance(tour[i], tour[j]) + instance.distance(tour[i_next], tour[j_next]);
107
108 d_added - d_removed
109 }
110
111 fn apply_two_opt(tour: &mut [usize], i: usize, j: usize) {
113 let i_next = i + 1;
114 tour[i_next..=j].reverse();
115 }
116
117 #[allow(clippy::too_many_arguments)]
119 fn find_best_move(
120 &self,
121 tour: &[usize],
122 instance: &TspInstance,
123 tabu_list: &HashMap<(usize, usize), usize>,
124 iteration: usize,
125 best_known: f64,
126 current_length: f64,
127 rng: &mut StdRng,
128 ) -> Option<(usize, usize, f64)> {
129 let n = tour.len();
130 let mut best_move: Option<(usize, usize, f64)> = None;
131 let mut best_delta = f64::INFINITY;
132
133 let mut candidates: Vec<(usize, usize)> = Vec::new();
135 for i in 0..n - 2 {
136 for j in i + 2..n {
137 if i == 0 && j == n - 1 {
138 continue; }
140 candidates.push((i, j));
141 }
142 }
143
144 candidates.shuffle(rng);
146 candidates.truncate(self.max_neighbors);
147
148 for (i, j) in candidates {
149 let delta = Self::two_opt_delta(tour, instance, i, j);
150 let new_length = current_length + delta;
151
152 let edge1 = (tour[i].min(tour[j]), tour[i].max(tour[j]));
154 let edge2 = (
155 tour[(i + 1) % n].min(tour[(j + 1) % n]),
156 tour[(i + 1) % n].max(tour[(j + 1) % n]),
157 );
158
159 let is_tabu = tabu_list.get(&edge1).is_some_and(|&exp| exp > iteration)
160 || tabu_list.get(&edge2).is_some_and(|&exp| exp > iteration);
161
162 let accept = !is_tabu || (self.use_aspiration && new_length < best_known);
164
165 if accept && delta < best_delta {
166 best_delta = delta;
167 best_move = Some((i, j, new_length));
168 }
169 }
170
171 best_move
172 }
173
174 pub fn refine(
176 &mut self,
177 tour: Vec<usize>,
178 instance: &TspInstance,
179 iterations: usize,
180 ) -> TspResult<TspSolution> {
181 let mut current_tour = tour;
182 let mut current_length = instance.tour_length(¤t_tour);
183 let mut best_tour = current_tour.clone();
184 let mut best_length = current_length;
185
186 let mut rng = match self.seed {
187 Some(s) => StdRng::seed_from_u64(s),
188 None => StdRng::from_entropy(),
189 };
190
191 let mut tabu_list: HashMap<(usize, usize), usize> = HashMap::new();
192 let mut history = Vec::with_capacity(iterations);
193 let mut evaluations = 0;
194
195 for iteration in 0..iterations {
196 if let Some((i, j, new_length)) = self.find_best_move(
197 ¤t_tour,
198 instance,
199 &tabu_list,
200 iteration,
201 best_length,
202 current_length,
203 &mut rng,
204 ) {
205 let n = current_tour.len();
207 let edge1 = (
208 current_tour[i].min(current_tour[(i + 1) % n]),
209 current_tour[i].max(current_tour[(i + 1) % n]),
210 );
211 let edge2 = (
212 current_tour[j].min(current_tour[(j + 1) % n]),
213 current_tour[j].max(current_tour[(j + 1) % n]),
214 );
215
216 Self::apply_two_opt(&mut current_tour, i, j);
218 current_length = new_length;
219 evaluations += 1;
220
221 tabu_list.insert(edge1, iteration + self.tenure);
223 tabu_list.insert(edge2, iteration + self.tenure);
224
225 if current_length < best_length {
227 best_length = current_length;
228 best_tour.clone_from(¤t_tour);
229 }
230 }
231
232 history.push(best_length);
233 }
234
235 Ok(TspSolution {
236 tour: best_tour,
237 length: best_length,
238 evaluations,
239 history,
240 })
241 }
242}
243
244impl TspSolver for TabuSolver {
245 fn solve(&mut self, instance: &TspInstance, budget: Budget) -> TspResult<TspSolution> {
246 let max_iterations = budget.limit();
247
248 let mut rng = match self.seed {
249 Some(s) => StdRng::seed_from_u64(s),
250 None => StdRng::from_entropy(),
251 };
252
253 let start_city = rng.gen_range(0..instance.num_cities());
255 let initial_tour = Self::nearest_neighbor_tour(instance, start_city);
256
257 self.refine(initial_tour, instance, max_iterations)
258 }
259
260 fn name(&self) -> &'static str {
261 "Tabu Search"
262 }
263
264 fn reset(&mut self) {
265 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn square_instance() -> TspInstance {
274 let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
275 TspInstance::from_coords("square", coords).expect("should create")
276 }
277
278 fn triangle_instance() -> TspInstance {
279 let coords = vec![(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)];
280 TspInstance::from_coords("triangle", coords).expect("should create")
281 }
282
283 fn line_instance() -> TspInstance {
284 let coords = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)];
286 TspInstance::from_coords("line", coords).expect("should create")
287 }
288
289 #[test]
290 fn test_tabu_default_params() {
291 let tabu = TabuSolver::default();
292 assert_eq!(tabu.tenure, 20);
293 assert_eq!(tabu.max_neighbors, 100);
294 assert!(tabu.use_aspiration);
295 }
296
297 #[test]
298 fn test_tabu_builder() {
299 let tabu = TabuSolver::new()
300 .with_tenure(30)
301 .with_max_neighbors(50)
302 .with_aspiration(false)
303 .with_seed(42);
304
305 assert_eq!(tabu.tenure, 30);
306 assert_eq!(tabu.max_neighbors, 50);
307 assert!(!tabu.use_aspiration);
308 assert_eq!(tabu.seed, Some(42));
309 }
310
311 #[test]
312 fn test_tabu_solves_square() {
313 let instance = square_instance();
314 let mut solver = TabuSolver::new().with_seed(42);
315
316 let solution = solver
317 .solve(&instance, Budget::Iterations(50))
318 .expect("should solve");
319
320 assert!(solution.length <= 5.0, "Length {} > 5.0", solution.length);
322 assert_eq!(solution.tour.len(), 4);
323 assert!(instance.validate_tour(&solution.tour).is_ok());
324 }
325
326 #[test]
327 fn test_tabu_solves_triangle() {
328 let instance = triangle_instance();
329 let mut solver = TabuSolver::new().with_seed(42);
330
331 let solution = solver
332 .solve(&instance, Budget::Iterations(50))
333 .expect("should solve");
334
335 assert!(solution.length <= 13.0, "Length {} > 13.0", solution.length);
337 assert_eq!(solution.tour.len(), 3);
338 }
339
340 #[test]
341 fn test_tabu_solves_line() {
342 let instance = line_instance();
343 let mut solver = TabuSolver::new().with_seed(42).with_tenure(10);
344
345 let solution = solver
346 .solve(&instance, Budget::Iterations(100))
347 .expect("should solve");
348
349 assert!(solution.length <= 9.0, "Length {} > 9.0", solution.length);
351 }
352
353 #[test]
354 fn test_tabu_deterministic_with_seed() {
355 let instance = square_instance();
356
357 let mut solver1 = TabuSolver::new().with_seed(42);
358 let mut solver2 = TabuSolver::new().with_seed(42);
359
360 let solution1 = solver1
361 .solve(&instance, Budget::Iterations(20))
362 .expect("should solve");
363 let solution2 = solver2
364 .solve(&instance, Budget::Iterations(20))
365 .expect("should solve");
366
367 assert!((solution1.length - solution2.length).abs() < 1e-10);
368 }
369
370 #[test]
371 fn test_tabu_tracks_history() {
372 let instance = square_instance();
373 let mut solver = TabuSolver::new().with_seed(42);
374
375 let solution = solver
376 .solve(&instance, Budget::Iterations(20))
377 .expect("should solve");
378
379 assert_eq!(solution.history.len(), 20);
380 }
381
382 #[test]
383 fn test_tabu_refine() {
384 let instance = square_instance();
385 let mut solver = TabuSolver::new().with_seed(42);
386
387 let initial_tour = vec![0, 2, 1, 3]; let initial_length = instance.tour_length(&initial_tour);
390
391 let solution = solver
392 .refine(initial_tour, &instance, 50)
393 .expect("should refine");
394
395 assert!(solution.length <= initial_length);
397 }
398
399 #[test]
400 fn test_nearest_neighbor_heuristic() {
401 let instance = line_instance();
402 let tour = TabuSolver::nearest_neighbor_tour(&instance, 0);
403
404 assert_eq!(tour.len(), 5);
405 assert!(instance.validate_tour(&tour).is_ok());
406 assert_eq!(tour[0], 0);
408 assert_eq!(tour[1], 1);
409 }
410
411 #[test]
412 fn test_two_opt_delta() {
413 let instance = square_instance();
414 let tour = vec![0, 1, 2, 3];
415 let original_length = instance.tour_length(&tour);
416
417 let delta = TabuSolver::two_opt_delta(&tour, &instance, 0, 2);
419
420 let mut new_tour = tour.clone();
422 TabuSolver::apply_two_opt(&mut new_tour, 0, 2);
423 let new_length = instance.tour_length(&new_tour);
424
425 assert!((new_length - original_length - delta).abs() < 1e-10);
426 }
427
428 #[test]
429 fn test_tabu_name() {
430 let solver = TabuSolver::new();
431 assert_eq!(solver.name(), "Tabu Search");
432 }
433
434 #[test]
435 fn test_tabu_aspiration_criterion() {
436 let instance = square_instance();
437
438 let mut solver_no_asp = TabuSolver::new().with_aspiration(false).with_seed(42);
440
441 let mut solver_asp = TabuSolver::new().with_aspiration(true).with_seed(42);
443
444 let sol1 = solver_no_asp
445 .solve(&instance, Budget::Iterations(50))
446 .expect("should solve");
447 let sol2 = solver_asp
448 .solve(&instance, Budget::Iterations(50))
449 .expect("should solve");
450
451 assert!(instance.validate_tour(&sol1.tour).is_ok());
453 assert!(instance.validate_tour(&sol2.tour).is_ok());
454 }
455}