FICO
FICO Xpress Optimization Examples Repository
FICO Optimization Community FICO Xpress Optimization Home
Back to examples browserPrevious exampleNext example

The travelling salesman problem

Description
Solves the classic travelling salesman problem as a MIP, where sub-tour elimination constraints are added only as needed during the branch-and-bound search.


Source Files
By clicking on a file name, a preview is opened at the bottom of this page.
objects/TravelingSalesPerson.java[download]
TSP.java[download]





TravelingSalesPerson.java

// (c) 2023-2024 Fair Isaac Corporation

import static com.dashoptimization.objects.Utils.sum;
import static java.util.stream.IntStream.range;

import java.util.Arrays;
import java.util.Random;

import com.dashoptimization.ColumnType;
import com.dashoptimization.DefaultMessageListener;
import com.dashoptimization.DoubleHolder;
import com.dashoptimization.IntHolder;
import com.dashoptimization.XPRSenumerations.ObjSense;
import com.dashoptimization.XPRSenumerations.SolStatus;
import com.dashoptimization.objects.LinExpression;
import com.dashoptimization.objects.Variable;
import com.dashoptimization.objects.XpressProblem;

/**
 * Example for solving a MIP using lazily separated cuts/constraints.
 *
 * We solve a random instance of the symmetric TSP using lazily separated
 * cuts/constraints.
 *
 * <p>
 * The model is based on a directed 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.
 * </p>
 * <p>
 * The model contains only two explicit constraints:
 * <pre>
  for each v in V: sum(u in V : u != v) x[uv] == 1
  for each v in V: sum(u in V : u != v) x[vu] == 1
 </pre>
 * These state that node u should have exactly one outgoing and exactly
 * one incoming edge in a tour.
 * </p>
 * <p>
 * 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 a subtour elimination constraint
 * and is
 * <pre>
    sum(e in S) x[e] <= |S|-1  for each subtour S
 * </pre>
 *
 * 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 constraint. If it does
 * then we accept the solution. Otherwise we reject the solution and augment the
 * model by the violated subtour eliminiation constraint.
 * </p>
 * <p>
 * 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.
 * </p>
 * <p>
 * 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.
 * </p>
 */
public final class TravelingSalesPerson {
    /** Number of nodes in the instance. */
    private final int nodes;
    /** X coordinate of nodes. */
    private final double[] nodeX;
    /** Y coordinate of nodes. */
    private final double[] nodeY;
    /** Variables the edges. */
    private Variable[][] x;

    /**
     * Construct a new random instance with random seed 0.
     *
     * @param nodes The number of nodes in the instance.
     */
    public TravelingSalesPerson(int nodes) {
        this(nodes, 0);
    }

    /**
     * Construct a new random instance.
     *
     * @param nodes The number of nodes in the instance.
     * @param seed  Random number seed.
     */
    public TravelingSalesPerson(int nodes, int seed) {
        this.nodes = nodes;
        nodeX = new double[nodes];
        nodeY = new double[nodes];
        Random rand = new Random(seed);
        for (int i = 0; i < nodes; ++i) {
            nodeX[i] = 4.0 * rand.nextDouble();
            nodeY[i] = 4.0 * rand.nextDouble();
        }
    }

