Exact

▶ Open interactive explainer →

Branch and Bound

Alias branch_bound
Type Exact — only when --n_nearest ≥ the number of cities
Complexity Exponential worst-case; effective pruning often makes it practical for small instances
Benchmarks published results ↓

Description

Systematic enumeration of candidate tours that prunes any branch whose lower-bound cost already exceeds the best complete tour found so far. Explores the search tree depth-first, backtracking whenever it can prove no improvement is possible below the current node.

Do not use on more than ~20 cities — worst-case complexity is factorial.

Not exact at the default settings

This solver is labelled exact, and it is — but only when the candidate set covers every city. The default --n_nearest 3 restricts branching to the three nearest cities when extending a partial tour, which turns the search into a beam search: it can no longer reach tours whose edges leave that candidate set, so it returns valid but suboptimal tours. Measured against three instances with known optima:

Instance Optimum --n_nearest 3 (default) --n_nearest 100
burma14 3323 3651 (+9.9 %) 3323
gr17 2085 2187 (+4.9 %) 2085
ulysses16 6859 6909 (+0.7 %) 6859

With the candidate set widened to all cities, all three optima are found.

This is easy to miss from the outside: the tour is still a valid tour, the solver still reports success, and the gap only shows up if you compare against a known optimum or against bellman_karp. If you need a proof of optimality, pass --n_nearest at least equal to the number of cities. A small value buys a large speed-up and a quietly worse answer.

The trade-off is a property of candidate-list pruning in general, not a defect unique to this solver — but it is documented here because this is the only solver that claims exactness while defaulting to a pruned search.

procedure BranchAndBound(cities):
    best ← nearest_neighbor(cities)   // initial upper bound
    queue ← [partial tour starting at city 0]
    while queue not empty:
        node ← pop_most_promising(queue)
        if lower_bound(node) ≥ length(best):
            continue                   // prune branch
        if node is a complete tour:
            best ← node
        else:
            for each unvisited city c:
                queue.push(extend(node, c))
    return best

Usage

teeline solve branch_bound -i ./data/discopt/tsp_5_1.tsp
teeline solve branch_bound -i ./data/discopt/tsp_5_1.tsp --verbose

References

Benchmarks

Loading published results…