Back to Graph Theory Series

TSP: Brute Force, Backtracking & Sorted Edges

October 4, 2026 Wasil Zafar 27 min read

See the same Traveling Salesman Problem through three lenses: exhaustive enumeration that proves optimality, backtracking that safely cuts away hopeless searches, and Cheapest Link—a fast edge-first heuristic that trades proof for speed.

Contents

  1. A Short History of Exact TSP
  2. Problem Model & Guarantees
  3. TSP Brute Force: Permutation Enumeration
  4. TSP Backtracking: Bounding & Pruning
  5. Why Lower Bounds Matter
  6. Sorted Edges (Cheapest Link)
  7. Worked Example
  8. Complexity Analysis
  9. Implementation
  10. Real-World Applications
  11. Exercises
  12. Limitations

A Short History of Exact TSP

The Traveling Salesman Problem is easy to state but has repeatedly driven new ideas in combinatorial optimization. Exhaustive permutation search remains the clearest exact baseline, yet the history of exact TSP also includes linear programming, cutting planes, dynamic programming, and complexity theory.

1954
George Dantzig, Ray Fulkerson, and Selmer Johnson proved an optimal tour for a 49-city instance using linear programming and subtour-elimination cuts—an early landmark in exact computation.
1962
Richard Bellman and, independently, Michael Held and Richard Karp developed subset dynamic-programming approaches that reduce factorial enumeration to exponential time.
1972
Richard Karp placed the decision version of TSP among the foundational NP-complete problems, explaining why a universally efficient exact algorithm is unlikely.
Today
Exact solvers combine strong bounds, cutting planes, branching, and careful engineering. Cheapest Link, meanwhile, remains a useful teaching and initialization heuristic: it builds a tour edge by edge without proving optimality.

This article keeps the original three-part progression: brute force establishes the truth, backtracking preserves exactness while pruning, and Sorted Edges (Cheapest Link) shows what changes when speed is preferred over a proof of optimality.

Problem Model & Guarantees

Given cities $V$ and travel costs $d(u,v)$, TSP asks for a minimum-cost Hamiltonian cycle: start at one city, visit every city exactly once, and return to the start. This page assumes a complete, symmetric, nonnegative distance matrix.

Exact methods

Brute force and backtracking eventually compare enough possibilities to certify the optimum. Their answer comes with a proof by exhaustion or bounds.

Constructive heuristic

Cheapest Link quickly constructs one legal cycle. It may be excellent or poor, but the tour alone does not prove how close it is to optimal.

A valid TSP tour compared with invalid edge selections A five-city cycle visits every city once and returns to the start. Two smaller cycles and a branching vertex are invalid. Valid Hamiltonian cycle ABCDE Every vertex has degree 2; one cycle covers all five cities. Invalid partial choices premature subtoursdegree 3 at one city
A legal TSP solution is one cycle, not merely a collection of cheap edges. The degree-two and no-premature-subtour rules protect this structure.
visit each city oncereturn to the startminimize total cost

TSP Brute Force: Permutation Enumeration

Fix city 0 as the start—rotating a cycle does not produce a genuinely new tour. Enumerate every ordering of the other $n-1$ cities, add the closing edge back to 0, and keep the least expensive cycle. This is conceptually simple and exact.

Remove rotational duplicates

Fix one start city, leaving $(n-1)!$ candidate orderings rather than $n!$ written sequences.

Remove reverse duplicates

For symmetric costs, a tour and its reverse cost the same. A canonical orientation leaves $(n-1)!/2$ distinct cycles for $n > 2$.

Evaluate every remaining tour

Sum its $n$ edges, including the return to the start. The smallest observed cost is provably optimal.

Why keep brute force?

It is an oracle for tiny instances. A slow, obviously correct solver is invaluable for testing faster exact algorithms and measuring heuristic error.

TSP Backtracking: Bounding & Pruning

Backtracking explores the same permutation tree but reasons about partial tours. It maintains an incumbent (the cheapest complete tour seen) and a lower bound on every unfinished branch. If a branch cannot beat the incumbent, discard it without listing its descendants.

A branch-and-bound search tree for TSP Partial tours branch from city A. A complete tour establishes cost eighty. A different partial branch with lower bound eighty-four is pruned. start A A → BA → CA → D complete tourcost = 80 ✓ lower bound = 84prune ✕ lower bound = 73continue Incumbent = 80Only bounds below 80 can improve it.
Pruning is safe only because the bound is optimistic: it never overstates the cheapest possible completion. If even that optimistic cost cannot beat 80, no descendant can.

Why Lower Bounds Matter

