/*********************************************************************** Xpress Optimizer Examples ========================= file txp.cc ``````````` Solve a MIP using cuts/constraints that are lazily separated. We take a random instance of the symmetric TSP and solve that using lazily separated constraints. (c) 2024-2025 Fair Isaac Corporation ***********************************************************************/ #include
#include
#include
#include
#include
using xpress::XPRSProblem;
/** Example for solving a MIP using lazily separated cuts/constraints.
*
* We solve a random instance of the symmetric TSP using lazily separated
* cuts/constraints.
*
*
* The model is based on a graph G = (V,E).
* We have one binary variable x[e] for each edge e in E. That variable
* is set to 1 if edge e is selected in the optimal tour and 0 otherwise.
*
*
* The model contains only one explicit set of constraints:
*
for each v in V: sum(u in V : u != v) x[uv] == 2
* This states that from all the edges incident to a node u, exactly two
* must be selected in the optimal tour.
*
* * The above constraints ensures that the selected edges form tours. However, * it allows multiple tours, also known as subtours. So we need a constraint * that requires that there is only one tour (which then necessarily hits * all the nodes). This constraint is known as subtour elimination constraint * and is *
sum(e in S) x[e] <= |S|-1 for each subtour S
* Since there are exponentially many subtours in a graph, this constraint * is not stated explicitly. Instead we check for any solution that the * optimizer finds, whether it satisfies the subtour elimination constraints. * If it does then we accept the solution. Otherwise we reject the solution * and augment the model by the violated subtour eliminiation constraints. *
*
* This lazy addition of constraints is implemented using
* a preintsol callback that rejects any solution that violates a
* subtour elimination constraint and injects a violated subtour elimination
* constraint in case the solution candidate came from an integral node.
*
* * An important thing to note about this strategy is that dual reductions * have to be disabled. Since the optimizer does not see the whole model * (subtour elimination constraints are only generated on the fly), dual * reductions may cut off the optimal solution. *
*/
class TSP {
public:
/** Number of nodes in the instance. */
unsigned const nodes;
/** Number of edges in the instance. */
unsigned const edges;
/** X coordinate of nodes. */
std::vector nodeX;
/** Y coordinate of nodes. */
std::vector nodeY;
/** Variable indices for the edges. */
std::vector> x;
public:
/** Construct a new random instance.
* @param nodes The number of nodes in the instance.
* @param seed Random number seed.
*/
TSP(unsigned n, int seed = 0)
: nodes(n), edges((n * (n - 1)) / 2), nodeX(n), nodeY(n),
x(n, std::vector(n)) {
// Fill coordinates with random values.
std::srand(seed);
auto randCoord = []() {
return 4 * static_cast(rand()) / RAND_MAX;
};
std::generate(nodeX.begin(), nodeX.end(), randCoord);
std::generate(nodeY.begin(), nodeY.end(), randCoord);
}
/** Get the distance between two nodes.
* @param u First node.
* @param v Second node.
* @return The distance between u and v.
* The distance is symmetric.
*/
double distance(unsigned u, unsigned v) const {
return sqrt((nodeX[u] - nodeX[v]) * (nodeX[u] - nodeX[v]) +
(nodeY[u] - nodeY[v]) * (nodeY[u] - nodeY[v]));
}
/** Find the tour rooted at 0 in a solution.
* As a side effect, the tour is printed to the console.
* @param sol The current solution.
* @param from Stores the tour. from[u] yields the
* predecessor of u in the tour. If
* from[u] is negative then u
* is not in the tour.
* This parameter can be null.
* @return The length of the tour.
*/
unsigned findTour(std::vector const &sol,
std::vector *from) const {
std::unique_ptr> allocatedFrom;
if (!from) {
allocatedFrom.reset(new std::vector(nodes));
from = allocatedFrom.get();
}
from->assign(nodes, -1);
std::vector inTour(edges); // Marks edges on the subtour
unsigned u = 0;
unsigned used = 0;
std::cout << "0";
do {
for (unsigned v = 0; v < nodes; ++v) {
if (u == v) // no self-loops
continue;
else if ((*from)[v] != -1) // node already on tour
continue;
else if (sol[x[u][v]] < 0,5) // edge not selected in solution
continue;
else if (inTour[x[u][v]]) // edge already on tour
continue;
else {
std::cout << " -> " << v;
inTour[x[u][v]] = true;
(*from)[v] = u;
used += 1;
u = v;
break;
}
}
} while (u != 0);
std::cout << std::endl;
return used;
}
/** Integer solution check callback.
*/
class PreIntsolCallback {
TSP const &data;
public:
PreIntsolCallback(TSP const &d) : data(d) {}
void operator()(XPRSProblem &prob, int soltype, int *p_reject, double *) {
std::cout << "Checking candidate solution ..." << std::endl;
// Get current solution and check whether it is feasible
std::vector sol(data.edges);
prob.getLpSol(sol, nullptr, nullptr, nullptr);
std::vector from(data.nodes); unsigned used = data.findTour(sol, &from); std::cout << "Solution is " << std::flush; if (used < data.nodes) { // The tour given by the current solution does not pass through // all the nodes and is thus infeasible. // If soltype is non-zero then we reject. // If instead soltype is zero then the solution came from an // integral node. In this case we can reject by adding a cut // that cuts off that solution. Note that we must NOT return // null in that case because that would result in just dropping // the node, no matter whether we add cuts or not. std::cout << "infeasible (" << used << " edges)" << std::endl; if (soltype != 0) { *p_reject = 1; } else { // The tour is too short. Get the edges on the tour and // add a subtour elimination constraint std::vector ind(used);
for (unsigned u = 0, next = 0; u < data.nodes; ++u) {
if (from[u] >= 0)
ind[next++] = data.x[u][from[u]];
}
std::vector val(used, 1);
// Since we created the constraint in the original space,
// we must crush it to the presolved space before we can
// add it.
int status = -1;
double prerhs = 0.0;
int maxcoefs = prob.attributes.getCols();
int coefs = -1;
std::vector preind(maxcoefs);
std::vector preval(maxcoefs);
prob.presolveRow('L', used, ind, val, used - 1, maxcoefs, &coefs,
preind, preval, &prerhs, &status);
if (status == 0)
prob.addCuts(1, std::vector{0}, "L", &prerhs,
std::vector{0, coefs}, preind, preval);
else
throw std::runtime_error(
"failed to presolve subtour elimination constraint");
}
} else {
std::cout << "feasible" << std::endl;
// We accept the solution, so nothing to do
}
}
};
/** Create a feasible tour and add this as initial MIP solution. */
void createInitialTour(XPRSProblem &prob) {
std::vector ind(nodes);
std::vector val(nodes); // Create a tour that just visits each node in order. for (unsigned i = 0; i < nodes; ++i) { ind[i] = x[i][(i + 1) % nodes]; val[i] = 1; } prob.addMipSol(nodes, val, ind, "initial"); } /** Solve the TSP represented by this instance. */ void solve() { XPRSProblem prob; // Create variables. We create one variable for each edge in // the (complete) graph. x[u][v] gives the index of the variable // that represents edge uv. Since edges are undirected, x[v][u] // gives the same variable. // All variables are binary. for (unsigned i = 0; i < nodes; ++i) { for (unsigned j = i + 1; j < nodes; ++j) { x[j][i] = x[i][j] = prob.attributes.getCols(); prob.addCols(1, 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); prob.chgColType(1, &x[i][j], "B"); } } // Objective. All variables are in the objective and their // respective coefficient is the distance between the two nodes. std::vector objInd(edges);
std::vector objVal(edges);
for (unsigned i = 0, nz = 0; i < nodes; ++i) {
for (unsigned j = i + 1; j < nodes; ++j) {
objInd[nz] = x[i][j];
objVal[nz] = distance(i, j);
++nz;
}
}
prob.chgObj(edges, objInd, objVal);
// Constraint: In the graph that is induced by the selected
// edges, each node should have degree 2.
// This is the only constraint we add explicitly.
// Subtour elimination constraints are added
// dynamically via a callback.
for (unsigned u = 0; u < nodes; ++u) {
std::vector ind(nodes - 1);
for (unsigned v = 0, next = 0; v < nodes; ++v) {
if (u != v)
ind[next++] = x[u][v];
}
std::vector val(nodes - 1, 1);
prob.addRows(1, nodes - 1, "E", std::vector({2}), nullptr,
std::vector({0}), ind, val); } // Create a starting solution. // This is optional but having a feasible solution available right // from the beginning can improve optimizer performance. createInitialTour(prob); // Write out the model in case we want to look at it. prob.writeProb("tsp.lp", "l"); // We don't have all constraints explicitly in the matrix, hence // we must disable dual reductions. Otherwise MIP presolve may // cut off the optimal solution. prob.controls.setMipDualReductions(0); // Add a callback that rejects solutions that do not satisfy // the subtour constraints. PreIntsolCallback cb(*this); prob.addPreIntsolCallback(cb); // Add a message listener to display log information. prob.addMessageCallback(XPRSProblem::console); prob.optimize("", nullptr, nullptr); auto sol = prob.getSolution(); // Print the optimal tour. std::cout << "Tour with length " << prob.attributes.getObjVal() << std::endl; findTour(sol, nullptr); } }; int main(void) { try { TSP(10).solve(); return 0; } catch (std::exception &e) { std::cout << "Exception: " << e.what() << std::endl; return -1; } }