#[macro_use]
extern crate log;
use microlp::{ComparisonOp, LinearExpr, OptimizationDirection, Variable};
use std::io;
#[derive(Clone, Copy, Debug)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn sqr_dist(self, other: Self) -> f64 {
(self.x - other.x) * (self.x - other.x) + (self.y - other.y) * (self.y - other.y)
}
fn dist(self, other: Self) -> f64 {
f64::sqrt(self.sqr_dist(other))
}
}
struct Problem {
name: String,
points: Vec<Point>,
}
fn read_line<R: io::BufRead>(mut input: R) -> io::Result<Vec<String>> {
let mut line = String::new();
input.read_line(&mut line)?;
Ok(line.split_whitespace().map(|tok| tok.to_owned()).collect())
}
fn parse_num<T: std::str::FromStr>(input: &str, line_num: usize) -> io::Result<T> {
input.parse::<T>().or(Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("line {}: couldn't parse number", line_num),
)))
}
impl Problem {
fn dist(&self, n1: usize, n2: usize) -> f64 {
self.points[n1].dist(self.points[n2])
}
fn parse<R: io::BufRead>(mut input: R) -> io::Result<Problem> {
let mut name = String::new();
let mut dimension = None;
let mut line_num = 0;
loop {
let line = read_line(&mut input)?;
if line.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"premature end of header".to_string(),
));
}
let mut keyword = line[0].clone();
if keyword.ends_with(":") {
keyword.pop();
}
if keyword == "NAME" {
name = line.last().unwrap().clone();
} else if keyword == "TYPE" {
if line.last().unwrap() != "TSP" {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"only problems with TYPE: TSP supported".to_string(),
));
}
} else if keyword == "EDGE_WEIGHT_TYPE" {
if line.last().unwrap() != "EUC_2D" {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"only problems with EDGE_WEIGHT_TYPE: EUC_2D supported".to_string(),
));
}
} else if keyword == "DIMENSION" {
let dim: usize = parse_num(line.last().as_ref().unwrap(), line_num)?;
dimension = Some(dim);
} else if keyword == "NODE_COORD_SECTION" {
break;
}
line_num += 1;
}
let num_points = dimension.ok_or(io::Error::new(
io::ErrorKind::InvalidData,
"no DIMENSION specified".to_string(),
))?;
if num_points > 100_000 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("problem dimension: {} is suspiciously large", num_points),
));
}
let mut point_opts = vec![None; num_points];
for _ in 0..num_points {
let line = read_line(&mut input)?;
let node_num: usize = parse_num(&line[0], line_num)?;
let x: f64 = parse_num(&line[1], line_num)?;
let y: f64 = parse_num(&line[2], line_num)?;
if node_num == 0 || node_num > num_points {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("line {}: bad node number: {}", line_num, node_num),
));
}
if point_opts[node_num - 1].is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("line {}: node {} specified twice", line_num, node_num),
));
}
point_opts[node_num - 1] = Some(Point { x, y });
line_num += 1;
}
let line = read_line(input)?;
if line.len() > 1 || (line.len() == 1 && line[0] != "EOF") {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("line {}: expected EOF", line_num),
));
}
let mut points = vec![];
for (i, po) in point_opts.into_iter().enumerate() {
if let Some(p) = po {
points.push(p);
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("node {} is not specified", i),
));
}
}
Ok(Problem { name, points })
}
}
struct Tour(Vec<usize>);
impl Tour {
fn to_string(&self) -> String {
self.0
.iter()
.map(|n| (n + 1).to_string())
.collect::<Vec<_>>()
.join(" ")
}
fn to_svg(&self, problem: &Problem) -> String {
let cmp_f64 = |x: &f64, y: &f64| x.partial_cmp(y).unwrap();
let min_x = problem.points.iter().map(|p| p.x).min_by(cmp_f64).unwrap();
let max_x = problem.points.iter().map(|p| p.x).max_by(cmp_f64).unwrap();
let min_y = problem.points.iter().map(|p| p.y).min_by(cmp_f64).unwrap();
let max_y = problem.points.iter().map(|p| p.y).max_by(cmp_f64).unwrap();
let width = 600;
let margin = 50;
let scale = ((width - 2 * margin) as f64) / (max_x - min_x);
let height = f64::round((max_y - min_y) * scale) as usize + 2 * margin;
let mut svg = String::new();
svg += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n";
svg += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n";
svg += " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
svg += &format!(
"<svg width=\"{}px\" height=\"{}px\" version=\"1.1\"",
width, height
);
svg += " xmlns=\"http://www.w3.org/2000/svg\">\n";
use std::fmt::Write;
svg += " <path fill=\"none\" stroke=\"black\" stroke-width=\"4px\" d=\"\n";
for &i in &self.0 {
let p = problem.points[i];
let px = f64::round((p.x - min_x) * scale) as usize + margin;
let py = f64::round((p.y - min_y) * scale) as usize + margin;
if i == 0 {
writeln!(&mut svg, " M {} {}", px, py).unwrap();
} else {
writeln!(&mut svg, " L {} {}", px, py).unwrap();
}
}
svg += " Z\n";
svg += " \"/>\n";
svg += "</svg>\n";
svg
}
}
fn solve(problem: &Problem) -> Tour {
info!("starting, problem name: {}", problem.name);
let num_points = problem.points.len();
let mut lp_problem = microlp::Problem::new(OptimizationDirection::Minimize);
let mut edge_vars = vec![vec![]; num_points];
for i in 0..num_points {
for j in 0..num_points {
let var = if j < i {
edge_vars[j][i]
} else {
lp_problem.add_var(problem.dist(i, j), (0.0, 1.0))
};
edge_vars[i].push(var);
}
}
for i in 0..num_points {
let mut edges_sum = LinearExpr::empty();
for j in 0..num_points {
if i != j {
edges_sum.add(edge_vars[i][j], 1.0);
}
}
lp_problem.add_constraint(edges_sum, ComparisonOp::Eq, 2.0);
}
let mut cur_solution = lp_problem.solve().unwrap();
cur_solution = add_subtour_constraints(cur_solution, &edge_vars);
struct Step {
start_solution: microlp::Solution, var: Variable,
start_val: u8,
cur_val: Option<u8>,
}
fn choose_branch_var(cur_solution: µlp::Solution) -> Option<Variable> {
let mut max_divergence = 0.0;
let mut max_var = None;
for (var, &val) in cur_solution {
let divergence = f64::abs(val - val.round());
if divergence > 1e-5 && divergence > max_divergence {
max_divergence = divergence;
max_var = Some(var);
}
}
max_var
}
fn new_step(start_solution: microlp::Solution, var: Variable) -> Step {
let start_val = if start_solution[var] < 0.5 { 0 } else { 1 };
Step {
start_solution,
var,
start_val,
cur_val: None,
}
}
let mut best_cost = f64::INFINITY;
let mut best_tour = None;
let mut dfs_stack = if let Some(var) = choose_branch_var(&cur_solution) {
info!(
"starting branch&bound, current obj. value: {:.2}",
cur_solution.objective(),
);
vec![new_step(cur_solution, var)]
} else {
info!(
"found optimal solution with initial relaxation! cost: {:.2}",
cur_solution.objective(),
);
return tour_from_lp_solution(&cur_solution, &edge_vars);
};
for iter in 0.. {
let cur_step = dfs_stack.last_mut().unwrap();
if let Some(ref mut val) = cur_step.cur_val {
if *val == cur_step.start_val {
*val = 1 - *val;
} else {
dfs_stack.pop();
if dfs_stack.is_empty() {
break;
} else {
continue;
}
}
} else {
cur_step.cur_val = Some(cur_step.start_val);
};
let mut cur_solution = cur_step.start_solution.clone();
if let Ok(new_solution) =
cur_solution.fix_var(cur_step.var, cur_step.cur_val.unwrap() as f64)
{
cur_solution = new_solution;
} else {
continue;
}
cur_solution = add_subtour_constraints(cur_solution, &edge_vars);
let obj_val = cur_solution.objective();
if obj_val > best_cost {
continue;
}
if let Some(var) = choose_branch_var(&cur_solution) {
dfs_stack.push(new_step(cur_solution, var));
} else {
if obj_val < best_cost {
info!(
"iter {} (search depth {}): found new best solution, cost: {:.2}",
iter,
dfs_stack.len(),
obj_val
);
best_cost = obj_val;
best_tour = Some(tour_from_lp_solution(&cur_solution, &edge_vars));
}
};
}
info!("found optimal solution, cost: {:.2}", best_cost);
best_tour.unwrap()
}
fn add_subtour_constraints(
mut cur_solution: microlp::Solution,
edge_vars: &[Vec<Variable>],
) -> microlp::Solution {
let num_points = edge_vars.len();
let mut edge_weights = Vec::with_capacity(num_points * num_points);
loop {
edge_weights.clear();
edge_weights.resize(num_points * num_points, 0.0);
for i in 0..num_points {
for j in 0..num_points {
if i != j {
edge_weights[i * num_points + j] = cur_solution[edge_vars[i][j]];
}
}
}
let (cut_weight, cut_mask) = find_min_cut(num_points, &mut edge_weights);
if cut_weight > 2.0 - 1e-8 {
return cur_solution;
}
let mut cut_edges_sum = LinearExpr::empty();
for i in 0..num_points {
for j in 0..i {
if cut_mask[i] != cut_mask[j] {
cut_edges_sum.add(edge_vars[i][j], 1.0);
}
}
}
cur_solution = cur_solution
.add_constraint(cut_edges_sum, ComparisonOp::Ge, 2.0)
.unwrap();
}
}
fn find_min_cut(size: usize, weights: &mut [f64]) -> (f64, Vec<bool>) {
assert!(size >= 2);
assert_eq!(weights.len(), size * size);
let mut is_merged = vec![false; size];
let mut next_in_cluster = (0..size).collect::<Vec<_>>();
let mut is_added = Vec::with_capacity(size);
let mut node_weights = Vec::with_capacity(size);
let mut best_cut_weight = f64::INFINITY;
let mut best_cut = Vec::with_capacity(size);
for i_phase in 0..(size - 1) {
is_added.clear();
is_added.extend_from_slice(&is_merged);
node_weights.clear();
node_weights.extend_from_slice(&weights[0..size]);
let mut prev_node = 0;
let mut last_node = 0;
for _ in 1..(size - i_phase) {
prev_node = last_node;
let mut max_weight = f64::NEG_INFINITY;
for n in 1..size {
if !is_added[n] && node_weights[n] > max_weight {
last_node = n;
max_weight = node_weights[n];
}
}
is_added[last_node] = true;
for i in 0..size {
if !is_added[i] {
node_weights[i] += weights[i * size + last_node];
}
}
}
let cut_weight = node_weights[last_node];
let is_best_cut = cut_weight < best_cut_weight;
if is_best_cut {
best_cut_weight = cut_weight;
best_cut.clear();
best_cut.resize(size, false);
}
is_merged[prev_node] = true;
let mut list_elem = last_node;
loop {
if is_best_cut {
best_cut[list_elem] = true;
}
if next_in_cluster[list_elem] != list_elem {
list_elem = next_in_cluster[list_elem];
} else {
next_in_cluster[list_elem] = prev_node;
break;
}
}
for n in 0..size {
weights[last_node * size + n] += weights[prev_node * size + n];
}
for n in 0..size {
weights[n * size + last_node] = weights[last_node * size + n];
}
}
assert!(best_cut_weight.is_finite());
(best_cut_weight, best_cut)
}
#[cfg(test)]
mod tests {
#[test]
fn test_min_cut() {
let size = 8;
let weights = [
[0.0, 2.0, 3.0, 0.0, 2.0, 2.0, 0.0, 0.0],
[2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0],
[3.0, 0.0, 0.0, 4.0, 0.0, 0.0, 2.0, 0.0],
[0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 2.0, 2.0],
[2.0, 3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0],
[2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 2.0, 2.0, 0.0, 1.0, 0.0, 3.0],
[0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 3.0, 0.0],
];
let mut weights_flat = vec![];
for row in &weights {
weights_flat.extend_from_slice(row);
}
let (weight, cut_mask) = super::find_min_cut(size, &mut weights_flat);
let nodes = (0..size).filter(|&n| cut_mask[n]).collect::<Vec<_>>();
assert_eq!(weight, 4.0);
assert_eq!(&nodes, &[2, 3, 6, 7]);
}
}
fn tour_from_lp_solution(lp_solution: µlp::Solution, edge_vars: &[Vec<Variable>]) -> Tour {
let num_points = edge_vars.len();
let mut tour = vec![];
let mut is_visited = vec![false; num_points];
let mut cur_point = 0;
for _ in 0..num_points {
assert!(!is_visited[cur_point]);
is_visited[cur_point] = true;
tour.push(cur_point);
for neighbor in 0..num_points {
if !is_visited[neighbor] && lp_solution[edge_vars[cur_point][neighbor]].round() == 1.0 {
cur_point = neighbor;
break;
}
}
}
assert_eq!(tour.len(), num_points);
Tour(tour)
}
const USAGE: &str = "\
USAGE:
tsp --help
tsp [--svg-output] INPUT_FILE
INPUT_FILE is a problem description in TSPLIB format. You can download some
problems from http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/.
Use - for stdin.
By default, prints a single line containing 1-based node indices in the
optimal tour order to stdout. If --svg-output option is enabled, prints an
SVG document containing the optimal tour.
Set RUST_LOG environment variable (e.g. to info) to enable logging to stderr.
";
fn main() {
env_logger::init();
let args = std::env::args().collect::<Vec<_>>();
if args.len() <= 1 {
eprint!("{}", USAGE);
std::process::exit(1);
}
if args[1] == "--help" {
eprintln!("Finds the optimal solution for a traveling salesman problem.\n");
eprint!("{}", USAGE);
return;
}
let (enable_svg_output, filename) = if args.len() == 2 {
(false, &args[1])
} else if args.len() == 3 && args[1] == "--svg-output" {
(true, &args[2])
} else {
eprintln!("Failed to parse arguments.\n");
eprint!("{}", USAGE);
std::process::exit(1);
};
let problem = if filename == "-" {
Problem::parse(std::io::stdin().lock()).unwrap()
} else {
let file = std::fs::File::open(filename).unwrap();
let input = io::BufReader::new(file);
Problem::parse(input).unwrap()
};
let tour = solve(&problem);
if enable_svg_output {
print!("{}", tour.to_svg(&problem));
} else {
println!("{}", tour.to_string());
}
}