Using only the cost already paid is a valid bound when all edge weights are nonnegative, but it is weak. The implementations below use a stronger completion bound:

$$\text{partial cost} + \min(\text{last}\to R) + \operatorname{MST}(R) + \min(R\to\text{start})$$

Here $R$ is the set of unvisited cities. Any completion must enter $R$, connect all cities in $R$, and eventually leave $R$ for the start. A minimum spanning tree is no more expensive than the internal edges used by any such completion, making the expression optimistic and safe for pruning.

A good incumbent amplifies the bound

Run a fast heuristic first and use its tour cost as the initial incumbent. Candidate ordering then matters too: trying short edges early often discovers stronger incumbents sooner, though worst-case time remains factorial.

Sorted Edges (Cheapest Link)

Nearest Neighbor chooses the next city from the current endpoint. Cheapest Link instead scans all undirected edges from lightest to heaviest and accepts an edge only when two feasibility rules hold:

  • Degree rule: neither endpoint may already have two chosen tour edges.
  • Subtour rule: the edge may not close a cycle until it is the final, $n$th selected edge.

A disjoint-set structure tracks whether two endpoints are already in the same path fragment. It is not constructing the tour by itself; it is only answering the premature-cycle question efficiently.

Legal does not mean optimal

The rules guarantee one Hamiltonian cycle on a complete symmetric instance. They do not undo an early greedy choice that later forces an expensive edge.

Worked Example: Four Cities

Let AB=10, AC=15, AD=20, BD=25, CD=30, and BC=35. The sorted order is AB, AC, AD, BD, CD, BC.

EdgeDecisionReasonChosen edges
AB (10)AcceptBoth degrees are below 2; no cycle.AB
AC (15)AcceptA–B and A–C form one path fragment.AB, AC
AD (20)SkipA already has degree 2.AB, AC
BD (25)AcceptExtends the fragment through B.AB, AC, BD
CD (30)AcceptThis is the final edge, so closing the full cycle is allowed.AB, AC, BD, CD
Cheapest Link decisions on the four-city example Edges AB, AC, BD, and CD are accepted. Edge AD is rejected because A already has degree two. The result is the cycle A B D C A of cost eighty. ABCD AB 10 ✓AC 15 ✓BD 25 ✓CD 30 ✓AD 20: degree rule ✕ ResultA → B → D → C → Acost = 80
The globally third-cheapest edge, AD, is rejected because A already has two selected edges. The final CD edge closes the only full four-city cycle.

Complexity Analysis

MethodTimeExtra spaceGuarantee
Brute force$\Theta(n \cdot (n-1)!/2)$ for symmetric TSP$O(n)$ beyond generated permutationsExact optimum
Backtracking + bounds$O(n!)$ worst case; pruning is instance-dependent$O(n)$ search path plus bound workspaceExact optimum
Cheapest Link$O(n^2 \log n)$ for sorting all edges$O(n^2)$ for the edge listFeasible heuristic tour, not an optimality proof

Brute force spends time proving that nothing was missed. Backtracking seeks the same proof with fewer explored leaves. Cheapest Link stops asking for a proof and commits to one sequence of locally legal choices.

Implementation

from itertools import permutations
from math import isfinite


def validate_symmetric_tsp(dist):
    n = len(dist)
    if n < 3:
        raise ValueError("this simple-cycle implementation needs at least 3 cities")
    if any(len(row) != n for row in dist):
        raise ValueError("distance matrix must be square")
    for i in range(n):
        if dist[i][i] != 0:
            raise ValueError("diagonal distances must be zero")
        for j in range(i + 1, n):
            if not isfinite(dist[i][j]) or dist[i][j] < 0:
                raise ValueError("distances must be finite and nonnegative")
            if dist[i][j] != dist[j][i]:
                raise ValueError("distance matrix must be symmetric")
    return n


def tour_cost(dist, tour):
    return sum(dist[tour[i]][tour[i + 1]] for i in range(len(tour) - 1))


def tsp_brute_force(dist):
    """Exact enumeration with rotational and reversal symmetry removed."""
    n = validate_symmetric_tsp(dist)
    best_tour, best_cost = None, float("inf")

    for middle in permutations(range(1, n)):
        if middle[0] > middle[-1]:
            continue  # keep one orientation of each symmetric cycle
        tour = (0,) + middle + (0,)
        cost = tour_cost(dist, tour)
        if cost < best_cost:
            best_tour, best_cost = list(tour), cost
    return best_tour, best_cost


