Hamiltonian Flights
Problem Overview
| Attribute | Value |
|---|---|
| Difficulty | Hard |
| Category | Bitmask DP / Graph |
| Time Limit | 1 second |
| Key Technique | Bitmask DP for subset enumeration |
| CSES Link | Hamiltonian Flights |
Learning Goals
After solving this problem, you will be able to:
- Understand when to apply Bitmask DP for subset problems
- Represent visited vertices as a bitmask
- Define DP states combining bitmask and current position
- Implement efficient state transitions using bitwise operations
- Handle counting problems modulo a prime
Problem Statement
Problem: Count the number of Hamiltonian paths from city 1 to city n in a directed graph.
A Hamiltonian path visits every vertex exactly once.
Input:
- Line 1: n (cities) and m (flights)
- Next m lines: a b (flight from city a to city b)
Output:
- Number of Hamiltonian paths from 1 to n, modulo 10^9 + 7
Constraints:
- 2 <= n <= 20
- 1 <= m <= n^2
- 1 <= a, b <= n
Example
Input:
4 6
1 2
1 3
2 3
3 2
2 4
3 4
Output:
2
Explanation: Two Hamiltonian paths exist:
- Path 1: 1 -> 2 -> 3 -> 4
- Path 2: 1 -> 3 -> 2 -> 4
Both visit all 4 cities exactly once.
Intuition: How to Think About This Problem
Pattern Recognition
Key Question: How do we efficiently track which cities have been visited?
When n <= 20, we can represent the set of visited cities as a bitmask. Each bit position corresponds to a city: bit i is 1 if city i has been visited.
Breaking Down the Problem
- What are we counting? Paths visiting all n cities exactly once, starting at 1 and ending at n.
- What state do we need? The set of visited cities AND the current city.
- Why bitmask? With n <= 20, there are at most 2^20 = ~10^6 possible subsets, which is manageable.
Analogies
Think of this like a delivery driver who must visit all customers exactly once. The bitmask is like a checklist tracking which customers have been served, and we need to count all valid orderings.
Solution 1: Brute Force (DFS)
Idea
Try all possible orderings of cities using depth-first search with backtracking.
Algorithm
- Start DFS from city 1
- For each unvisited neighbor, mark it visited and recurse
- If we reach city n with all cities visited, increment count
- Backtrack by unmarking the city
Code
def solve_brute_force(n, adj):
"""
Brute force using DFS backtracking.
Time: O(n! * n) - factorial paths, n edges each
Space: O(n) - recursion stack
"""
MOD = 10**9 + 7
count = 0
def dfs(city, visited_count, visited):
nonlocal count
if city == n:
if visited_count == n:
count = (count + 1) % MOD
return
for next_city in adj[city]:
if not visited[next_city]:
visited[next_city] = True
dfs(next_city, visited_count + 1, visited)
visited[next_city] = False
visited = [False] * (n + 1)
visited[1] = True
dfs(1, 1, visited)
return count
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O(n! * n) | At most n! paths, each path has n transitions |
| Space | O(n) | Visited array and recursion stack |
Why This Works (But Is Slow)
This explores every possible path. For n=20, there could be up to 20! paths - far too many to enumerate.
Solution 2: Optimal Solution (Bitmask DP)
Key Insight
The Trick: Use a bitmask to represent visited cities, converting the exponential backtracking into polynomial DP.
Instead of exploring paths one by one, we count paths to each (mask, city) state.
DP State Definition
| State | Meaning |
|---|---|
dp[mask][v] |
Number of paths that visit exactly the cities in mask and end at city v |
In plain English: dp[mask][v] counts how many ways we can arrive at city v having visited precisely the cities whose bits are set in mask.
State Transition
dp[new_mask][u] += dp[mask][v] for each edge v -> u
where new_mask = mask | (1 << u)
Why? If we have dp[mask][v] paths ending at v, and there’s an edge v -> u where u is unvisited (bit not set), we can extend all those paths to u.
Base Cases
| Case | Value | Reason |
|---|---|---|
dp[1][1] |
1 | Start at city 1, only city 1 visited |
| All other | 0 | No paths initially |
Algorithm
- Initialize dp[1][1] = 1 (city 1 visited, at city 1)
- Iterate through all masks from 1 to 2^n - 1
- For each valid (mask, v) state, extend to neighbors
- Answer is dp[(1 « n) - 1][n] (all visited, at city n)
Dry Run Example
Let’s trace with n=4, edges: 1->2, 1->3, 2->3, 3->2, 2->4, 3->4:
Initial: dp[0001][1] = 1 (binary 0001 = city 1 visited)
Process mask=0001 (city 1):
From city 1:
Edge 1->2: dp[0011][2] += dp[0001][1] = 1
Edge 1->3: dp[0101][3] += dp[0001][1] = 1
Process mask=0011 (cities 1,2):
From city 2:
Edge 2->3: dp[0111][3] += dp[0011][2] = 1
Edge 2->4: dp[1011][4] += dp[0011][2] = 1
Process mask=0101 (cities 1,3):
From city 3:
Edge 3->2: dp[0111][2] += dp[0101][3] = 1
Edge 3->4: dp[1101][4] += dp[0101][3] = 1
Process mask=0111 (cities 1,2,3):
From city 2: dp[1111][4] += dp[0111][2] = 1
From city 3: dp[1111][4] += dp[0111][3] = 1
Final: dp[1111][4] = 2 (all cities visited, at city 4)
Visual Diagram
State Space:
mask=0001 mask=0011 mask=0111 mask=1111
city 1 city 2 city 3 city 4
(1) ------> (1) ------> (1) ------> (1)
\ / /
\-> city 3 \/ /
(1) ------> (1) ------+
city 2 \
-> (2) Total
Bitmask meaning:
0001 = {1}
0011 = {1,2}
0101 = {1,3}
0111 = {1,2,3}
1111 = {1,2,3,4}
Code
def solve(n, m, edges):
"""
Bitmask DP solution for counting Hamiltonian paths.
Time: O(2^n * n^2)
Space: O(2^n * n)
"""
MOD = 10**9 + 7
# Build adjacency list
adj = [[] for _ in range(n + 1)]
for a, b in edges:
adj[a].append(b)
# dp[mask][v] = number of paths visiting cities in mask, ending at v
dp = [[0] * (n + 1) for _ in range(1 << n)]
# Base case: start at city 1
dp[1][1] = 1
# Process all masks in order
for mask in range(1, 1 << n):
for v in range(1, n + 1):
# Skip if v is not in current mask
if not (mask & (1 << (v - 1))):
continue
if dp[mask][v] == 0:
continue
# Try extending to each neighbor
for u in adj[v]:
# Skip if u already visited
if mask & (1 << (u - 1)):
continue
new_mask = mask | (1 << (u - 1))
dp[new_mask][u] = (dp[new_mask][u] + dp[mask][v]) % MOD
# Answer: all cities visited, ending at city n
full_mask = (1 << n) - 1
return dp[full_mask][n]
# Input handling
def main():
import sys
input_data = sys.stdin.read().split()
idx = 0
n = int(input_data[idx]); idx += 1
m = int(input_data[idx]); idx += 1
edges = []
for _ in range(m):
a = int(input_data[idx]); idx += 1
b = int(input_data[idx]); idx += 1
edges.append((a, b))
print(solve(n, m, edges))
if __name__ == "__main__":
main()
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O(2^n * n^2) | 2^n masks, n cities per mask, n edges per city |
| Space | O(2^n * n) | DP table size |
Common Mistakes
Mistake 1: Wrong Bit Indexing
# WRONG: Using 0-indexed cities with 1-indexed bit positions
if mask & (1 << v): # If cities are 1-indexed, this is off by one
# CORRECT: Adjust for 1-indexed cities
if mask & (1 << (v - 1)):
Problem: Mismatch between city numbering and bit positions. Fix: Be consistent. If cities are 1 to n, use bits 0 to n-1.
Mistake 2: Checking Visited After Adding
# WRONG: Update then check
new_mask = mask | (1 << (u - 1))
if new_mask & (1 << (u - 1)): # Always true!
# CORRECT: Check before updating
if mask & (1 << (u - 1)): # Skip if already visited
continue
new_mask = mask | (1 << (u - 1))
Mistake 3: Forgetting Modular Arithmetic
# WRONG: Overflow for large counts
dp[new_mask][u] += dp[mask][v]
# CORRECT: Apply modulo
dp[new_mask][u] = (dp[new_mask][u] + dp[mask][v]) % MOD
Mistake 4: Wrong Initial State
# WRONG: Empty mask or wrong city
dp[0][1] = 1 # mask=0 means no cities visited
# CORRECT: City 1 visited, at city 1
dp[1][1] = 1 # mask=1 means city 1 is visited
Edge Cases
| Case | Input | Expected Output | Why |
|---|---|---|---|
| n=2, direct path | 2 1, 1 2 |
1 | Only path is 1->2 |
| n=2, no path | 2 0 |
0 | No edges at all |
| No path to n | 3 2, 1 2, 2 1 |
0 | City 3 unreachable |
| Must visit middle | 3 2, 1 3, 2 3 |
0 | Cannot visit city 2 |
| Multiple paths | 3 4, 1 2, 1 3, 2 3, 3 2 |
1 | Only 1->2->3 works |
When to Use This Pattern
Use Bitmask DP When:
- Small n (<= 20-25): Bitmask has 2^n states
- Subset tracking needed: Need to know which elements are “used”
- Order matters: DP state includes both subset and position
- Counting or optimization: Finding count/min/max over all orderings
Don’t Use When:
- n is large (> 25): 2^n becomes too big
- Simple linear DP suffices: No need for subset tracking
- Polynomial solution exists: Always prefer lower complexity
Pattern Recognition Checklist:
- n <= 20-25? -> Consider bitmask
- Need to track visited/used elements? -> Bitmask
- Asking for paths visiting all vertices? -> Hamiltonian path, use bitmask DP
- Asking for circuits returning to start? -> Hamiltonian circuit variant
Related Problems
Easier (Do These First)
| Problem | Why It Helps |
|---|---|
| Grid Paths | Basic DP counting paths |
| Counting Paths | DP on DAG |
Similar Difficulty
| Problem | Key Difference |
|---|---|
| Round Trip | Find any cycle, not count |
| Round Trip II | Directed graph cycle |
| Graph Girth | Shortest cycle length |
Harder (Do These After)
| Problem | New Concept |
|---|---|
| Elevator Rides | Bitmask DP with optimization |
| Counting Tilings | Profile DP (bitmask on grid) |
| General Graph Matching | Bitmask with complex state |
Key Takeaways
- The Core Idea: Represent visited vertices as bits in an integer; DP state = (visited set, current position).
- Time Optimization: From O(n!) backtracking to O(2^n * n^2) DP by memoizing states.
- Space Trade-off: O(2^n * n) space to avoid redundant computation.
- Pattern: Bitmask DP - classic technique for small n with subset requirements.
Practice Checklist
Before moving on, make sure you can:
- Solve this problem without looking at the solution
- Explain why bitmask works here but not for n=100
- Convert between city numbers and bit positions correctly
- Trace through the DP table for a small example
- Implement in your preferred language in under 15 minutes
Additional Resources
- CP-Algorithms: Bitmask DP
- CP-Algorithms: Hamiltonian Path
- CSES Hamiltonian Flights - Bitmask DP application