    /**
     * Get the distance between two nodes.
     *
     * @param u First node.
     * @param v Second node.
     * @return The distance between <code>u</code> and <code>v</code>. The distance
     *         is symmetric.
     */
    public double distance(int u, int v) {
        return Math.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. <code>from[u]</code> yields the predecessor of
     *             <code>u</code> in the tour. If <code>from[u]</code> is negative
     *             then <code>u</code> is not in the tour. This parameter can be
     *             <code>null</code>.
     * @return The length of the tour.
     */
    private int findTour(double[] sol, int[] from) {
        if (from == null)
            from = new int[nodes];
        Arrays.fill(from, -1);

        int node = 0;
        int used = 0;
        System.out.print("0");
        while (node != 0 || used == 0) {
            // Find the edge leaving node
            Variable edge = null;
            for (int i = 0; i < nodes; ++i) {
                if (i != node && x[node][i].getValue(sol) > 0.5) {
                    System.out.printf(" -> %d", i);
                    edge = x[node][i];
                    from[i] = node;
                    node = i;
                    ++used;
                    break;
                }
            }
            if (edge == null)
                break;
        }

        System.out.println();

        return used;
    }

    /**
     * Integer solution check callback.
     */
    private final class PreIntsolCallback implements XpressProblem.CallbackAPI.PreIntsolCallback {
        @Override
        public void preIntsol(XpressProblem prob, int soltype, IntHolder p_reject, DoubleHolder p_cutoff) {
            System.out.println("Checking candidate solution ...");

            // Get current solution and check whether it is feasible
            double[] sol = prob.getLpSolX();
            int[] from = new int[nodes];
            int used = findTour(sol, from);
            System.out.print("Solution is ");
            if (used < 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 by setting
                // p_reject.value=1.
                // 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 reject
                // in that case because that would result in just dropping
                // the node, no matter whether we add cuts or not.
                System.out.println("infeasible (" + used + " edges)");
                if (soltype != 0) {
                    p_reject.value = 1;
                } else {
                    // The tour is too short. Get the edges on the tour and
                    // add a subtour elimination constraint
                    LinExpression subtour = LinExpression.create();
                    for (int u = 0; u < nodes; ++u) {
                        if (from[u] >= 0)
                            subtour.addTerm(x[from[u]][u]);
                    }
                    // We add the constraint. The solver must translate the
                    // constraint from the original space into the presolved
                    // space. This may fail (in theory). In that case the
                    // addCut() function will return non-zero.
                    if (prob.addCut(1, subtour.leq(used - 1)) != 0)
                        throw new RuntimeException("failed to presolve subtour elimination constraint");
                }
            } else {
                System.out.println("feasible");
            }
        }
    }

    /** Create a feasible tour and add this as initial MIP solution. */
    private void createInitialTour(XpressProblem prob) {
        Variable[] variable = new Variable[nodes];
        double[] value = new double[nodes];
        // Create a tour that just visits each node in order.
        for (int i = 0; i < nodes; ++i) {
            variable[i] = x[i][(i + 1) % nodes];
            value[i] = 1.0;
        }
        prob.addMipSol(value, variable, "init");
    }

    /**
     * Solve the TSP represented by this instance.
     */
    public void solve() {
        try (XpressProblem prob = new XpressProblem(null)) {
            // Create variables. We create one variable for each edge in
            // the (complete) graph. That is, we create variables from each
            // node u to all other nodes v. We even create a variable for
            // the self-loop from u to u, but that variable is fixed to 0.
            // x[u][v] gives the variable that represents edge uv.
            // All variables are binary.
            x = prob.addVariables(nodes, nodes)
                .withType(ColumnType.Binary)
                .withName((i,j) -> String.format("x_%d_%d", i, j))
                .withUB((i,j) -> (i == j) ? 0.0 : 1.0)
                .toArray();

            // Objective. All variables are in the objective and their
            // respective coefficient is the distance between the two nodes.
            prob.setObjective(sum(nodes,
                                  u -> sum(nodes,
                                           v -> x[u][v].mul(distance(u, v)))),
                              ObjSense.MINIMIZE);

            // Constraint: In the graph that is induced by the selected
            //             edges, each node should have exactly one outgoing
            //             and exactly one incoming edge.
            //             These are the only constraints we add explicitly.
            //             Subtour elimination constraints are added
            //             dynamically via a callback.
            prob.addConstraints(nodes,
                                u -> sum(range(0, nodes)
                                         .filter(v -> v != u)
                                         .mapToObj(v -> x[u][v]))
                                     .eq(1));
            prob.addConstraints(nodes,
                                u -> sum(range(0, nodes)
                                         .filter(v -> v != u)
                                         .mapToObj(v -> x[v][u]))
                                     .eq(1));

            // 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("travelingsalesperson.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.
            prob.callbacks.addPreIntsolCallback(new PreIntsolCallback());

            // Add a message listener to display log information.
            prob.addMessageListener(DefaultMessageListener::console);

            prob.optimize();
            if (prob.attributes().getSolStatus() != SolStatus.OPTIMAL)
                throw new RuntimeException("failed to solve");

            double[] sol = prob.getSolution();

            // Print the optimal tour.
            System.out.println("Tour with length " + prob.attributes().getMIPBestObjVal());
            findTour(sol, null);

            x = null; // We are done with the variables
        }
    }

    public static void main(String[] args) {
        new TravelingSalesPerson(10).solve();
    }
}

Back to examples browserPrevious exampleNext example