def sorted_edges_tsp(dist):
    """Return (ordered_tour, cost, selected_edges) using Cheapest Link."""
    n = validate_symmetric_tsp(dist)
    edges = sorted((dist[u][v], u, v) for u in range(n) for v in range(u + 1, n))
    parent, rank, degree = list(range(n)), [0] * n, [0] * n
    selected = []

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    def union(a, b):
        a, b = find(a), find(b)
        if a == b:
            return
        if rank[a] < rank[b]:
            a, b = b, a
        parent[b] = a
        if rank[a] == rank[b]:
            rank[a] += 1

    for weight, u, v in edges:
        if degree[u] == 2 or degree[v] == 2:
            continue
        final_edge = len(selected) == n - 1
        if find(u) == find(v) and not final_edge:
            continue
        selected.append((u, v, weight))
        degree[u] += 1
        degree[v] += 1
        union(u, v)
        if len(selected) == n:
            break

    if len(selected) != n or any(d != 2 for d in degree):
        raise RuntimeError("failed to construct a Hamiltonian cycle")

    neighbors = [[] for _ in range(n)]
    for u, v, _ in selected:
        neighbors[u].append(v)
        neighbors[v].append(u)
    tour, previous, current = [0], -1, 0
    while True:
        a, b = neighbors[current]
        nxt = a if a != previous else b
        if nxt == 0:
            break
        tour.append(nxt)
        previous, current = current, nxt
    tour.append(0)
    return tour, tour_cost(dist, tour), selected


def tsp_backtracking(dist):
    """Exact branch-and-bound using an MST-based completion lower bound."""
    n = validate_symmetric_tsp(dist)
    seed_tour, seed_cost, _ = sorted_edges_tsp(dist)
    best_tour, best_cost = seed_tour, seed_cost
    path, visited = [0], [False] * n
    visited[0] = True

    def mst_cost(vertices):
        if len(vertices) < 2:
            return 0
        remaining = set(vertices)
        root = remaining.pop()
        cheapest = {v: dist[root][v] for v in remaining}
        total = 0
        while remaining:
            v = min(remaining, key=cheapest.get)
            total += cheapest[v]
            remaining.remove(v)
            for w in remaining:
                cheapest[w] = min(cheapest[w], dist[v][w])
        return total

    def search(cost_so_far):
        nonlocal best_tour, best_cost
        remaining = [v for v in range(1, n) if not visited[v]]
        if not remaining:
            total = cost_so_far + dist[path[-1]][0]
            if total < best_cost:
                best_cost, best_tour = total, path[:] + [0]
            return

        lower_bound = (cost_so_far
                       + min(dist[path[-1]][v] for v in remaining)
                       + mst_cost(remaining)
                       + min(dist[v][0] for v in remaining))
        if lower_bound >= best_cost:
            return

        for city in sorted(remaining, key=lambda v: dist[path[-1]][v]):
            visited[city] = True
            path.append(city)
            search(cost_so_far + dist[path[-2]][city])
            path.pop()
            visited[city] = False

    search(0)
    return best_tour, best_cost


distances = [
    [0, 10, 15, 20],
    [10, 0, 35, 25],
    [15, 35, 0, 30],
    [20, 25, 30, 0],
]
print("Brute force:", tsp_brute_force(distances))
print("Backtracking:", tsp_backtracking(distances))
print("Cheapest Link:", sorted_edges_tsp(distances)[:2])
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <stdexcept>
#include <tuple>

using namespace std;

int find(vector<int>& parent, int x) {
    while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
    return x;
}

vector<pair<int,int>> sortedEdgesTSP(const vector<vector<int>>& dist, int& totalCost) {
    int n = static_cast<int>(dist.size());
    if (n < 3) throw invalid_argument("at least 3 cities are required");
    for (const auto& row : dist)
        if (static_cast<int>(row.size()) != n)
            throw invalid_argument("distance matrix must be square");
    vector<tuple<int,int,int>> edges;
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j < n; j++)
            edges.push_back({dist[i][j], i, j});
    sort(edges.begin(), edges.end());

    vector<int> degree(n, 0);
    vector<int> parent(n);
    iota(parent.begin(), parent.end(), 0);

    vector<pair<int,int>> tourEdges;
    totalCost = 0;

    for (auto& [w, u, v] : edges) {
        if ((int)tourEdges.size() == n) break;
        if (degree[u] < 2 && degree[v] < 2) {
            int ru = find(parent, u), rv = find(parent, v);
            if (ru != rv || (int)tourEdges.size() == n - 1) {
                tourEdges.push_back({u, v});
                degree[u]++; degree[v]++;
                parent[ru] = rv;
                totalCost += w;
            }
        }
    }
    if ((int)tourEdges.size() != n)
        throw runtime_error("failed to construct a Hamiltonian cycle");
    return tourEdges;
}

