1070-hamiltonian-path — Hamiltonian Path
Tutorial
The Problem
A Hamiltonian path visits every vertex of a graph exactly once. Unlike Euler paths (which traverse every edge once), Hamiltonian paths are NP-complete — no polynomial algorithm is known. The Traveling Salesman Problem (TSP) is a weighted Hamiltonian path problem and is one of the most famous NP-hard problems in computer science.
The backtracking approach tries to extend the path one vertex at a time, backtracking when a dead end is reached. For small graphs (up to ~20 vertices), this is practical.
🎯 Learning Outcomes
Code Example
#![allow(clippy::all)]
// 1070: Hamiltonian Path — Backtracking
// Approach 1: Adjacency matrix backtracking (fixed start = 0)
fn hamiltonian_path(adj: &[Vec<i32>]) -> Option<Vec<usize>> {
let n = adj.len();
let mut path = vec![0usize; n];
let mut visited = vec![false; n];
path[0] = 0;
visited[0] = true;
fn solve(pos: usize, adj: &[Vec<i32>], path: &mut Vec<usize>, visited: &mut Vec<bool>) -> bool {
let n = adj.len();
if pos == n {
return true;
}
for v in 0..n {
if !visited[v] && adj[path[pos - 1]][v] == 1 {
path[pos] = v;
visited[v] = true;
if solve(pos + 1, adj, path, visited) {
return true;
}
visited[v] = false;
}
}
false
}
if solve(1, adj, &mut path, &mut visited) {
Some(path)
} else {
None
}
}
// Approach 2: Try all starting vertices
fn hamiltonian_path_any(adj: &[Vec<i32>]) -> Option<Vec<usize>> {
let n = adj.len();
for start in 0..n {
let mut path = vec![0usize; n];
let mut visited = vec![false; n];
path[0] = start;
visited[start] = true;
fn solve(
pos: usize,
adj: &[Vec<i32>],
path: &mut Vec<usize>,
visited: &mut Vec<bool>,
) -> bool {
let n = adj.len();
if pos == n {
return true;
}
for v in 0..n {
if !visited[v] && adj[path[pos - 1]][v] == 1 {
path[pos] = v;
visited[v] = true;
if solve(pos + 1, adj, path, visited) {
return true;
}
visited[v] = false;
}
}
false
}
if solve(1, adj, &mut path, &mut visited) {
return Some(path);
}
}
None
}
// Approach 3: Bitmask DP for Hamiltonian path existence (O(2^n * n^2))
fn hamiltonian_exists_dp(adj: &[Vec<i32>]) -> bool {
let n = adj.len();
if n == 0 {
return true;
}
// dp[mask][i] = can we reach node i having visited exactly the nodes in mask?
let mut dp = vec![vec![false; n]; 1 << n];
for i in 0..n {
dp[1 << i][i] = true;
}
for mask in 1..(1 << n) {
for u in 0..n {
if !dp[mask][u] {
continue;
}
if mask & (1 << u) == 0 {
continue;
}
for v in 0..n {
if mask & (1 << v) == 0 && adj[u][v] == 1 {
dp[mask | (1 << v)][v] = true;
}
}
}
}
let full = (1 << n) - 1;
(0..n).any(|i| dp[full][i])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complete_graph() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
let path = hamiltonian_path(&adj).unwrap();
assert_eq!(path.len(), 4);
}
#[test]
fn test_path_graph() {
let adj = vec![
vec![0, 1, 0, 0],
vec![1, 0, 1, 0],
vec![0, 1, 0, 1],
vec![0, 0, 1, 0],
];
let path = hamiltonian_path(&adj).unwrap();
assert_eq!(path.len(), 4);
}
#[test]
fn test_any_start() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
assert!(hamiltonian_path_any(&adj).is_some());
}
#[test]
fn test_bitmask_dp() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
assert!(hamiltonian_exists_dp(&adj));
// Disconnected graph
let adj2 = vec![vec![0, 0, 0], vec![0, 0, 0], vec![0, 0, 0]];
assert!(!hamiltonian_exists_dp(&adj2));
}
}Key Differences
true/false, naturally exiting; OCaml uses a ref to propagate success upward.OCaml Approach
let hamiltonian_path adj =
let n = Array.length adj in
let path = Array.make n 0 in
let visited = Array.make n false in
visited.(0) <- true;
let rec solve pos =
if pos = n then Some (Array.to_list path)
else
let result = ref None in
for v = 0 to n - 1 do
if !result = None && not visited.(v) && adj.(path.(pos-1)).(v) = 1 then begin
path.(pos) <- v; visited.(v) <- true;
result := solve (pos + 1);
if !result = None then visited.(v) <- false
end
done;
!result
in
solve 1
Structurally identical. OCaml's ref result manages early exit; Rust returns from the inner function.
Full Source
#![allow(clippy::all)]
// 1070: Hamiltonian Path — Backtracking
// Approach 1: Adjacency matrix backtracking (fixed start = 0)
fn hamiltonian_path(adj: &[Vec<i32>]) -> Option<Vec<usize>> {
let n = adj.len();
let mut path = vec![0usize; n];
let mut visited = vec![false; n];
path[0] = 0;
visited[0] = true;
fn solve(pos: usize, adj: &[Vec<i32>], path: &mut Vec<usize>, visited: &mut Vec<bool>) -> bool {
let n = adj.len();
if pos == n {
return true;
}
for v in 0..n {
if !visited[v] && adj[path[pos - 1]][v] == 1 {
path[pos] = v;
visited[v] = true;
if solve(pos + 1, adj, path, visited) {
return true;
}
visited[v] = false;
}
}
false
}
if solve(1, adj, &mut path, &mut visited) {
Some(path)
} else {
None
}
}
// Approach 2: Try all starting vertices
fn hamiltonian_path_any(adj: &[Vec<i32>]) -> Option<Vec<usize>> {
let n = adj.len();
for start in 0..n {
let mut path = vec![0usize; n];
let mut visited = vec![false; n];
path[0] = start;
visited[start] = true;
fn solve(
pos: usize,
adj: &[Vec<i32>],
path: &mut Vec<usize>,
visited: &mut Vec<bool>,
) -> bool {
let n = adj.len();
if pos == n {
return true;
}
for v in 0..n {
if !visited[v] && adj[path[pos - 1]][v] == 1 {
path[pos] = v;
visited[v] = true;
if solve(pos + 1, adj, path, visited) {
return true;
}
visited[v] = false;
}
}
false
}
if solve(1, adj, &mut path, &mut visited) {
return Some(path);
}
}
None
}
// Approach 3: Bitmask DP for Hamiltonian path existence (O(2^n * n^2))
fn hamiltonian_exists_dp(adj: &[Vec<i32>]) -> bool {
let n = adj.len();
if n == 0 {
return true;
}
// dp[mask][i] = can we reach node i having visited exactly the nodes in mask?
let mut dp = vec![vec![false; n]; 1 << n];
for i in 0..n {
dp[1 << i][i] = true;
}
for mask in 1..(1 << n) {
for u in 0..n {
if !dp[mask][u] {
continue;
}
if mask & (1 << u) == 0 {
continue;
}
for v in 0..n {
if mask & (1 << v) == 0 && adj[u][v] == 1 {
dp[mask | (1 << v)][v] = true;
}
}
}
}
let full = (1 << n) - 1;
(0..n).any(|i| dp[full][i])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complete_graph() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
let path = hamiltonian_path(&adj).unwrap();
assert_eq!(path.len(), 4);
}
#[test]
fn test_path_graph() {
let adj = vec![
vec![0, 1, 0, 0],
vec![1, 0, 1, 0],
vec![0, 1, 0, 1],
vec![0, 0, 1, 0],
];
let path = hamiltonian_path(&adj).unwrap();
assert_eq!(path.len(), 4);
}
#[test]
fn test_any_start() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
assert!(hamiltonian_path_any(&adj).is_some());
}
#[test]
fn test_bitmask_dp() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
assert!(hamiltonian_exists_dp(&adj));
// Disconnected graph
let adj2 = vec![vec![0, 0, 0], vec![0, 0, 0], vec![0, 0, 0]];
assert!(!hamiltonian_exists_dp(&adj2));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complete_graph() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
let path = hamiltonian_path(&adj).unwrap();
assert_eq!(path.len(), 4);
}
#[test]
fn test_path_graph() {
let adj = vec![
vec![0, 1, 0, 0],
vec![1, 0, 1, 0],
vec![0, 1, 0, 1],
vec![0, 0, 1, 0],
];
let path = hamiltonian_path(&adj).unwrap();
assert_eq!(path.len(), 4);
}
#[test]
fn test_any_start() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
assert!(hamiltonian_path_any(&adj).is_some());
}
#[test]
fn test_bitmask_dp() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 1],
vec![1, 1, 0, 1],
vec![1, 1, 1, 0],
];
assert!(hamiltonian_exists_dp(&adj));
// Disconnected graph
let adj2 = vec![vec![0, 0, 0], vec![0, 0, 0], vec![0, 0, 0]];
assert!(!hamiltonian_exists_dp(&adj2));
}
}
Deep Comparison
Hamiltonian Path — Comparison
Core Insight
Finding a Hamiltonian path is NP-complete. Backtracking explores all orderings; bitmask DP (Held-Karp style) checks existence in O(2^n × n^2). The bitmask approach represents visited-node sets as integers, enabling efficient DP.
OCaml Approach
Array.fill for resetting path/visited between start verticesref flag for early exit in inner loopArray.to_list for result conversionRust Approach
vec![false; n] for visited trackingfn with explicit mutable referencesvec![vec![false; n]; 1 << n] — 2D table indexed by (mask, node)(0..n).any() for checking if any ending node reaches full maskComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Reset arrays | Array.fill path 0 n (-1) | Recreate vec![0; n] per start |
| Bitmask DP | Not idiomatic | 1 << n + bitwise ops — natural |
| Early exit | ref flag | return true |
| Complexity | O(n!) backtracking | O(2^n × n^2) bitmask DP |
| Path reconstruction | Array.to_list | Return Vec<usize> directly |
Exercises
hamiltonian_circuit that additionally requires the last vertex to be adjacent to the first (completing the cycle).dp[mask][v] = true if the subset encoded by mask has a Hamiltonian path ending at vertex v.tsp_exact(dist: &Vec<Vec<f64>>) -> (f64, Vec<usize>) using Held-Karp DP for the minimum-cost Hamiltonian circuit.