Back to Graph Theory Series

Hamiltonian Cycle Backtracking

October 4, 2026 Wasil Zafar 28 min read

Build a simple path one vertex at a time. When a partial route cannot possibly visit every remaining vertex and return to its start, undo the last choice immediately instead of exploring every completion of that mistake.

Contents

  1. A Bit of History
  2. Cycle vs. Path
  3. Choose, Explore, Undo
  4. Visual Path Walkthrough
  5. The Search Tree
  6. Safe Pruning Rules
  7. Necessary vs. Sufficient
  8. Implementation
  9. Complexity & Alternatives
  10. Hamiltonian vs. Eulerian
  11. Real-World Applications
  12. Exercises
  13. Limitations

A Bit of History

From a Victorian puzzle to NP-completeness

1857William Rowan Hamilton creates the Icosian Game: find a cycle through all 20 vertices of a dodecahedron.
1972Richard Karp includes Hamiltonian Cycle among his original 21 NP-complete problems.
TodayExact solvers combine backtracking with propagation, dynamic programming, SAT, ILP, and problem-specific structure.

Hamiltonian Cycle, Hamiltonian Path, and TSP

Hamiltonian path

Visits every vertex exactly once; its two endpoints need not be adjacent.

Hamiltonian cycle

Visits every vertex exactly once and then closes back to the first vertex.

Traveling salesperson

Optimizes the total weight of a Hamiltonian cycle rather than merely asking whether one exists.

For a simple undirected graph, a Hamiltonian cycle has at least three vertices. The repeated start vertex is normally shown only to make closure explicit: $[0,1,2,3,0]$ represents three distinct visited vertices plus the return edge.

Museum-tour analogy. Enter each gallery exactly once, using only open doors, and return to the lobby. A partial tour can look promising yet trap you in a gallery whose remaining doors lead only to rooms already visited.

Choose, Explore, Undo

  1. Break rotational symmetry. Fix vertex 0 as the start; any Hamiltonian cycle can be rotated to begin there.
  2. Choose. From the current endpoint, append one adjacent unvisited vertex and mark it used.
  3. Explore. Recurse. If all vertices are present, accept only when the endpoint connects to the start.
  4. Undo. If the branch fails, remove the last vertex, clear its visited flag, and try the next candidate.
  5. Stop or exhaust. Return the first cycle found, or report failure after every candidate prefix has been ruled out.

Visual Path Walkthrough

Both panels use the same six-vertex graph: a hexagon plus chords $0$–$2$ and $2$–$4$. Choosing neighbor 1 leads directly around the outer cycle. Choosing chord $0$–$2$, then $2$–$4$, then $4$–$3$ traps the endpoint because both neighbors of 3 are already used.

Promising and dead-end Hamiltonian path prefixes The left panel shows a complete Hamiltonian cycle around a six-vertex graph. The right panel shows a partial path using two chords that becomes trapped at vertex 3 while vertices 1 and 5 remain unvisited. Promising branchDead-end branch 0 → 1 → 2 → 3 → 4 → 5 → 00 → 2 → 4 → 3 · vertices 1 and 5 remain 012345023415 All six vertices + closing edge = success Endpoint 3 has no unvisited neighbor → undo
Prefixes matter. Backtracking never evaluates complete permutations that share an already impossible prefix. Once vertex 3 is trapped, every hypothetical continuation of $[0,2,4,3]$ is discarded together.

The Search Tree, Not Just the Graph

The algorithm explores a second structure: a tree whose nodes are partial paths. A graph vertex may appear in many search-tree nodes because it can be reached under different visited sets. Backtracking is depth-first traversal of this implicit tree.

Backtracking recursion tree for Hamiltonian-cycle search The recursion tree starts at path zero. One branch extends around the outer cycle and succeeds. Another chooses chords, reaches a dead end, and returns to the earlier decision point. Each box is a partial path UNDO [0][0, 1][0, 2][0, 1, 2][0,1,2,3,4,5,0] ✓[0, 2, 4][0, 2, 4, 3] ✕[0, 2, 1] … choose 1choose 2eventual closure Choose changes path + visited · Undo restores both exactly.
State restoration is the algorithm. After a failed child returns, the parent must see precisely the state it had before that choice. Forgetting either path.pop() or visited[candidate] = false corrupts every later branch.

Safe Pruning Rules