int main() {
    vector<vector<int>> dist = {
        {0, 10, 15, 20},
        {10, 0, 35, 25},
        {15, 35, 0, 30},
        {20, 25, 30, 0}
    };

    int totalCost = 0;
    auto tour = sortedEdgesTSP(dist, totalCost);

    cout << "Sorted Edges tour: ";
    for (auto& [u, v] : tour) cout << "(" << u << "-" << v << ") ";
    cout << "\nTotal cost: " << totalCost << endl;
    return 0;
}
import java.util.*;

public class TSPExactConstructive {
    static int find(int[] parent, int x) {
        while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
        return x;
    }

    public static List<int[]> sortedEdgesTSP(int[][] dist, int n) {
        if (n < 3 || dist.length != n)
            throw new IllegalArgumentException("at least 3 cities and an n x n matrix are required");
        for (int[] row : dist)
            if (row.length != n) throw new IllegalArgumentException("matrix must be square");
        List<int[]> edges = new ArrayList<>(); // {weight, u, v}
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++)
                edges.add(new int[]{dist[i][j], i, j});
        edges.sort((a, b) -> Integer.compare(a[0], b[0]));

        int[] degree = new int[n];
        int[] parent = new int[n];
        for (int i = 0; i < n; i++) parent[i] = i;

        List<int[]> tourEdges = new ArrayList<>();

        for (int[] e : edges) {
            int w = e[0], u = e[1], v = e[2];
            if (tourEdges.size() == n) break;
            if (degree[u] < 2 && degree[v] < 2) {
                int ru = find(parent, u), rv = find(parent, v);
                if (ru != rv || tourEdges.size() == n - 1) {
                    tourEdges.add(new int[]{u, v, w});
                    degree[u]++; degree[v]++;
                    parent[ru] = rv;
                }
            }
        }
        if (tourEdges.size() != n)
            throw new IllegalStateException("failed to construct a Hamiltonian cycle");
        return tourEdges;
    }

    public static void main(String[] args) {
        int n = 4;
        int[][] dist = {
            {0, 10, 15, 20},
            {10, 0, 35, 25},
            {15, 35, 0, 30},
            {20, 25, 30, 0}
        };

        List<int[]> tour = sortedEdgesTSP(dist, n);
        int total = 0;
        System.out.print("Sorted Edges tour: ");
        for (int[] e : tour) {
            System.out.print("(" + e[0] + "-" + e[1] + ") ");
            total += e[2];
        }
        System.out.println("\nTotal cost: " + total);
    }
}

Real-World Applications

Correctness oracle

Generate tiny random instances and compare a production solver's answer with brute force. Any cost mismatch reveals a bug or an invalid assumption.

Small exact routing

When the number of stops is genuinely small, backtracking with a good bound may certify an optimal route within the available time.

Heuristic initialization

A Cheapest-Link tour supplies an immediate incumbent for branch-and-bound or a starting point for 2-opt and 3-opt improvement.

Quality measurement

For small benchmark instances, the exact optimum lets you report a heuristic's percentage gap instead of judging a tour in isolation.

Exercises

1. Count distinct cycles

For $n=6$, begin with $6!$ written sequences. Remove rotations and reversals step by step to derive the number of distinct symmetric tours.

2. Audit a lower bound

At partial path A → B in the worked instance, compute the enter-$R$ + MST($R$) + return-to-A bound. Why may it be lower than every realizable completion?

3. Force a premature subtour

Construct five-city weights whose three cheapest edges form a triangle. Trace how the disjoint-set check rejects the triangle-closing edge.

4. Measure, then improve

Generate symmetric Euclidean instances, compare Cheapest Link with the optimum for small $n$, then add 2-opt and report how often the gap shrinks.

Limitations

  • Factorial worst case: backtracking can still visit essentially the full permutation tree when bounds are weak or many tours have similar costs.
  • Heuristic quality depends on assumptions: on unrestricted weighted complete graphs, Cheapest Link can be arbitrarily worse than optimal. Metric structure helps analysis, but it still lacks Christofides' constant $3/2$ guarantee.
  • This implementation is symmetric: asymmetric TSP needs directed-edge degree constraints and different subtour bookkeeping.
  • A matrix assumes completeness: missing routes, time windows, capacities, and multiple vehicles turn the problem into richer routing variants.
  • Floating-point comparisons: real geographic costs may require tolerances and deterministic tie-breaking.

Practical scale is not a fixed city count

Whether exact backtracking is feasible depends on bound strength, cost structure, incumbent quality, symmetry, and hardware—not a universal threshold such as “15 cities” or “20 cities.” Always enforce a time or node budget in production.