Routes that visit every node once
A directed graph has n nodes numbered 0 to n − 1; each pair
[u, v] in edges is an edge from u to v. Return the number of
Hamiltonian paths from source to destination: paths that start at
source, end at destination, and visit every node of the graph exactly once.
Examples
| Input | Output | Why |
|---|---|---|
n = 4, edges = [[0,1],[0,2],[1,2],[2,1],[1,3],[2,3],[0,3]], source = 0, destination = 3 | 2 | 0 → 1 → 2 → 3 and 0 → 2 → 1 → 3. The edge 0 → 3 skips nodes. |
n = 3, edges = [[0,2],[2,1]], source = 0, destination = 2 | 0 | The only route that visits all three nodes ends at 1. |
n = 1, edges = [], source = 0, destination = 0 | 1 | A single node is a path of length zero. |
Constraints
- 1 ≤ n ≤ 12
- No self-loops or repeated edges
Hints
Hint 1
Backtracking: extend a path from the source through unvisited nodes and count it when it has n nodes and ends at the destination.
Hint 2
Two partial paths that have visited the same set of nodes and stand at the same node have the same number of completions. Memoise on (set of visited nodes, current node).
Hint 3
Represent the visited set as a bitmask: dp[mask][v] = number of paths from the source covering exactly mask and ending at v.
Where people lose marks
- Counting paths that reach the destination before all nodes are visited.
- Letting a path pass through the destination and continue: the destination must be last.
- Using plain backtracking for n = 12 on a dense graph: up to 10! partial orderings.
Dry run on 4,[[0,1],[0,2],[1,2],[2,1],[1,3],[2,3],[0,3]],0,3
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 10current path00paths foundnone yet0
Start at 0, the only node on the path.
-
Step 2 / 10current path0011paths foundnone yet0
Extend the path to 1.
-
Step 3 / 10current path001122paths foundnone yet0
Extend the path to 2.
-
Step 4 / 10current path00112233paths found01230
Reached 3 along 0 → 1 → 2 → 3: all 4 nodes visited, count it.
-
Step 5 / 10current path001132paths found012300131
Reached 3 along 0 → 1 → 3: only 3 of 4 nodes visited, do not count it.
-
Step 6 / 10current path0021paths found012300131
Extend the path to 2.
-
Step 7 / 10current path002112paths found012300131
Extend the path to 1.
-
Step 8 / 10current path00211233paths found01230013102132
Reached 3 along 0 → 2 → 1 → 3: all 4 nodes visited, count it.
-
Step 9 / 10current path002132paths found012300131021320233
Reached 3 along 0 → 2 → 3: only 3 of 4 nodes visited, do not count it.
-
Step 10 / 10current path0031paths found012300131021320233034
Reached 3 along 0 → 3: only 2 of 4 nodes visited, do not count it.
How to think about it
Backtracking over simple paths (the pattern of this module) answers the question directly, but it explores every ordering, up to (n − 2)! of them. The search wastes most of that: what matters for the rest of a path is only which nodes have been used and where the path stands, not the order it took.
So count with dynamic programming over subsets. Let dp[mask][v] be the number of paths that start at
the source, visit exactly the nodes in mask, and end at v. Start with
dp[{source}][source] = 1. For every mask in increasing order and every v in it, each edge v → w to a node
w outside the mask adds dp[mask][v] to dp[mask ∪ {w}][w]. The answer is
dp[all nodes][destination]. There are 2ⁿ · n states with up to n moves each: for n = 12, about
600,000 steps.
Complexity. Time O(2ⁿ · n²) · Space O(2ⁿ · n)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def count_hamiltonian(n, edges, source, destination):
out = [[] for _ in range(n)]
for u, v in edges:
out[u].append(v)
full = (1 << n) - 1
# dp[mask][v]: paths from source covering exactly mask, ending at v
dp = [[0] * n for _ in range(1 << n)]
dp[1 << source][source] = 1
for mask in range(1 << n):
if not mask >> source & 1:
continue # every path contains the source
for v in range(n):
ways = dp[mask][v]
if ways == 0 or v == destination and mask != full:
continue # the destination must be the last node
for w in out[v]:
if not mask >> w & 1:
dp[mask | 1 << w][w] += ways
return dp[full][destination]#include <vector>
long long solve(int n, const std::vector<std::vector<int>>& edges, int source, int destination) {
std::vector<std::vector<int>> out(n);
for (const auto& e : edges) out[e[0]].push_back(e[1]);
const int full = (1 << n) - 1;
// dp[mask][v]: paths from source covering exactly mask, ending at v
std::vector<std::vector<long long>> dp(1 << n, std::vector<long long>(n, 0));
dp[1 << source][source] = 1;
for (int mask = 0; mask <= full; ++mask) {
if (!(mask >> source & 1)) continue;
for (int v = 0; v < n; ++v) {
long long ways = dp[mask][v];
if (ways == 0 || (v == destination && mask != full)) continue; // destination comes last
for (int w : out[v])
if (!(mask >> w & 1)) dp[mask | 1 << w][w] += ways;
}
}
return dp[full][destination];
}import java.util.*;
class Solution {
public long countHamiltonian(int n, int[][] edges, int source, int destination) {
List<List<Integer>> out = new ArrayList<>();
for (int i = 0; i < n; i++) out.add(new ArrayList<>());
for (int[] e : edges) out.get(e[0]).add(e[1]);
int full = (1 << n) - 1;
long[][] dp = new long[1 << n][n]; // paths covering mask, ending at v
dp[1 << source][source] = 1;
for (int mask = 0; mask <= full; mask++) {
if ((mask >> source & 1) == 0) continue;
for (int v = 0; v < n; v++) {
long ways = dp[mask][v];
if (ways == 0 || (v == destination && mask != full)) continue; // destination comes last
for (int w : out.get(v))
if ((mask >> w & 1) == 0) dp[mask | 1 << w][w] += ways;
}
}
return dp[full][destination];
}
}function countHamiltonian(n, edges, source, destination) {
const out = Array.from({ length: n }, () => []);
for (const [u, v] of edges) out[u].push(v);
const full = (1 << n) - 1;
// dp[mask * n + v]: paths from source covering exactly mask, ending at v
const dp = new Array((1 << n) * n).fill(0);
dp[(1 << source) * n + source] = 1;
for (let mask = 0; mask <= full; mask++) {
if (!(mask >> source & 1)) continue;
for (let v = 0; v < n; v++) {
const ways = dp[mask * n + v];
if (ways === 0 || (v === destination && mask !== full)) continue; // destination comes last
for (const w of out[v]) if (!(mask >> w & 1)) dp[(mask | 1 << w) * n + w] += ways;
}
}
return dp[full * n + destination];
}fn solve(n: i64, edges: &[[i64; 2]], source: i64, destination: i64) -> i64 {
let (n, s, d) = (n as usize, source as usize, destination as usize);
let mut out = vec![Vec::new(); n];
for e in edges {
out[e[0] as usize].push(e[1] as usize);
}
let full = (1usize << n) - 1;
// dp[mask][v]: paths from source covering exactly mask, ending at v
let mut dp = vec![vec![0i64; n]; 1 << n];
dp[1 << s][s] = 1;
for mask in 0..=full {
if mask >> s & 1 == 0 {
continue;
}
for v in 0..n {
let ways = dp[mask][v];
if ways == 0 || (v == d && mask != full) {
continue; // destination comes last
}
for &w in &out[v] {
if mask >> w & 1 == 0 {
dp[mask | 1 << w][w] += ways;
}
}
}
}
dp[full][d]
}import Data.Bits (setBit, testBit, (.|.), shiftL)
import qualified Data.Map.Strict as M
solve :: Int -> [[Int]] -> Int -> Int -> Int
solve n edges source dest = M.findWithDefault 0 (full, dest) table
where
full = (1 `shiftL` n) - 1 :: Int
out = M.fromListWith (++) [(u, [v]) | [u, v] <- edges]
-- table: (mask, v) -> number of paths from source covering exactly mask and ending at v
table = foldl step (M.singleton (setBit 0 source, source) 1) [0 .. full]
step t mask = foldl (extend mask) t [0 .. n - 1]
extend mask t v = case M.lookup (mask, v) t of
Just ways | not (v == dest && mask /= full) ->
foldl (\acc w -> M.insertWith (+) (mask .|. setBit 0 w, w) ways acc) t
[w | w <- M.findWithDefault [] v out, not (testBit mask w)]
_ -> t
Variants to try
- The cheapest Hamiltonian path in a weighted graph: replace the count by a minimum (the travelling salesman recurrence).
- Hamiltonian cycles: count paths from the source covering all nodes whose last node has an edge back to the source.
- Return one Hamiltonian path, or report that none exists, by storing a predecessor per state.
Next problems
Every route from source to target
List every simple path from node 0 to node n − 1 in a directed graph. Search depth-first and take each node off the path on the way back.
Open the problem → Medium20 minRoutes with an exact total weight
Count the paths from source to destination, never repeating a node, whose edge weights add up to exactly the target.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem →Write a solution and run it against the real test table.
Keep going
Every route from source to target
List every simple path from node 0 to node n − 1 in a directed graph. Search depth-first and take each node off the path on the way back.
Open the problem → Medium20 minRoutes with an exact total weight
Count the paths from source to destination, never repeating a node, whose edge weights add up to exactly the target.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.