Skip to content

Hamiltonian Bitmask DP

TSP bitmask DP: state is (visited mask, current city)

Hamiltonian path visits every node exactly once

Euler paths visit every edge. Hamiltonian paths visit every vertex. That one-word swap -- edge to vertex -- rockets the problem from polynomial to NP-hard. No one knows a polynomial algorithm. But when \(N \le 20\), bitmask DP solves it in \(O(2^N \cdot N^2)\), which is fast enough.


Euler vs Hamilton: The Complexity Gap

Euler Hamilton
Visit every... Edge Vertex
Existence check Degree conditions, \(O(V + E)\) NP-complete
Finding the path Hierholzer's, \(O(V + E)\) NP-hard
Best known for small \(N\) \(O(V + E)\) \(O(2^N \cdot N^2)\) bitmask DP

The key difference: edges can be consumed independently, but vertices interact. Visiting vertex \(v\) affects which other vertices are reachable. You need to track the entire set of visited vertices.

Key insight. Bitmask DP works because the state is "which vertices have I visited?" plus "where am I now?" A bitmask of \(N\) bits encodes the visited set. There are \(2^N \cdot N\) states total. Each transition checks one edge. Total: \(O(2^N \cdot N^2)\).


The DP Formulation

Define:

\[dp[\text{mask}][v] = \text{number of paths that visit exactly the vertices in mask and end at } v\]

Base case: \(dp[1 \ll s][s] = 1\) where \(s\) is the starting vertex.

Transition: For each state \((mask, u)\) with \(dp[mask][u] > 0\), for each neighbor \(v\) of \(u\) such that \(v \notin mask\):

\[dp[mask \mid (1 \ll v)][v] \mathrel{+}= dp[mask][u]\]

Answer: \(dp[(1 \ll N) - 1][t]\) where \(t\) is the target vertex. The mask \((1 \ll N) - 1\) has all \(N\) bits set, meaning all vertices visited.


CSES Hamiltonian Flights

Problem. Given a directed graph with \(N\) cities (\(N \le 20\)) and \(M\) flights, count the number of routes from city 1 to city \(N\) that visit every city exactly once. Output modulo \(10^9 + 7\).

#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    vector<vector<int>> adj(n);
    for (int i = 0; i < m; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        adj[a - 1].push_back(b - 1);
    }
    vector<vector<long long>> dp(1 << n, vector<long long>(n, 0));
    dp[1][0] = 1; // start at node 0, mask = {0}

    for (int mask = 1; mask < (1 << n); mask++) {
        for (int u = 0; u < n; u++) {
            if (!(mask & (1 << u))) continue;
            if (!dp[mask][u]) continue;
            for (int v : adj[u]) {
                if (mask & (1 << v)) continue;
                int nm = mask | (1 << v);
                dp[nm][v] = (dp[nm][v] + dp[mask][u]) % MOD;
            }
        }
    }
    printf("%lld\n", dp[(1 << n) - 1][n - 1]);
}

Trace on a Small Example

Graph: 4 nodes. Edges: 0→1, 0→2, 1→2, 1→3, 2→3. Start = 0, end = 3.

Masks are 4-bit: bit \(i\) means node \(i\) is visited. Mask 0001 = {0}, mask 1111 = {0,1,2,3}.

Step mask (binary) Node dp value Transitions
Init 0001 0 1 → node 1 (mask 0011), → node 2 (mask 0101)
1 0011 1 1 → node 2 (mask 0111), → node 3 (mask 1011)
2 0101 2 1 → node 3 (mask 1101)
3 0111 2 1 → node 3 (mask 1111)
4 1011 3 1 no unvisited neighbors (3 is the end)
5 1101 3 1 partial path, node 1 not visited -- stuck
6 1111 3 1 answer

Wait -- mask 1101 has nodes {0, 2, 3} visited but not node 1. Since we end at 3 and node 1 isn't visited, this path doesn't reach the full mask. So it contributes nothing.

Answer: 1 (the path 0 → 1 → 2 → 3).