RuleWhen it rejectsCost / value
AdjacencyThe next vertex is not adjacent to the current endpoint.Constant-time with a matrix; fundamental pruning.
VisitedThe candidate already appears in the path.Prevents repetition in constant time.
Initial degreeSome vertex has degree below 2 in a simple cycle instance.Cheap necessary-condition precheck.
Connectivity / cut vertexThe graph is disconnected, has a bridge, or has an articulation point.A Hamiltonian cycle on $V\ge3$ is 2-vertex-connected.
Remaining frontierUnvisited vertices plus the two path endpoints cannot remain connected or satisfy future degree needs.Stronger but costs extra work per search node.
Candidate orderingDoes not reject; tries the most constrained neighbor first.Can expose failure earlier without changing correctness.

Pruning must prove impossibility

A heuristic ordering may be wrong about which branch is promising, so it may only change search order. A pruning rule deletes a branch permanently and must therefore be a necessary condition for every completion of that partial path.

Necessary Conditions vs. Sufficient Theorems

Necessary checks can quickly prove no: connectivity, minimum degree at least two, absence of bridges and articulation points, and—in a bipartite graph—equal partition sizes. None of these alone proves a cycle exists.

Sufficient theorems can quickly prove yes. For a simple graph with $n\ge3$, Dirac’s condition $deg(v)\ge n/2$ for every vertex guarantees Hamiltonicity. Ore’s condition $deg(u)+\deg(v)\ge n$ for every nonadjacent pair also guarantees it. Graphs can be Hamiltonian without satisfying either condition, so failure of the theorem’s premise means “unknown,” not “no.”

Core intuition: backtracking is controlled proof by cases. Each partial path either closes successfully, has a rigorously impossible future, or generates smaller cases by choosing its next vertex.

Implementation

The following versions target simple undirected graphs represented by an adjacency matrix. They fix start vertex 0, restore state on every failed branch, reject graphs with fewer than three vertices, and return the repeated start vertex so the closing edge is visible.

def hamiltonian_cycle(n, adj_matrix):
    """
    n: number of vertices
    adj_matrix: n x n boolean adjacency matrix
    Returns a Hamiltonian cycle as a list of vertices, or None if none exists.
    """
    if n < 3:
        return None  # simple undirected cycles need at least 3 vertices

    path = [0]  # fix the starting vertex (WLOG)
    visited = [False] * n
    visited[0] = True

    def backtrack():
        if len(path) == n:
            # All vertices placed; check the closing edge back to start
            return adj_matrix[path[-1]][path[0]]

        last = path[-1]
        for candidate in range(n):
            if not visited[candidate] and adj_matrix[last][candidate]:
                path.append(candidate)
                visited[candidate] = True

                if backtrack():
                    return True

                # Backtrack: undo this choice
                path.pop()
                visited[candidate] = False

        return False

    if backtrack():
        return path + [path[0]]  # show the closing edge explicitly
    return None

# Example: pentagon (0-1-2-3-4-0) plus diagonal 0-2
n = 5
adj = [[False] * n for _ in range(n)]
edges = [(0,1), (1,2), (2,3), (3,4), (4,0), (0,2)]
for u, v in edges:
    adj[u][v] = adj[v][u] = True

result = hamiltonian_cycle(n, adj)
print("Hamiltonian cycle:", result)
#include <iostream>
#include <vector>

using namespace std;

bool backtrack(int n, vector<vector<bool>>& adj, vector<int>& path, vector<bool>& visited) {
    if ((int)path.size() == n) {
        return adj[path.back()][path[0]];
    }

    int last = path.back();
    for (int candidate = 0; candidate < n; candidate++) {
        if (!visited[candidate] && adj[last][candidate]) {
            path.push_back(candidate);
            visited[candidate] = true;

            if (backtrack(n, adj, path, visited)) return true;

            path.pop_back();
            visited[candidate] = false;
        }
    }
    return false;
}

vector<int> hamiltonianCycle(int n, vector<vector<bool>>& adj) {
    if (n < 3) return {};
    vector<int> path = {0};
    vector<bool> visited(n, false);
    visited[0] = true;

    if (backtrack(n, adj, path, visited)) {
        path.push_back(path[0]);
        return path;
    }
    return {};
}

int main() {
    int n = 5;
    vector<vector<bool>> adj(n, vector<bool>(n, false));
    vector<pair<int,int>> edges = {{0,1},{1,2},{2,3},{3,4},{4,0},{0,2}};
    for (auto& e : edges) {
        adj[e.first][e.second] = adj[e.second][e.first] = true;
    }

    vector<int> result = hamiltonianCycle(n, adj);
    cout << "Hamiltonian cycle: ";
    for (int v : result) cout << v << " ";
    cout << endl;
    return 0;
}
import java.util.*;

