Solves the traveling salesman problem using a callback for separating sub-tour elimination constraints.
Let \(V = \{1,\dots,n\}\) and let \(c_{ij}\) be the travel cost.
Decision variables: \(x_{ij} \in \{0,1\}\) for all \(i \neq j\).
\[
\begin{aligned}
\min_x \quad
& \sum_{i \in V} \sum_{j \in V \setminus \{i\}} c_{ij} x_{ij} \\
\text{s.t.}\quad
& \sum_{j \in V \setminus \{i\}} x_{ij} = 1
&& \forall i \in V \\
& \sum_{i \in V \setminus \{j\}} x_{ij} = 1
&& \forall j \in V \\
& \sum_{i \in S} \sum_{j \in S \setminus \{i\}} x_{ij}
\le |S| - 1
&& \forall S \subset V,\; 2 \le |S| \le n-1 \\
& x_{ij} \in \{0,1\}
&& \forall i \neq j
\end{aligned}
\]
\(S\) is any proper subset of cities.
#include <iostream>
#include "idol/modeling.h"
#include "idol/mixed-integer/optimizers/branch-and-bound/BranchAndBound.h"
#include "idol/mixed-integer/optimizers/wrappers/Gurobi/Gurobi.h"
#include "idol/mixed-integer/optimizers/callbacks/Callback.h"
using namespace idol;
Vector<Var, 2> m_x;
public:
SubTourEliminationCallback(const Vector<Var, 2>& t_x);
class Strategy;
};
class SubTourEliminationCallback::Strategy :
public Callback {
Vector<Var, 2> m_x;
public:
Strategy(const Vector<Var, 2>& t_x);
protected:
void operator()(CallbackEvent t_event) override;
};
int main(int t_argc, const char** t_argv) {
Vector<std::string> cities = {
"Montpellier",
"Grenoble",
"Lille",
"Pau",
"Toulouse",
"Paris",
"Trier"
};
Vector<double, 2> distances = {
{0.0, 296.0, 963.0, 780.0, 241.0, 750.0, 863.0},
{296.0, 0.0, 980.0, 860.0, 536.0, 570.0, 860.0},
{963.0, 980.0, 0.0,1000.0, 895.0, 225.0, 830.0},
{780.0, 860.0,1000.0, 0.0, 370.0, 795.0,1050.0},
{241.0, 536.0, 895.0, 370.0, 0.0, 680.0, 900.0},
{750.0, 570.0, 225.0, 795.0, 680.0, 0.0, 420.0},
{863.0, 860.0, 830.0,1050.0, 900.0, 420.0, 0.0}
};
const auto n_cities = cities.size();
const auto x = model.add_vars(
Dim<2>(n_cities, n_cities), 0, 1, Binary, 0,
"x");
model.set_obj_expr(idol_Sum(i,
Range(n_cities), idol_Sum(j,
Range(n_cities), distances[i][j] * x[i][j])));
for (unsigned int i = 0 ; i < n_cities ; ++i) {
model.add_ctr(idol_Sum(j,
Range(n_cities), x[i][j]) == 1);
model.add_ctr(idol_Sum(j,
Range(n_cities), x[j][i]) == 1);
}
for (unsigned int i = 0 ; i < n_cities ; ++i) {
model.remove(x[i][i]);
}
.with_lazy_cut(true)
.add_callback(SubTourEliminationCallback(x));
model.use(gurobi);
model.optimize();
for (unsigned int i = 0 ; i < n_cities ; ++i) {
for (unsigned int j = 0 ; j < n_cities ; ++j) {
if (i == j) { continue; }
const bool is_one = model.get_var_primal(x[i][j]) > .5;
if (is_one) {
std::cout << cities[i] << " ---> " << cities[j] << std::endl;
}
}
}
return 0;
}
SubTourEliminationCallback::SubTourEliminationCallback(const Vector<Var, 2>& t_x) : m_x(t_x) {
}
return new SubTourEliminationCallback(*this);
}
Callback* SubTourEliminationCallback::operator()() {
return new Strategy(m_x);
}
SubTourEliminationCallback::Strategy::Strategy(const Vector<Var, 2>& t_x) : m_x(t_x) {
}
void SubTourEliminationCallback::Strategy::operator()(CallbackEvent t_event) {
if (t_event != IncumbentSolution) {
return;
}
const unsigned int n_cities = m_x.size();
std::vector<int> visited(n_cities, 0);
const auto incumbent = primal_solution();
for (int start = 0; start < n_cities; ++start) {
if (visited[start]) continue;
std::vector<int> cycle;
int curr = start;
while (!visited[curr]) {
visited[curr] = 1;
cycle.push_back(curr);
int next = -1;
for (int j = 0; j < n_cities; ++j) {
if (curr == j) { continue; }
if (incumbent.get(m_x[curr][j]) > 0.5) {
next = j;
break;
}
}
if (next == -1) break;
curr = next;
}
if (cycle.size() < n_cities) {
for (int i : cycle) {
for (int j : cycle) {
if (i != j) {
cut_lhs += m_x[i][j];
}
}
}
std::cout << "Adding subtour cut for " << cycle.size() << " cities" << std::endl;
add_lazy_cut(cut_lhs <= cycle.size() - 1);
}
}
}
CRTP & with_logs(bool t_value)