Gotcha. Pruning matters for speed. If bit 0 is not set in the mask, skip it -- we always start at node 0. If the full mask doesn't include the end node, skip it. These checks prevent wasting time on unreachable states.


Optimization: Pruning Dead States

Two easy prunings cut runtime significantly:

  1. Start node must be in the mask. Skip any mask where bit 0 is unset.
  2. End node appears only in the full mask. If \(v = N-1\) and mask \(\ne (1 \ll N) - 1\), don't transition into that state.
for (int mask = 1; mask < (1 << n); mask++) {
    if (!(mask & 1)) continue; // node 0 must be visited
    for (int u = 0; u < n; u++) {
        if (!(mask & (1 << u)) || !dp[mask][u]) continue;
        for (int v : adj[u]) {
            if (mask & (1 << v)) continue;
            if (v == n - 1 && __builtin_popcount(mask) != n - 1) continue;
            dp[mask | (1 << v)][v] = (dp[mask | (1 << v)][v] + dp[mask][u]) % MOD;
        }
    }
}

CSES Knight's Tour: Backtracking + Warnsdorff's

Problem. Find a sequence of knight moves on an \(8 \times 8\) board that visits every square exactly once, starting from a given square.

This is a Hamiltonian path on the knight's-move graph. With 64 nodes, bitmask DP is impossible (\(2^{64}\) states). Instead, use backtracking with Warnsdorff's heuristic: always move to the square with the fewest onward moves.

#include <bits/stdc++.h>
using namespace std;

int dx[] = {-2,-2,-1,-1,1,1,2,2};
int dy[] = {-1,1,-2,2,-2,2,-1,1};
int board[8][8];

int onward(int x, int y) {
    int cnt = 0;
    for (int d = 0; d < 8; d++) {
        int nx = x + dx[d], ny = y + dy[d];
        if (nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && board[nx][ny] == 0)
            cnt++;
    }
    return cnt;
}

bool solve(int x, int y, int move) {
    board[x][y] = move;
    if (move == 64) return true;
    vector<tuple<int,int,int>> next;
    for (int d = 0; d < 8; d++) {
        int nx = x + dx[d], ny = y + dy[d];
        if (nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && board[nx][ny] == 0) {
            next.push_back({onward(nx, ny), nx, ny});
        }
    }
    sort(next.begin(), next.end()); // Warnsdorff: fewest onward first
    for (auto [deg, nx, ny] : next) {
        if (solve(nx, ny, move + 1)) return true;
    }
    board[x][y] = 0;
    return false;
}

int main() {
    int r, c;
    scanf("%d%d", &r, &c);
    memset(board, 0, sizeof(board));
    solve(r - 1, c - 1, 1);
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            printf("%d%c", board[i][j], " \n"[j == 7]);
        }
    }
}

Key insight. Warnsdorff's heuristic works because moving to the most constrained square first avoids creating isolated dead ends. It's a greedy heuristic, not a guarantee, but on the standard \(8 \times 8\) board it finds a solution almost instantly with no backtracking needed.


Connection to TSP

The Traveling Salesman Problem asks for the minimum-weight Hamiltonian cycle. The bitmask DP is identical in structure:

\[dp[mask][v] = \min \text{ cost to visit exactly the vertices in mask, ending at } v\]

Transition: \(dp[mask \mid (1 \ll v)][v] = \min(dp[mask][v], dp[mask][u] + w(u, v))\).

Same \(O(2^N \cdot N^2)\) time. Same \(N \le 20\) constraint. The only difference is min instead of count and adding edge weights.


Complexity Summary

Approach Time Space \(N\) limit
Bitmask DP \(O(2^N \cdot N^2)\) \(O(2^N \cdot N)\) ~20
Brute-force permutations \(O(N!)\) \(O(N)\) ~12
Backtracking + heuristic Varies \(O(N^2)\) Problem-dependent

For \(N = 20\): \(2^{20} \cdot 400 \approx 4 \times 10^8\). Tight but feasible with fast constant factors.


Problems

Problem Link Difficulty
CSES Hamiltonian Flights cses.fi/problemset/task/1690 Medium
CSES Knight's Tour cses.fi/problemset/task/1202 Hard