public class HamiltonianCycle {
    static boolean backtrack(int n, boolean[][] adj, List<Integer> path, boolean[] visited) {
        if (path.size() == n) {
            return adj[path.get(path.size() - 1)][path.get(0)];
        }

        int last = path.get(path.size() - 1);
        for (int candidate = 0; candidate < n; candidate++) {
            if (!visited[candidate] && adj[last][candidate]) {
                path.add(candidate);
                visited[candidate] = true;

                if (backtrack(n, adj, path, visited)) return true;

                path.remove(path.size() - 1);
                visited[candidate] = false;
            }
        }
        return false;
    }

    public static List<Integer> solve(int n, boolean[][] adj) {
        if (n < 3) return null;
        List<Integer> path = new ArrayList<>();
        path.add(0);
        boolean[] visited = new boolean[n];
        visited[0] = true;

        if (backtrack(n, adj, path, visited)) {
            path.add(path.get(0));
            return path;
        }
        return null;
    }

    public static void main(String[] args) {
        int n = 5;
        boolean[][] adj = new boolean[n][n];
        int[][] edges = {{0,1},{1,2},{2,3},{3,4},{4,0},{0,2}};
        for (int[] e : edges) {
            adj[e[0]][e[1]] = true;
            adj[e[1]][e[0]] = true;
        }

        List<Integer> result = solve(n, adj);
        System.out.println("Hamiltonian cycle: " + result);
    }
}

Complexity and Exact Alternatives

Fixing the start removes rotational duplicates, leaving at most $(V-1)!$ vertex orders. Backtracking can still explore a factorial number of prefixes in adversarial graphs:

$$T(V)=O(V!), \qquad S_{\mathrm{aux}}(V)=O(V)$$

The auxiliary space covers the path, visited set, and recursion stack. The shown adjacency matrix itself occupies $O(V^2)$. A memoized subset dynamic program—often associated with Held–Karp—trades memory for a better exact bound of roughly $O(V^2 2^V)$ time and $O(V2^V)$ space.

ApproachTypical strengthMain cost
BacktrackingSimple, low memory, benefits strongly from structure and pruning.Factorial worst case; performance depends on ordering.
Subset DPPredictable $2^V$-scale exact method for modest $V$.Exponential memory.
SAT / ILP / CPPowerful propagation and easy addition of side constraints.Requires a solver and careful formulation.
HeuristicsCan find good tours quickly in large optimization instances.Cannot certify nonexistence unless embedded in an exact method.

Hamiltonian vs. Eulerian: Do Not Mix Them Up

QuestionHamiltonian cycleEulerian circuit
Visit exactly onceVerticesEdges
Vertices may repeatNo, except the repeated start that closes the cycle.Yes.
General decision testNP-complete; no known polynomial-time test.$O(V+E)$ via connectivity and even-degree conditions.
Constructive algorithmBacktracking, subset DP, or exact solver.Hierholzer’s algorithm in linear time.

Real-World Applications

Puzzles and tours

Model Knight’s tours, board traversals, and route puzzles as vertex-once searches.

Sequencing constraints

Ask whether tasks or states can be ordered once each under allowed-transition rules.

Routing foundations

Hamiltonian cycles form the feasibility skeleton beneath TSP-style weighted tour optimization.

Real routing and drilling systems usually optimize distance on a weighted graph, making them TSP variants rather than plain Hamiltonian-cycle decision problems. Genome assembly also largely moved from fragment-overlap Hamiltonian formulations toward Eulerian walks in de Bruijn graphs. These connections are best understood as modeling history and shared structure, not identical problems.

Exercises

Explore the search space

  1. Trace the dead branch $[0,2,4,3]$ and list exactly which candidate checks fail.
  2. Modify the code to return a Hamiltonian path by removing only the final closing-edge test.
  3. Add the safe initial checks for connectivity and minimum degree two. Test a path graph and a star.
  4. Order candidates by the number of their currently unvisited neighbors. Compare recursive-call counts on several graphs.
  5. Challenge: implement subset DP with state $(S,v)$ meaning “a path from 0 visits exactly $S$ and ends at $v$.”

Hint: instrument the recursive function with a counter; elapsed time alone is noisy when comparing pruning strategies.

Limitations

Exact search still scales exponentially

  • No polynomial guarantee: pruning can transform practical performance without changing the NP-complete worst case.
  • Recursion depth: the path can be $V$ vertices long; iterative state or stack configuration may be needed.
  • Representation cost: matrices give fast lookups but consume $O(V^2)$ memory.
  • Directed graphs: adjacency and closure become directional; necessary degree checks must use in- and out-degree.
  • Enumeration: finding one cycle is easier than listing all cycles without rotational and reversal duplicates.

Use backtracking when graphs are small, structured, or aggressively constrained. For larger exact instances, compare subset DP and modern SAT/ILP/constraint-programming solvers before committing to a hand-built search.