use crate::graph::Graph;
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap, HashSet};
struct State {
dist: f64,
node: usize,
}
impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
self.dist == other.dist
}
}
impl Eq for State {}
impl PartialOrd for State {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for State {
fn cmp(&self, other: &Self) -> Ordering {
other
.dist
.partial_cmp(&self.dist)
.unwrap_or(Ordering::Equal)
}
}
pub fn dijkstra(graph: &Graph, source: usize) -> Vec<f64> {
let mut dist = vec![f64::INFINITY; graph.n_nodes];
dist[source] = 0.0;
let mut heap = BinaryHeap::new();
heap.push(State {
dist: 0.0,
node: source,
});
while let Some(State { dist: d, node }) = heap.pop() {
if d > dist[node] {
continue;
}
for (next, w) in graph.neighbors(node) {
let nd = d + w;
if nd < dist[next] {
dist[next] = nd;
heap.push(State {
dist: nd,
node: next,
});
}
}
}
dist
}
pub fn shortest_path_route(
graph: &Graph,
source: usize,
target: usize,
) -> Option<(Vec<usize>, Vec<usize>, f64)> {
dijkstra_combined(graph, graph, 0.0, source, target).map(|(p, e, c, _)| (p, e, c))
}
fn shortest_path_blocked(
graph: &Graph,
source: usize,
target: usize,
blocked_nodes: &HashSet<usize>,
blocked_edges: &HashSet<(usize, usize)>,
) -> (Vec<usize>, f64) {
let mut dist = vec![f64::INFINITY; graph.n_nodes];
let mut prev = vec![usize::MAX; graph.n_nodes];
dist[source] = 0.0;
let mut heap = BinaryHeap::new();
heap.push(State {
dist: 0.0,
node: source,
});
while let Some(State { dist: d, node }) = heap.pop() {
if node == target {
break;
}
if d > dist[node] {
continue;
}
for (next, w) in graph.neighbors(node) {
if blocked_nodes.contains(&next) || blocked_edges.contains(&(node, next)) {
continue;
}
let nd = d + w;
if nd < dist[next] {
dist[next] = nd;
prev[next] = node;
heap.push(State {
dist: nd,
node: next,
});
}
}
}
if dist[target].is_infinite() {
return (Vec::new(), f64::INFINITY);
}
let mut path = vec![target];
let mut cur = target;
while cur != source {
cur = prev[cur];
path.push(cur);
}
path.reverse();
(path, dist[target])
}
fn edge_argmin_map(from: &[usize], to: &[usize], weight: &[f64]) -> HashMap<(usize, usize), usize> {
let mut m: HashMap<(usize, usize), usize> = HashMap::new();
for i in 0..from.len() {
let key = (from[i], to[i]);
match m.entry(key) {
std::collections::hash_map::Entry::Occupied(mut e) => {
if weight[i] < weight[*e.get()] {
e.insert(i);
}
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(i);
}
}
}
m
}
fn path_cost(path: &[usize], amap: &HashMap<(usize, usize), usize>, weight: &[f64]) -> f64 {
let mut c = 0.0;
for w in path.windows(2) {
c += amap
.get(&(w[0], w[1]))
.map(|&i| weight[i])
.unwrap_or(f64::INFINITY);
}
c
}
fn path_edges(path: &[usize], amap: &HashMap<(usize, usize), usize>) -> Vec<usize> {
path.windows(2)
.filter_map(|w| amap.get(&(w[0], w[1])).copied())
.collect()
}
pub struct Route {
pub path: Vec<usize>,
pub edges: Vec<usize>,
pub cost: f64,
}
pub fn k_shortest_paths(
graph: &Graph,
from: &[usize],
to: &[usize],
weight: &[f64],
source: usize,
target: usize,
k: usize,
) -> Vec<Route> {
let amap = edge_argmin_map(from, to, weight);
let empty_n: HashSet<usize> = HashSet::new();
let empty_e: HashSet<(usize, usize)> = HashSet::new();
let mut accepted: Vec<(Vec<usize>, f64)> = Vec::new();
let (first, first_cost) = shortest_path_blocked(graph, source, target, &empty_n, &empty_e);
if first.is_empty() {
return Vec::new();
}
accepted.push((first, first_cost));
let mut candidates: Vec<(Vec<usize>, f64)> = Vec::new();
while accepted.len() < k {
let last = &accepted[accepted.len() - 1].0;
for i in 0..last.len() - 1 {
let spur_node = last[i];
let root_path = &last[0..=i];
let mut blocked_edges: HashSet<(usize, usize)> = HashSet::new();
for (p, _) in &accepted {
if p.len() > i && &p[0..=i] == root_path {
blocked_edges.insert((p[i], p[i + 1]));
}
}
let mut blocked_nodes: HashSet<usize> = HashSet::new();
for &n in &root_path[0..root_path.len() - 1] {
blocked_nodes.insert(n);
}
let (spur, _) =
shortest_path_blocked(graph, spur_node, target, &blocked_nodes, &blocked_edges);
if spur.is_empty() {
continue;
}
let mut total = root_path[0..root_path.len() - 1].to_vec();
total.extend_from_slice(&spur);
let cost = path_cost(&total, &amap, weight);
if !candidates.iter().any(|(p, _)| *p == total)
&& !accepted.iter().any(|(p, _)| *p == total)
{
candidates.push((total, cost));
}
}
if candidates.is_empty() {
break;
}
candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
accepted.push(candidates.remove(0));
}
accepted
.into_iter()
.map(|(path, cost)| {
let edges = path_edges(&path, &amap);
Route { path, edges, cost }
})
.collect()
}
pub fn distance_matrix(graph: &Graph, sources: &[usize], targets: &[usize]) -> Vec<f64> {
let mut out = Vec::with_capacity(sources.len() * targets.len());
for &s in sources {
let dist = dijkstra(graph, s);
for &t in targets {
out.push(dist[t]);
}
}
out
}
#[inline]
fn arc(cost: &[f64], n: usize, i: usize, j: usize) -> f64 {
cost[i * n + j]
}
fn tour_cost(cost: &[f64], n: usize, tour: &[usize], closed: bool) -> f64 {
let mut c = 0.0;
for w in tour.windows(2) {
c += arc(cost, n, w[0], w[1]);
}
if closed && tour.len() > 1 {
c += arc(cost, n, tour[tour.len() - 1], tour[0]);
}
c
}
fn nearest_neighbor_tour(cost: &[f64], n: usize, start: usize, end: Option<usize>) -> Vec<usize> {
let mut visited = vec![false; n];
let mut tour = Vec::with_capacity(n);
tour.push(start);
visited[start] = true;
let reserved = match end {
Some(e) if e != start => {
visited[e] = true;
1
}
_ => 0,
};
while tour.len() < n - reserved {
let last = *tour.last().unwrap();
let mut best = usize::MAX;
let mut best_cost = f64::INFINITY;
for (j, &seen) in visited.iter().enumerate() {
if !seen && (best == usize::MAX || arc(cost, n, last, j) < best_cost) {
best = j;
best_cost = arc(cost, n, last, j);
}
}
visited[best] = true;
tour.push(best);
}
if let Some(e) = end {
if e != start {
tour.push(e);
}
}
tour
}
fn local_search(cost: &[f64], n: usize, tour: &mut Vec<usize>, closed: bool, fix_end: bool) -> f64 {
let len = tour.len();
let mut best_cost = tour_cost(cost, n, tour, closed);
if len < 3 {
return best_cost;
}
let tail = if fix_end { 1 } else { 0 };
loop {
let mut improved = false;
for seg_len in 1..=3usize.min(len - 2) {
if 1 + seg_len + tail > len {
break;
}
for i in 1..=(len - seg_len - tail) {
let segment: Vec<usize> = tour[i..i + seg_len].to_vec();
let mut rest: Vec<usize> = Vec::with_capacity(len - seg_len);
rest.extend_from_slice(&tour[..i]);
rest.extend_from_slice(&tour[i + seg_len..]);
let last_slot = rest.len() - tail;
for j in 1..=last_slot {
for reversed in [false, true] {
if reversed && seg_len == 1 {
continue;
}
let mut cand = Vec::with_capacity(len);
cand.extend_from_slice(&rest[..j]);
if reversed {
cand.extend(segment.iter().rev());
} else {
cand.extend_from_slice(&segment);
}
cand.extend_from_slice(&rest[j..]);
let c = tour_cost(cost, n, &cand, closed);
if c < best_cost {
best_cost = c;
*tour = cand;
improved = true;
}
}
}
}
}
let hi = len - tail;
for i in 1..hi {
for j in (i + 1)..hi {
let mut cand = tour.clone();
cand.swap(i, j);
let c = tour_cost(cost, n, &cand, closed);
if c < best_cost {
best_cost = c;
*tour = cand;
improved = true;
}
let mut cand = tour.clone();
cand[i..=j].reverse();
let c = tour_cost(cost, n, &cand, closed);
if c < best_cost {
best_cost = c;
*tour = cand;
improved = true;
}
}
}
if !improved {
break;
}
}
best_cost
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn range(&mut self, lo: usize, hi: usize) -> usize {
lo + (self.next() % (hi - lo) as u64) as usize
}
}
fn double_bridge(rng: &mut Rng, tour: &[usize]) -> Vec<usize> {
let len = tour.len();
if len < 4 {
return tour.to_vec();
}
let mut cuts = [rng.range(1, len), rng.range(1, len), rng.range(1, len)];
cuts.sort_unstable();
let [a, b, c] = cuts;
if a == b || b == c {
return tour.to_vec();
}
let mut out = Vec::with_capacity(len);
out.extend_from_slice(&tour[..a]);
out.extend_from_slice(&tour[b..c]);
out.extend_from_slice(&tour[a..b]);
out.extend_from_slice(&tour[c..]);
out
}
pub fn tsp_tour(
cost: &[f64],
n: usize,
start: usize,
end: Option<usize>,
closed: bool,
improve: bool,
) -> Vec<usize> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![start];
}
let cost: Vec<f64> = cost
.iter()
.map(|&c| if c.is_finite() { c } else { 1e15 })
.collect();
let end = if closed { None } else { end };
let mut tour = nearest_neighbor_tour(&cost, n, start, end);
if !improve {
return tour;
}
let fix_end = end.is_some();
let mut best_cost = local_search(&cost, n, &mut tour, closed, fix_end);
let kicks = (100_000_000 / (n * n * n).max(1)).clamp(25, 400);
let patience = (kicks / 4).max(20);
let mut rng = Rng(0x9E3779B97F4A7C15);
let mut since_improved = 0usize;
for _ in 0..kicks {
let mut cand = double_bridge(&mut rng, &tour);
let c = local_search(&cost, n, &mut cand, closed, fix_end);
if c < best_cost {
best_cost = c;
tour = cand;
since_improved = 0;
} else {
since_improved += 1;
if since_improved >= patience {
break;
}
}
}
tour
}
pub struct ParetoRoute {
pub path: Vec<usize>,
pub edges: Vec<usize>,
pub cost: f64,
pub resource: f64,
}
struct Label {
node: usize,
edge: usize,
pred: usize,
}
pub fn csr_edge_order(from: &[usize], n_nodes: usize) -> Vec<usize> {
let m = from.len();
let mut degree = vec![0usize; n_nodes + 1];
for &u in from {
degree[u + 1] += 1;
}
for i in 0..n_nodes {
degree[i + 1] += degree[i];
}
let mut cursor = degree;
let mut order = vec![0usize; m];
for e in 0..m {
let pos = cursor[from[e]];
order[pos] = e;
cursor[from[e]] += 1;
}
order
}
struct BiState {
cost: f64,
resource: f64,
label: usize,
}
impl PartialEq for BiState {
fn eq(&self, other: &Self) -> bool {
self.cost == other.cost && self.resource == other.resource
}
}
impl Eq for BiState {}
impl PartialOrd for BiState {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BiState {
fn cmp(&self, other: &Self) -> Ordering {
other
.cost
.partial_cmp(&self.cost)
.unwrap_or(Ordering::Equal)
.then(
other
.resource
.partial_cmp(&self.resource)
.unwrap_or(Ordering::Equal),
)
}
}
#[allow(clippy::too_many_arguments)] pub fn pareto_paths(
from: &[usize],
to: &[usize],
w_cost: &[f64],
w_res: &[f64],
n_nodes: usize,
source: usize,
target: usize,
budget: f64,
max_routes: usize,
) -> Vec<ParetoRoute> {
if max_routes == 0 {
return Vec::new();
}
if source == target {
return vec![ParetoRoute {
path: vec![source],
edges: Vec::new(),
cost: 0.0,
resource: 0.0,
}];
}
let g_cost = Graph::from_edges(from, to, w_cost, n_nodes);
let g_res = Graph::from_edges(from, to, w_res, n_nodes);
let rev_res = Graph::from_edges(to, from, w_res, n_nodes);
let lb_res = dijkstra(&rev_res, target);
let edge_order = csr_edge_order(from, n_nodes);
let mut arena: Vec<Label> = Vec::new();
let mut heap: BinaryHeap<BiState> = BinaryHeap::new();
let mut min_res = vec![f64::INFINITY; n_nodes];
let mut routes: Vec<ParetoRoute> = Vec::new();
if lb_res[source].is_finite() && lb_res[source] <= budget {
arena.push(Label {
node: source,
edge: usize::MAX,
pred: usize::MAX,
});
heap.push(BiState {
cost: 0.0,
resource: 0.0,
label: 0,
});
}
while let Some(BiState {
cost,
resource,
label,
}) = heap.pop()
{
let node = arena[label].node;
if min_res[node] <= resource {
continue; }
min_res[node] = resource;
if node == target {
let mut path = Vec::new();
let mut edges = Vec::new();
let mut cur = label;
while cur != usize::MAX {
path.push(arena[cur].node);
if arena[cur].edge != usize::MAX {
edges.push(edge_order[arena[cur].edge]);
}
cur = arena[cur].pred;
}
path.reverse();
edges.reverse();
routes.push(ParetoRoute {
path,
edges,
cost,
resource,
});
if routes.len() >= max_routes {
break;
}
continue;
}
let start = g_cost.offsets[node];
let end = g_cost.offsets[node + 1];
for e in start..end {
let next = g_cost.targets[e];
let nc = cost + g_cost.weights[e];
let nr = resource + g_res.weights[e];
if !lb_res[next].is_finite()
|| nr + lb_res[next] > budget
|| min_res[next] <= nr
|| min_res[target] <= nr + lb_res[next]
{
continue;
}
arena.push(Label {
node: next,
edge: e,
pred: label,
});
heap.push(BiState {
cost: nc,
resource: nr,
label: arena.len() - 1,
});
}
}
routes
}
pub struct LagrangianRoute {
pub path: Vec<usize>,
pub edges: Vec<usize>,
pub cost: f64,
pub resource: f64,
pub lower_bound: f64,
pub lambda: f64,
pub iterations: usize,
}
fn dijkstra_combined(
g1: &Graph,
g2: &Graph,
lambda: f64,
source: usize,
target: usize,
) -> Option<(Vec<usize>, Vec<usize>, f64, f64)> {
let n = g1.n_nodes;
let mut dist = vec![f64::INFINITY; n];
let mut pred_node = vec![usize::MAX; n];
let mut pred_edge = vec![usize::MAX; n];
dist[source] = 0.0;
let mut heap = BinaryHeap::new();
heap.push(State {
dist: 0.0,
node: source,
});
while let Some(State { dist: d, node }) = heap.pop() {
if node == target {
break;
}
if d > dist[node] {
continue;
}
for e in g1.offsets[node]..g1.offsets[node + 1] {
let next = g1.targets[e];
let nd = d + g1.weights[e] + lambda * g2.weights[e];
if nd < dist[next] {
dist[next] = nd;
pred_node[next] = node;
pred_edge[next] = e;
heap.push(State {
dist: nd,
node: next,
});
}
}
}
if dist[target].is_infinite() {
return None;
}
let mut path = vec![target];
let mut edges = Vec::new();
let (mut w1, mut w2) = (0.0, 0.0);
let mut cur = target;
while cur != source {
let e = pred_edge[cur];
edges.push(e);
w1 += g1.weights[e];
w2 += g2.weights[e];
cur = pred_node[cur];
path.push(cur);
}
path.reverse();
edges.reverse();
Some((path, edges, w1, w2))
}
#[allow(clippy::too_many_arguments)] pub fn csp_lagrangian(
from: &[usize],
to: &[usize],
w_cost: &[f64],
w_res: &[f64],
n_nodes: usize,
source: usize,
target: usize,
budget: f64,
max_iter: usize,
) -> Option<LagrangianRoute> {
if budget < 0.0 {
return None;
}
if source == target {
return Some(LagrangianRoute {
path: vec![source],
edges: Vec::new(),
cost: 0.0,
resource: 0.0,
lower_bound: 0.0,
lambda: 0.0,
iterations: 0,
});
}
let g_cost = Graph::from_edges(from, to, w_cost, n_nodes);
let g_res = Graph::from_edges(from, to, w_res, n_nodes);
let edge_order = csr_edge_order(from, n_nodes);
let to_input =
|slots: Vec<usize>| -> Vec<usize> { slots.into_iter().map(|s| edge_order[s]).collect() };
let (p0, e0, c0, r0) = dijkstra_combined(&g_cost, &g_res, 0.0, source, target)?;
if r0 <= budget {
return Some(LagrangianRoute {
path: p0,
edges: to_input(e0),
cost: c0,
resource: r0,
lower_bound: c0,
lambda: 0.0,
iterations: 1,
});
}
let (pr, er, rmin, c_of_rmin) = dijkstra_combined(&g_res, &g_cost, 0.0, source, target)?;
if rmin > budget {
return None;
}
let mut inf = (p0, e0, c0, r0); let mut feas = (pr, er, c_of_rmin, rmin); let mut best = feas.clone(); let mut best_lb = c0; let mut best_lambda = 0.0;
let mut iterations = 2usize;
for _ in 0..max_iter {
let lam = (feas.2 - inf.2) / (inf.3 - feas.3);
if !lam.is_finite() || lam <= 0.0 {
break;
}
let (p, e, c, r) = match dijkstra_combined(&g_cost, &g_res, lam, source, target) {
Some(x) => x,
None => break,
};
iterations += 1;
let lb = c + lam * (r - budget); if lb > best_lb {
best_lb = lb;
best_lambda = lam;
}
let tie = inf.2 + lam * inf.3;
if (c + lam * r) >= tie - 1e-9 * (1.0 + tie.abs()) {
break;
}
if r <= budget {
feas = (p, e, c, r);
if feas.2 < best.2 {
best = feas.clone();
}
} else {
inf = (p, e, c, r);
}
}
let lower_bound = best_lb.min(best.2); Some(LagrangianRoute {
path: best.0,
edges: to_input(best.1),
cost: best.2,
resource: best.3,
lower_bound,
lambda: best_lambda,
iterations,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn line_graph() -> Graph {
Graph::from_edges(&[0, 1, 2, 0], &[1, 2, 3, 3], &[1.0, 1.0, 1.0, 10.0], 4)
}
#[test]
fn dijkstra_distances() {
let g = line_graph();
let d = dijkstra(&g, 0);
assert_eq!(d, vec![0.0, 1.0, 2.0, 3.0]);
}
#[test]
fn path_prefers_shorter_route() {
let g = line_graph();
let (path, slots, cost) = shortest_path_route(&g, 0, 3).unwrap();
assert_eq!(path, vec![0, 1, 2, 3]);
assert_eq!(slots.len(), 3);
assert!((cost - 3.0).abs() < 1e-9);
let order = csr_edge_order(&[0, 1, 2, 0], 4);
let edges: Vec<usize> = slots.iter().map(|&s| order[s]).collect();
assert_eq!(edges, vec![0, 1, 2]);
}
#[test]
fn unreachable_is_none() {
let g = Graph::from_edges(&[0], &[1], &[1.0], 3);
assert!(shortest_path_route(&g, 0, 2).is_none());
}
#[test]
fn shortest_route_traverses_the_cheaper_parallel_edge() {
let from = vec![0, 0, 1];
let to = vec![1, 1, 2];
let w = vec![5.0, 1.0, 1.0];
let g = Graph::from_edges(&from, &to, &w, 3);
let (path, slots, cost) = shortest_path_route(&g, 0, 2).unwrap();
assert_eq!(path, vec![0, 1, 2]);
assert!((cost - 2.0).abs() < 1e-9);
let order = csr_edge_order(&from, 3);
let edges: Vec<usize> = slots.iter().map(|&s| order[s]).collect();
assert_eq!(edges, vec![1, 2]);
}
#[test]
fn yen_finds_two_distinct_paths() {
let from = vec![0, 1, 0, 2];
let to = vec![1, 3, 2, 3];
let w = vec![1.0, 1.0, 2.0, 2.0];
let g = Graph::from_edges(&from, &to, &w, 4);
let paths = k_shortest_paths(&g, &from, &to, &w, 0, 3, 2);
assert_eq!(paths.len(), 2);
assert_eq!(paths[0].path, vec![0, 1, 3]);
assert_eq!(paths[0].edges, vec![0, 1]);
assert!((paths[0].cost - 2.0).abs() < 1e-9);
assert_eq!(paths[1].path, vec![0, 2, 3]);
assert_eq!(paths[1].edges, vec![2, 3]);
assert!((paths[1].cost - 4.0).abs() < 1e-9);
}
#[test]
fn yen_caps_at_available_paths() {
let from = vec![0, 1];
let to = vec![1, 2];
let w = vec![1.0, 1.0];
let g = Graph::from_edges(&from, &to, &w, 3);
let paths = k_shortest_paths(&g, &from, &to, &w, 0, 2, 5);
assert_eq!(paths.len(), 1);
}
#[test]
fn distance_matrix_shape_and_values() {
let g = line_graph();
let dm = distance_matrix(&g, &[0, 1], &[2, 3]);
assert_eq!(dm, vec![2.0, 3.0, 1.0, 2.0]);
}
fn line_costs() -> Vec<f64> {
let xs: [f64; 4] = [0.0, 1.0, 2.0, 10.0];
let mut c = Vec::with_capacity(16);
for i in 0..4 {
for j in 0..4 {
c.push((xs[i] - xs[j]).abs());
}
}
c
}
#[test]
fn tsp_closed_tour_is_a_permutation_returning_home() {
let c = line_costs();
let tour = tsp_tour(&c, 4, 0, None, true, true);
let mut sorted = tour.clone();
sorted.sort_unstable();
assert_eq!(sorted, vec![0, 1, 2, 3]);
assert_eq!(tour[0], 0);
assert!((tour_cost(&c, 4, &tour, true) - 20.0).abs() < 1e-9);
}
#[test]
fn tsp_open_route_respects_fixed_end() {
let c = line_costs();
let tour = tsp_tour(&c, 4, 3, Some(0), false, true);
assert_eq!(tour[0], 3);
assert_eq!(tour[3], 0);
assert!((tour_cost(&c, 4, &tour, false) - 10.0).abs() < 1e-9);
}
#[test]
fn or_opt_fixes_a_greedy_mistake() {
let c = vec![
0.0, 1.0, 2.0, 50.0, 1.0, 0.0, 100.0, 2.0, 2.0, 100.0, 0.0, 1.0, 50.0, 3.0, 100.0, 0.0,
];
let nn = tsp_tour(&c, 4, 0, None, false, false);
let improved = tsp_tour(&c, 4, 0, None, false, true);
assert_eq!(nn, vec![0, 1, 3, 2]);
assert!((tour_cost(&c, 4, &nn, false) - 103.0).abs() < 1e-9);
assert_eq!(improved, vec![0, 2, 3, 1]);
assert!((tour_cost(&c, 4, &improved, false) - 6.0).abs() < 1e-9);
}
#[test]
fn tsp_handles_asymmetric_costs() {
let c = vec![0.0, 1.0, 10.0, 10.0, 0.0, 1.0, 1.0, 10.0, 0.0];
let tour = tsp_tour(&c, 3, 0, None, true, true);
assert_eq!(tour, vec![0, 1, 2]);
}
fn brute_force(cost: &[f64], n: usize, closed: bool) -> f64 {
fn go(
cost: &[f64],
n: usize,
tour: &mut Vec<usize>,
rest: &mut Vec<usize>,
closed: bool,
best: &mut f64,
) {
if rest.is_empty() {
let c = tour_cost(cost, n, tour, closed);
if c < *best {
*best = c;
}
return;
}
for i in 0..rest.len() {
let x = rest.remove(i);
tour.push(x);
go(cost, n, tour, rest, closed, best);
tour.pop();
rest.insert(i, x);
}
}
let mut best = f64::INFINITY;
let mut tour = vec![0usize];
let mut rest: Vec<usize> = (1..n).collect();
go(cost, n, &mut tour, &mut rest, closed, &mut best);
best
}
#[test]
fn ils_matches_brute_force_on_random_asymmetric_instances() {
let n = 7;
for seed in 1..=8u64 {
let mut rng = Rng(seed);
let mut cost = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
if i != j {
cost[i * n + j] = (rng.next() % 1000) as f64 + 1.0;
}
}
}
for closed in [true, false] {
let tour = tsp_tour(&cost, n, 0, None, closed, true);
let got = tour_cost(&cost, n, &tour, closed);
let best = brute_force(&cost, n, closed);
assert!(
(got - best).abs() < 1e-9,
"seed {seed} closed {closed}: got {got}, optimum {best}"
);
}
}
}
#[test]
fn ils_improves_on_nearest_neighbor_at_fifty_stops() {
let n = 50;
let mut rng = Rng(7);
let mut cost = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
if i != j {
cost[i * n + j] = (rng.next() % 1000) as f64 + 1.0;
}
}
}
let nn = tsp_tour(&cost, n, 0, None, true, false);
let ils = tsp_tour(&cost, n, 0, None, true, true);
assert!(tour_cost(&cost, n, &ils, true) < tour_cost(&cost, n, &nn, true));
}
fn diamond() -> (Vec<usize>, Vec<usize>, Vec<f64>, Vec<f64>) {
let from = vec![0, 1, 0, 2, 0];
let to = vec![1, 3, 2, 3, 3];
let cost = vec![1.0, 1.0, 3.0, 3.0, 10.0];
let res = vec![5.0, 5.0, 0.0, 0.0, 2.0];
(from, to, cost, res)
}
#[test]
fn pareto_front_on_diamond() {
let (from, to, cost, res) = diamond();
let routes = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, usize::MAX);
let vals: Vec<(f64, f64)> = routes.iter().map(|r| (r.cost, r.resource)).collect();
assert_eq!(vals, vec![(2.0, 10.0), (6.0, 0.0)]);
assert_eq!(routes[0].path, vec![0, 1, 3]);
assert_eq!(routes[1].path, vec![0, 2, 3]);
}
#[test]
fn budget_picks_the_cheapest_feasible_route() {
let (from, to, cost, res) = diamond();
let routes = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, 5.0, 1);
assert_eq!(routes.len(), 1);
assert_eq!(routes[0].path, vec![0, 2, 3]);
let routes = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, 2.0, usize::MAX);
let vals: Vec<(f64, f64)> = routes.iter().map(|r| (r.cost, r.resource)).collect();
assert_eq!(vals, vec![(6.0, 0.0)]);
}
#[test]
fn infeasible_budget_and_unreachable_target_are_empty() {
let (from, to, cost, res) = diamond();
assert!(pareto_paths(&from, &to, &cost, &res, 4, 0, 3, -1.0, usize::MAX).is_empty());
assert!(
pareto_paths(&from, &to, &cost, &res, 5, 0, 4, f64::INFINITY, usize::MAX).is_empty()
);
}
#[test]
fn pareto_trivial_and_capped() {
let (from, to, cost, res) = diamond();
let same = pareto_paths(&from, &to, &cost, &res, 4, 2, 2, f64::INFINITY, usize::MAX);
assert_eq!(same.len(), 1);
assert_eq!(same[0].path, vec![2]);
let capped = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, 1);
assert_eq!(capped.len(), 1);
assert_eq!(capped[0].path, vec![0, 1, 3]);
}
fn enumerate_values(
from: &[usize],
to: &[usize],
cost: &[f64],
res: &[f64],
n: usize,
s: usize,
t: usize,
) -> Vec<(f64, f64)> {
fn go(
edges: &[(usize, usize, f64, f64)],
t: usize,
node: usize,
seen: &mut Vec<bool>,
c: f64,
r: f64,
out: &mut Vec<(f64, f64)>,
) {
if node == t {
out.push((c, r));
return;
}
for &(u, v, wc, wr) in edges {
if u == node && !seen[v] {
seen[v] = true;
go(edges, t, v, seen, c + wc, r + wr, out);
seen[v] = false;
}
}
}
let edges: Vec<(usize, usize, f64, f64)> = (0..from.len())
.map(|i| (from[i], to[i], cost[i], res[i]))
.collect();
let mut seen = vec![false; n];
seen[s] = true;
let mut out = Vec::new();
go(&edges, t, s, &mut seen, 0.0, 0.0, &mut out);
out
}
fn pareto_filter(mut vals: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
vals.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.unwrap()
.then(a.1.partial_cmp(&b.1).unwrap())
});
let mut front: Vec<(f64, f64)> = Vec::new();
for (c, r) in vals {
if front.iter().all(|&(fc, fr)| !(fc <= c && fr <= r)) {
front.push((c, r));
}
}
front
}
#[test]
fn pareto_matches_brute_force_on_random_graphs() {
for seed in 1..=12u64 {
let mut rng = Rng(seed);
let n = 6usize;
let mut from = Vec::new();
let mut to = Vec::new();
let mut cost = Vec::new();
let mut res = Vec::new();
for u in 0..n {
for v in 0..n {
if u != v && rng.next() % 100 < 45 {
from.push(u);
to.push(v);
cost.push((rng.next() % 20) as f64 + 1.0);
res.push(if rng.next() % 2 == 0 {
0.0
} else {
(rng.next() % 10) as f64
});
}
}
}
let all = enumerate_values(&from, &to, &cost, &res, n, 0, n - 1);
let want = pareto_filter(all.clone());
let got: Vec<(f64, f64)> = pareto_paths(
&from,
&to,
&cost,
&res,
n,
0,
n - 1,
f64::INFINITY,
usize::MAX,
)
.iter()
.map(|r| (r.cost, r.resource))
.collect();
assert_eq!(got, want, "seed {seed}: front mismatch");
for budget in [0.0, 3.0, 7.0, 15.0] {
let best = all
.iter()
.filter(|&&(_, r)| r <= budget)
.map(|&(c, _)| c)
.fold(f64::INFINITY, f64::min);
let solved = pareto_paths(&from, &to, &cost, &res, n, 0, n - 1, budget, 1);
match solved.first() {
Some(r) => {
assert!(
(r.cost - best).abs() < 1e-9 && r.resource <= budget,
"seed {seed} budget {budget}: got ({}, {}), optimum {best}",
r.cost,
r.resource
);
}
None => assert!(
best.is_infinite(),
"seed {seed} budget {budget}: solver found nothing, optimum {best}"
),
}
}
}
}
#[test]
fn lagrangian_shortcuts_when_unconstrained_is_feasible() {
let (from, to, cost, res) = diamond();
let r = csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, 100.0, 50).unwrap();
assert_eq!(r.path, vec![0, 1, 3]);
assert_eq!((r.cost, r.resource, r.lower_bound), (2.0, 10.0, 2.0));
assert_eq!(r.iterations, 1);
}
#[test]
fn lagrangian_finds_the_optimum_with_a_duality_gap() {
let (from, to, cost, res) = diamond();
let r = csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, 5.0, 50).unwrap();
assert_eq!(r.path, vec![0, 2, 3]);
assert_eq!((r.cost, r.resource), (6.0, 0.0));
assert!(r.lower_bound <= 6.0 && r.lower_bound >= 2.0);
assert!((r.lambda - 0.4).abs() < 1e-9);
}
#[test]
fn lagrangian_infeasible_and_unreachable_are_none() {
let (from, to, cost, res) = diamond();
assert!(csp_lagrangian(&from, &to, &cost, &res, 4, 0, 1, 1.0, 50).is_none());
assert!(csp_lagrangian(&from, &to, &cost, &res, 5, 0, 4, 100.0, 50).is_none());
assert!(csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, -1.0, 50).is_none());
}
#[test]
fn lagrangian_is_sandwiched_by_the_exact_solver_on_random_graphs() {
for seed in 1..=12u64 {
let mut rng = Rng(seed * 7 + 1);
let n = 7usize;
let mut from = Vec::new();
let mut to = Vec::new();
let mut cost = Vec::new();
let mut res = Vec::new();
for u in 0..n {
for v in 0..n {
if u != v && rng.next() % 100 < 40 {
from.push(u);
to.push(v);
cost.push((rng.next() % 20) as f64 + 1.0);
res.push(if rng.next() % 2 == 0 {
0.0
} else {
(rng.next() % 10) as f64
});
}
}
}
for budget in [0.0, 2.0, 5.0, 9.0, 20.0] {
let exact = pareto_paths(&from, &to, &cost, &res, n, 0, n - 1, budget, 1);
let lagr = csp_lagrangian(&from, &to, &cost, &res, n, 0, n - 1, budget, 50);
match (exact.first(), lagr) {
(Some(e), Some(l)) => {
assert!(
l.resource <= budget,
"seed {seed} budget {budget}: infeasible Lagrangian answer"
);
assert!(
l.lower_bound <= e.cost + 1e-9 && e.cost <= l.cost + 1e-9,
"seed {seed} budget {budget}: lb {} / exact {} / lagr {}",
l.lower_bound,
e.cost,
l.cost
);
}
(None, None) => {}
(e, l) => panic!(
"seed {seed} budget {budget}: feasibility disagreement (exact {:?}, lagr {:?})",
e.map(|x| x.cost),
l.map(|x| x.cost)
),
}
}
}
}
#[test]
fn pareto_paths_are_consistent_with_their_values() {
let (from, to, cost, res) = diamond();
let amap = edge_argmin_map(&from, &to, &cost);
for r in pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, usize::MAX) {
assert!((path_cost(&r.path, &amap, &cost) - r.cost).abs() < 1e-9);
}
}
#[test]
fn returned_edges_are_the_traversed_ones() {
let from = vec![0, 0, 1];
let to = vec![1, 1, 2];
let cost = vec![1.0, 5.0, 1.0];
let res = vec![5.0, 0.0, 0.0];
let routes = pareto_paths(&from, &to, &cost, &res, 3, 0, 2, f64::INFINITY, usize::MAX);
let vals: Vec<(f64, f64)> = routes.iter().map(|r| (r.cost, r.resource)).collect();
assert_eq!(vals, vec![(2.0, 5.0), (6.0, 0.0)]);
assert_eq!(routes[0].edges, vec![0, 2]);
assert_eq!(routes[1].edges, vec![1, 2]);
let l = csp_lagrangian(&from, &to, &cost, &res, 3, 0, 2, 0.0, 50).unwrap();
assert_eq!(l.edges, vec![1, 2]);
assert_eq!((l.cost, l.resource), (6.0, 0.0));
}
#[test]
fn returned_edges_align_with_paths_and_values() {
let (from, to, cost, res) = diamond();
let check = |path: &[usize], edges: &[usize], want_c: f64, want_r: f64| {
assert_eq!(edges.len() + 1, path.len());
let (mut c, mut r) = (0.0, 0.0);
for (i, &e) in edges.iter().enumerate() {
assert_eq!(from[e], path[i]);
assert_eq!(to[e], path[i + 1]);
c += cost[e];
r += res[e];
}
assert!((c - want_c).abs() < 1e-9 && (r - want_r).abs() < 1e-9);
};
for rt in pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, usize::MAX) {
check(&rt.path, &rt.edges, rt.cost, rt.resource);
}
let l = csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, 5.0, 50).unwrap();
check(&l.path, &l.edges, l.cost, l.resource);
}
#[test]
fn tsp_trivial_sizes() {
assert_eq!(tsp_tour(&[0.0], 1, 0, None, true, true), vec![0]);
let c = vec![0.0, 2.0, 3.0, 0.0];
assert_eq!(tsp_tour(&c, 2, 1, None, false, true), vec![1, 0]);
}
}