use super::TwoOptMove;
use crate::instance::SolomonInstance;
use crate::solution::{Route, Solution};
pub fn best_two_opt_move(route: &Route, instance: &SolomonInstance) -> Option<TwoOptMove> {
let mut best: Option<TwoOptMove> = None;
for i in 0..route.len() {
for j in i + 1..route.len() {
let delta = two_opt_delta(route, instance, i, j);
if delta.is_finite() && delta < 0.0 && best.is_none_or(|current| delta < current.delta)
{
best = Some(TwoOptMove { i, j, delta });
}
}
}
best
}
pub fn two_opt_delta(route: &Route, instance: &SolomonInstance, i: usize, j: usize) -> f32 {
debug_assert!(i < j, "i must be strictly less than j");
debug_assert!(j < route.nodes.len(), "j must be within route bounds");
let nodes = &route.nodes;
let n = nodes.len();
let prev_i = if i == 0 { 0 } else { nodes[i - 1] };
let next_j = if j == n - 1 { 0 } else { nodes[j + 1] };
let removed = instance.distance(prev_i, nodes[i]) + instance.distance(nodes[j], next_j);
let added = instance.distance(prev_i, nodes[j]) + instance.distance(nodes[i], next_j);
added - removed
}
pub fn apply_two_opt(route: &mut Route, i: usize, j: usize) {
debug_assert!(i < j, "i must be strictly less than j");
debug_assert!(j < route.nodes.len(), "j must be within route bounds");
route.nodes[i..=j].reverse();
}
pub fn two_opt_route(route: &mut Route, instance: &SolomonInstance) -> f32 {
let mut total_improvement = 0.0f32;
loop {
let n = route.nodes.len();
if n < 2 {
break;
}
let mut best_delta = 0.0f32;
let mut best_i = 0;
let mut best_j = 0;
for i in 0..n - 1 {
for j in i + 1..n {
let delta = two_opt_delta(route, instance, i, j);
if delta < best_delta {
best_delta = delta;
best_i = i;
best_j = j;
}
}
}
if best_delta < 0.0 {
apply_two_opt(route, best_i, best_j);
total_improvement -= best_delta;
} else {
break;
}
}
total_improvement
}
pub fn two_opt(solution: &mut Solution, instance: &SolomonInstance) -> f32 {
solution
.routes
.iter_mut()
.map(|route| two_opt_route(route, instance))
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instance::{VehicleConfig, compute_distance_matrix};
const EPSILON: f32 = 1e-5;
#[test]
fn test_best_move_handles_small_and_optimal_routes() {
let instance = create_test_instance();
for nodes in [vec![], vec![1], vec![1, 2], vec![1, 3, 2]] {
assert_eq!(
best_two_opt_move(&Route::from_nodes(nodes), &instance),
None
);
}
}
#[test]
fn test_best_move_breaks_ties_in_row_major_order_without_mutation() {
let mut instance = create_test_instance();
let n = instance.num_nodes;
for i in 0..n {
for j in 0..n {
instance.distance_matrix[i * n + j] = if i == j { 0.0 } else { 2.0 };
}
}
for (i, j) in [(0, 2), (2, 0), (1, 3), (3, 1)] {
instance.distance_matrix[i * n + j] = 1.0;
}
let route = Route::from_nodes(vec![1, 2, 3]);
let original = route.clone();
assert_eq!(
two_opt_delta(&route, &instance, 0, 1),
two_opt_delta(&route, &instance, 1, 2)
);
let best = best_two_opt_move(&route, &instance).unwrap();
assert_eq!((best.i, best.j), (0, 1));
assert_eq!(route, original);
let mut improved = route.clone();
apply_two_opt(&mut improved, best.i, best.j);
assert_approx_eq(
improved.distance(&instance) - route.distance(&instance),
best.delta,
);
}
#[test]
fn test_best_move_ignores_nonfinite_deltas() {
let mut instance = create_test_instance();
let route = Route::from_nodes(vec![1, 2, 3]);
instance.distance_matrix.fill(f32::INFINITY);
assert_eq!(best_two_opt_move(&route, &instance), None);
instance.distance_matrix.fill(f32::NAN);
assert_eq!(best_two_opt_move(&route, &instance), None);
}
#[test]
fn test_best_move_matches_full_cost_enumeration_for_all_permutations() {
let mut instance = create_test_instance();
let n = instance.num_nodes;
for i in 0..n {
for j in 0..n {
instance.distance_matrix[i * n + j] = i.abs_diff(j) as f32;
}
}
for a in 1..n {
for b in 1..n {
for c in 1..n {
for d in 1..n {
let mut unique = vec![a, b, c, d];
unique.sort_unstable();
unique.dedup();
if unique.len() != 4 {
continue;
}
let route = Route::from_nodes(vec![a, b, c, d]);
let before = route.distance(&instance);
let mut moves = Vec::new();
for i in 0..route.len() {
for j in i + 1..route.len() {
let mut reversed = route.clone();
reversed.nodes[i..=j].reverse();
let delta = reversed.distance(&instance) - before;
if delta < 0.0 {
moves.push(TwoOptMove { i, j, delta });
}
}
}
moves.sort_by(|a, b| {
a.delta
.partial_cmp(&b.delta)
.unwrap()
.then(a.i.cmp(&b.i))
.then(a.j.cmp(&b.j))
});
assert_eq!(best_two_opt_move(&route, &instance), moves.first().copied());
}
}
}
}
}
fn create_test_instance() -> SolomonInstance {
let xs = vec![0.0, 0.0, 1.0, 1.0, 2.0];
let ys = vec![0.0, 1.0, 0.0, 1.0, 0.0];
let demands = vec![0.0, 1.0, 1.0, 1.0, 1.0];
let distance_matrix = compute_distance_matrix(&xs, &ys);
SolomonInstance {
name: "TestTwoOpt".into(),
vehicle: VehicleConfig {
num_vehicles: 2,
capacity: 4.0,
},
num_nodes: xs.len(),
xs,
ys,
demands,
ready_times: vec![0.0; 5],
due_times: vec![1000.0; 5],
service_times: vec![0.0; 5],
distance_matrix,
}
}
fn assert_approx_eq(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < EPSILON,
"expected {expected}, got {actual}"
);
}
#[test]
fn test_two_opt_delta_matches_recomputed_route_distance() {
let instance = create_test_instance();
let route = Route::from_nodes(vec![1, 2, 3, 4]);
let distance_before = route.distance(&instance);
for i in 0..route.len() - 1 {
for j in i + 1..route.len() {
let expected_delta = two_opt_delta(&route, &instance, i, j);
let mut swapped = route.clone();
apply_two_opt(&mut swapped, i, j);
let actual_delta = swapped.distance(&instance) - distance_before;
assert_approx_eq(actual_delta, expected_delta);
}
}
}
#[test]
fn test_two_opt_delta_for_known_improving_move() {
let instance = create_test_instance();
let route = Route::from_nodes(vec![1, 2, 3]);
let delta = two_opt_delta(&route, &instance, 1, 2);
let expected = 2.0 - 2.0 * 2.0f32.sqrt();
assert!(delta < 0.0);
assert_approx_eq(delta, expected);
}
#[test]
fn test_apply_two_opt_reverses_only_selected_segment() {
let mut route = Route::from_nodes(vec![1, 2, 3, 4]);
apply_two_opt(&mut route, 1, 3);
assert_eq!(route.nodes, vec![1, 4, 3, 2]);
}
#[test]
fn test_two_opt_route_reaches_local_optimum() {
let instance = create_test_instance();
let mut route = Route::from_nodes(vec![1, 2, 3]);
let distance_before = route.distance(&instance);
let improvement = two_opt_route(&mut route, &instance);
let distance_after = route.distance(&instance);
assert_eq!(route.nodes, vec![1, 3, 2]);
assert!(distance_after < distance_before);
assert_approx_eq(improvement, distance_before - distance_after);
for i in 0..route.len() - 1 {
for j in i + 1..route.len() {
assert!(two_opt_delta(&route, &instance, i, j) >= 0.0);
}
}
assert_eq!(two_opt_route(&mut route, &instance), 0.0);
}
#[test]
fn test_two_opt_route_leaves_small_routes_unchanged() {
let instance = create_test_instance();
let routes = [
Route::new(),
Route::from_nodes(vec![1]),
Route::from_nodes(vec![1, 2]),
];
for original in routes {
let mut route = original.clone();
let improvement = two_opt_route(&mut route, &instance);
assert_eq!(route, original);
assert_eq!(improvement, 0.0);
}
}
#[test]
fn test_two_opt_improves_solution_without_changing_feasibility() {
let instance = create_test_instance();
let mut solution = Solution::new(vec![
Route::from_nodes(vec![1, 2, 3]),
Route::from_nodes(vec![4]),
]);
let distance_before = solution.total_distance(&instance);
let improvement = two_opt(&mut solution, &instance);
let distance_after = solution.total_distance(&instance);
assert!(solution.is_feasible(&instance));
assert_eq!(solution.routes[0].nodes, vec![1, 3, 2]);
assert_eq!(solution.routes[1].nodes, vec![4]);
assert!(distance_after < distance_before);
assert_approx_eq(improvement, distance_before - distance_after);
}
}