pdp_lns 0.1.1

Adaptive Large Neighbourhood Search solver for the Pickup and Delivery Problem with Time Windows (PDPTW)
Documentation
use crate::instance::Instance;
use crate::solution::{
    RouteInfo, Solution, best_pair_insertion_with_info, build_route_with_pair,
    build_route_with_pair_buf, compute_all_infos, route_distance,
};

/// Inter-route 2-pair relocate with threshold acceptance and split-destination support.
///
/// Removes 2 pairs from one route and inserts them into other routes.
/// Supports two modes:
///   - **Same-dst**: both pairs into one destination route (original behavior)
///   - **Split-dst**: each pair into a different destination route
///
/// `max_worsening` controls acceptance:
///   - 0.0  → only strictly improving moves (original behavior)
///   - >0.0 → accept moves that worsen distance by up to this amount
///
/// Returns true if a move was applied.
#[allow(clippy::too_many_lines)]
pub fn shake_inter_route_2pair(inst: &Instance, sol: &mut Solution, max_worsening: f64) -> bool {
    let nr = sol.routes.len();
    if nr < 2 {
        return false;
    }

    let mut infos: Vec<RouteInfo> = Vec::new();
    compute_all_infos(inst, &sol.routes, &mut infos);

    // Best move tracking — accepts up to max_worsening increase
    let mut best_delta = max_worsening;

    // Move type: 0=none, 1=same-dst, 2=split-dst
    let mut move_type: u8 = 0;
    let mut mv_src: usize = 0;
    let mut mv_p1: usize = 0;
    let mut mv_p2: usize = 0;
    // Same-dst fields
    let mut mv_same_dst: usize = 0;
    let mut mv_same_first_is_p1: bool = true;
    let mut mv_same_ep1: usize = 0;
    let mut mv_same_ed1: usize = 0;
    let mut mv_same_ep2: usize = 0;
    let mut mv_same_ed2: usize = 0;
    // Split-dst fields
    let mut mv_split_dst1: usize = 0;
    let mut mv_split_ep1: usize = 0;
    let mut mv_split_ed1: usize = 0;
    let mut mv_split_dst2: usize = 0;
    let mut mv_split_ep2: usize = 0;
    let mut mv_split_ed2: usize = 0;

    // Work buffers
    let mut stripped_buf: Vec<usize> = Vec::new();
    let mut intermediate_buf: Vec<usize> = Vec::new();
    let mut info_intermediate = RouteInfo::new();
    let mut pos_in_route: Vec<usize> = vec![usize::MAX; inst.n + 1];
    let mut pos_touched: Vec<usize> = Vec::new();

    for ri in 0..nr {
        let route = &sol.routes[ri];
        pos_touched.clear();
        for (idx, &v) in route.iter().enumerate() {
            pos_in_route[v] = idx;
            pos_touched.push(v);
        }

        // Collect pickups in this route with their detour costs
        let mut pairs: Vec<(f64, usize)> = Vec::new();
        for idx in 1..route.len() - 1 {
            let v = route[idx];
            if !inst.is_pickup(v) {
                continue;
            }
            let d = inst.delivery_of(v);

            let pos_d = pos_in_route[d];
            if pos_d == usize::MAX {
                continue;
            }

            let prev_v = route[idx - 1];
            let next_v = route[idx + 1];
            let cost_v = inst.dist(prev_v, v) + inst.dist(v, next_v) - inst.dist(prev_v, next_v);

            let prev_d = route[pos_d - 1];
            let next_d = route[pos_d + 1];
            let cost_d = inst.dist(prev_d, d) + inst.dist(d, next_d) - inst.dist(prev_d, next_d);

            pairs.push((cost_v + cost_d, v));
        }

        if pairs.len() < 2 {
            while let Some(v) = pos_touched.pop() {
                pos_in_route[v] = usize::MAX;
            }
            continue;
        }

        // Select top-W worst-fitting pairs (W=8 for broader exploration)
        let w = pairs.len().min(8);
        pairs.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
        pairs.truncate(w);

        // Try all combinations of 2 from these W pairs
        for i in 0..pairs.len() {
            let p1 = pairs[i].1;
            let d1 = inst.delivery_of(p1);

            for &(_, p2) in &pairs[i + 1..] {
                let d2 = inst.delivery_of(p2);

                // Remove both pairs from source route
                stripped_buf.clear();
                for &v in route {
                    if v != p1 && v != d1 && v != p2 && v != d2 {
                        stripped_buf.push(v);
                    }
                }

                if stripped_buf.len() < 2 {
                    continue;
                }

                let delta_remove = route_distance(inst, &stripped_buf) - infos[ri].distance;

                // For split-dst: track top-2 independent insertions for each pair.
                // top[0] = best, top[1] = second-best (for when best routes collide).
                // Each entry: (route_idx, delta_dist, ep, ed)
                const NONE: (usize, f64, usize, usize) = (usize::MAX, f64::MAX, 0, 0);
                let mut p1_top = [NONE; 2];
                let mut p2_top = [NONE; 2];

                for (rj, info_rj) in infos.iter().enumerate() {
                    if rj == ri {
                        continue;
                    }

                    // Compute insertion for each pair once, reuse for both same-dst and split-dst
                    let ins_p1 = best_pair_insertion_with_info::<false>(
                        inst,
                        &sol.routes[rj],
                        info_rj,
                        p1,
                        d1,
                    );
                    let ins_p2 = best_pair_insertion_with_info::<false>(
                        inst,
                        &sol.routes[rj],
                        info_rj,
                        p2,
                        d2,
                    );

                    // Update split-dst top-2 for p1
                    if let Some((new_dist, ep, ed)) = ins_p1 {
                        let delta = new_dist - info_rj.distance;
                        if delta < p1_top[0].1 {
                            p1_top[1] = p1_top[0];
                            p1_top[0] = (rj, delta, ep, ed);
                        } else if delta < p1_top[1].1 {
                            p1_top[1] = (rj, delta, ep, ed);
                        }
                    }

                    // Update split-dst top-2 for p2
                    if let Some((new_dist, ep, ed)) = ins_p2 {
                        let delta = new_dist - info_rj.distance;
                        if delta < p2_top[0].1 {
                            p2_top[1] = p2_top[0];
                            p2_top[0] = (rj, delta, ep, ed);
                        } else if delta < p2_top[1].1 {
                            p2_top[1] = (rj, delta, ep, ed);
                        }
                    }

                    // Same-dst: order A (p1 first, then p2)
                    if let Some((_dist_p1, ep1, ed1)) = ins_p1 {
                        build_route_with_pair_buf(
                            &sol.routes[rj],
                            p1,
                            ep1,
                            d1,
                            ed1,
                            &mut intermediate_buf,
                        );
                        info_intermediate.compute(inst, &intermediate_buf);
                        if let Some((dist_both, ep2, ed2)) = best_pair_insertion_with_info::<false>(
                            inst,
                            &intermediate_buf,
                            &info_intermediate,
                            p2,
                            d2,
                        ) {
                            let total_delta = delta_remove + dist_both - info_rj.distance;
                            if total_delta < best_delta {
                                best_delta = total_delta;
                                move_type = 1;
                                mv_src = ri;
                                mv_p1 = p1;
                                mv_p2 = p2;
                                mv_same_dst = rj;
                                mv_same_first_is_p1 = true;
                                mv_same_ep1 = ep1;
                                mv_same_ed1 = ed1;
                                mv_same_ep2 = ep2;
                                mv_same_ed2 = ed2;
                            }
                        }
                    }

                    // Same-dst: order B (p2 first, then p1)
                    if let Some((_dist_p2, ep2, ed2)) = ins_p2 {
                        build_route_with_pair_buf(
                            &sol.routes[rj],
                            p2,
                            ep2,
                            d2,
                            ed2,
                            &mut intermediate_buf,
                        );
                        info_intermediate.compute(inst, &intermediate_buf);
                        if let Some((dist_both, ep1, ed1)) = best_pair_insertion_with_info::<false>(
                            inst,
                            &intermediate_buf,
                            &info_intermediate,
                            p1,
                            d1,
                        ) {
                            let total_delta = delta_remove + dist_both - info_rj.distance;
                            if total_delta < best_delta {
                                best_delta = total_delta;
                                move_type = 1;
                                mv_src = ri;
                                mv_p1 = p1;
                                mv_p2 = p2;
                                mv_same_dst = rj;
                                mv_same_first_is_p1 = false;
                                mv_same_ep1 = ep2;
                                mv_same_ed1 = ed2;
                                mv_same_ep2 = ep1;
                                mv_same_ed2 = ed1;
                            }
                        }
                    }
                }

                // Evaluate best split-dst assignment (p1→dst1, p2→dst2, dst1 ≠ dst2)
                if p1_top[0].0 != usize::MAX && p2_top[0].0 != usize::MAX {
                    let (split_p1, split_p2) = if p1_top[0].0 != p2_top[0].0 {
                        // Best routes differ — use both bests
                        (p1_top[0], p2_top[0])
                    } else {
                        // Same best route — try second-best for each, pick cheaper
                        let opt_a = if p1_top[1].0 != usize::MAX {
                            p1_top[1].1 + p2_top[0].1
                        } else {
                            f64::MAX
                        };
                        let opt_b = if p2_top[1].0 != usize::MAX {
                            p1_top[0].1 + p2_top[1].1
                        } else {
                            f64::MAX
                        };
                        if opt_a <= opt_b {
                            (p1_top[1], p2_top[0])
                        } else {
                            (p1_top[0], p2_top[1])
                        }
                    };

                    if split_p1.0 != usize::MAX && split_p2.0 != usize::MAX {
                        let total_delta = delta_remove + split_p1.1 + split_p2.1;
                        if total_delta < best_delta {
                            best_delta = total_delta;
                            move_type = 2;
                            mv_src = ri;
                            mv_p1 = p1;
                            mv_p2 = p2;
                            mv_split_dst1 = split_p1.0;
                            mv_split_ep1 = split_p1.2;
                            mv_split_ed1 = split_p1.3;
                            mv_split_dst2 = split_p2.0;
                            mv_split_ep2 = split_p2.2;
                            mv_split_ed2 = split_p2.3;
                        }
                    }
                }
            }
        }

        while let Some(v) = pos_touched.pop() {
            pos_in_route[v] = usize::MAX;
        }
    }

    // Apply the best move found
    match move_type {
        0 => false,
        1 => {
            // Same-dst: both pairs into one destination route
            let d1 = inst.delivery_of(mv_p1);
            let d2 = inst.delivery_of(mv_p2);

            let (first_p, first_d, second_p, second_d) = if mv_same_first_is_p1 {
                (mv_p1, d1, mv_p2, d2)
            } else {
                (mv_p2, d2, mv_p1, d1)
            };

            // Remove both pairs from source
            stripped_buf.clear();
            for &v in &sol.routes[mv_src] {
                if v != mv_p1 && v != d1 && v != mv_p2 && v != d2 {
                    stripped_buf.push(v);
                }
            }

            // Insert first pair into destination
            build_route_with_pair_buf(
                &sol.routes[mv_same_dst],
                first_p,
                mv_same_ep1,
                first_d,
                mv_same_ed1,
                &mut intermediate_buf,
            );

            // Insert second pair
            sol.routes[mv_same_dst] = build_route_with_pair(
                &intermediate_buf,
                second_p,
                mv_same_ep2,
                second_d,
                mv_same_ed2,
            );

            // Update source route
            if stripped_buf.len() <= 2 {
                sol.routes.remove(mv_src);
            } else {
                sol.routes[mv_src].clone_from(&stripped_buf);
            }

            sol.routes.retain(|r| r.len() > 2);
            true
        }
        2 => {
            // Split-dst: p1 → dst1, p2 → dst2 (dst1 ≠ dst2 ≠ src)
            let d1 = inst.delivery_of(mv_p1);
            let d2 = inst.delivery_of(mv_p2);

            // Remove both pairs from source
            stripped_buf.clear();
            for &v in &sol.routes[mv_src] {
                if v != mv_p1 && v != d1 && v != mv_p2 && v != d2 {
                    stripped_buf.push(v);
                }
            }

            // Insert p1 into dst1 (independent of dst2)
            sol.routes[mv_split_dst1] = build_route_with_pair(
                &sol.routes[mv_split_dst1],
                mv_p1,
                mv_split_ep1,
                d1,
                mv_split_ed1,
            );

            // Insert p2 into dst2 (independent of dst1)
            sol.routes[mv_split_dst2] = build_route_with_pair(
                &sol.routes[mv_split_dst2],
                mv_p2,
                mv_split_ep2,
                d2,
                mv_split_ed2,
            );

            // Update source route
            if stripped_buf.len() <= 2 {
                sol.routes.remove(mv_src);
            } else {
                sol.routes[mv_src].clone_from(&stripped_buf);
            }

            sol.routes.retain(|r| r.len() > 2);
            true
        }
        _ => unreachable!(),
    }
}