CF 290F - Greedy Petya
We are given an undirected graph with up to 20 vertices and up to 400 edges, and we need to decide whether there exists a simple path that visits every vertex exactly once.
Rating: 2800
Tags: *special, dfs and similar, graphs, greedy
Solve time: 1m 8s
Verified: yes
Solution
Problem Understanding
We are given an undirected graph with up to 20 vertices and up to 400 edges, and we need to decide whether there exists a simple path that visits every vertex exactly once. This is the Hamiltonian path problem in its decision form: we are not asked to construct the path, only to say whether at least one such ordering of vertices exists where consecutive vertices are connected by edges.
The key detail is the scale of the graph. With at most 20 vertices, exponential methods become viable, but anything like enumerating all permutations of vertices directly would already be borderline because $20!$ is far too large. However, any solution with state compression over subsets of vertices, on the order of $2^n \cdot n^2$, is comfortably feasible.
One subtle point is that the graph may contain self-loops or multiple edges, and these do not affect the existence of a Hamiltonian path. A self-loop never helps in a path that must move between distinct vertices, and duplicate edges are redundant for connectivity.
A naive mistake that appears often is to interpret this as a connectivity problem. For example, checking that the graph is connected and all vertices have degree at least one is far from sufficient. A connected graph like a star with 20 vertices has a center connected to all leaves but no Hamiltonian path, since any path that enters the center can only leave it once, making it impossible to visit all leaves in a single chain.
Another failure case comes from greedy traversal. Starting from a random node and always going to an unvisited neighbor can get stuck early even when a valid Hamiltonian path exists. The local choice does not encode global constraints.
Approaches
A brute-force approach would try all permutations of the vertices and check whether consecutive vertices are connected by edges. For each permutation, we verify adjacency in $O(n)$, and there are $n!$ permutations, giving an overall complexity of $O(n! \cdot n)$. Even at $n = 20$, this is completely infeasible.
The structure of the problem changes significantly once we observe that we only care about which subset of vertices has been visited and the last vertex in the path. Two different permutations that visit the same set of vertices and end at the same vertex are equivalent in terms of future extendability. This suggests a dynamic programming formulation over subsets.
We define a state that captures a subset of visited vertices and the endpoint of the path. From any state, we try extending the path to an unvisited neighbor. This reduces the search space from permutations to subset transitions, turning an intractable enumeration into a manageable $2^n \cdot n$ state space.
This is the standard Hamiltonian path DP, and here it fits perfectly because $n \le 20$, making $2^n \approx 10^6$, which is small enough.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force (permutations) | $O(n! \cdot n)$ | $O(n)$ | Too slow |
| Bitmask DP over paths | $O(n^2 \cdot 2^n)$ | $O(n \cdot 2^n)$ | Accepted |
Algorithm Walkthrough
We build a dynamic programming table where we track whether a certain subset of vertices can form a valid path ending at a particular vertex.
- We represent each subset of vertices using a bitmask of length $n$. A bitmask encodes exactly which vertices are already included in the partial path. This representation is natural because transitions only add one vertex at a time.
- We define a DP array where
dp[mask][v]is true if there exists a path that visits exactly the vertices inmaskand ends at vertexv. This state captures all information needed to extend the path further. - We initialize the DP by setting
dp[1 << v][v] = Truefor every vertexv. Each vertex alone forms a trivial path of length one. - We iterate over all masks from small to large. For each mask, we try all possible endpoints
v. Ifdp[mask][v]is true, we attempt to extend the path. - To extend a state
(mask, v), we try all neighborstoofv. Iftois not inmask, we setdp[mask | (1 << to)][to] = True. This step corresponds exactly to appending a new vertex to the current path. - After processing all states, we check whether there exists any vertex
vsuch thatdp[(1 << n) - 1][v]is true. If so, a Hamiltonian path exists.
The correctness hinges on the fact that every valid Hamiltonian path has a last vertex, and the DP will eventually construct exactly that sequence of subsets leading to it.
Why it works
The invariant is that dp[mask][v] is true if and only if there exists a simple path that visits exactly the vertices in mask and ends at v. Every transition preserves simplicity because we only move to unvisited vertices, and every possible extension of a valid path is considered. Since every Hamiltonian path corresponds to some ordering of subset additions, it must appear in this state space, and no invalid state is ever introduced.
Python Solution
import sys
input = sys.stdin.readline
def solve():
n, m = map(int, input().split())
adj = [[False] * n for _ in range(n)]
for _ in range(m):
u, v = map(int, input().split())
u -= 1
v -= 1
adj[u][v] = True
adj[v][u] = True
if n == 1:
print("Yes")
return
size = 1 << n
dp = [[False] * n for _ in range(size)]
for i in range(n):
dp[1 << i][i] = True
for mask in range(size):
for v in range(n):
if not dp[mask][v]:
continue
if mask == (1 << n) - 1:
continue
for to in range(n):
if not adj[v][to]:
continue
if mask & (1 << to):
continue
dp[mask | (1 << to)][to] = True
full = (1 << n) - 1
for v in range(n):
if dp[full][v]:
print("Yes")
return
print("No")
if __name__ == "__main__":
solve()
The adjacency matrix is used so that edge checks are constant time. This is important because the DP already runs over all subsets and endpoints, and any logarithmic overhead per transition would be too expensive.
The DP table is indexed by bitmask first because subset iteration is the natural outer loop. Each state expands only along actual edges, and we explicitly skip transitions to already visited vertices to ensure path validity.
The final scan over dp[full] checks whether any endpoint is reachable after visiting all vertices.
Worked Examples
Example 1
Input graph:
3 2
1 2
2 3
We expect a Hamiltonian path like 1 → 2 → 3.
| mask | endpoint v | dp state | transition |
|---|---|---|---|
| 001 | 1 | true | start |
| 010 | 2 | true | start |
| 100 | 3 | true | start |
| 011 | 2 | true | 1→2 |
| 110 | 3 | true | 2→3 |
| 111 | 3 | true | 1→2→3 |
At the final mask, at least one endpoint is reachable, so the answer is “Yes”.
This trace shows how partial paths merge naturally as subsets grow.
Example 2
Input:
4 2
1 2
3 4
The graph is disconnected, so no Hamiltonian path exists.
All DP states remain confined within their components, and no transition can move from {1,2} to {3,4}. Therefore no state reaches mask 1111.
The DP never produces a full-mask state, confirming that connectivity alone inside subsets is not enough.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | $O(n^2 \cdot 2^n)$ | Each subset-state tries up to $n$ endpoints and up to $n$ neighbors |
| Space | $O(n \cdot 2^n)$ | DP table storing states for each mask and endpoint |
With $n \le 20$, $2^n \approx 10^6$, and the constant factor around $n^2$, the solution comfortably fits within limits in Python with adjacency matrix optimizations.
Test Cases
import sys, io
def run(inp: str) -> str:
sys.stdin = io.StringIO(inp)
from sys import stdout
import sys as _sys
output = []
def fake_print(*args):
output.append(" ".join(map(str, args)))
global print
old_print = print
print = fake_print
try:
solve()
finally:
print = old_print
return "\n".join(output) + ("\n" if output else "")
# provided sample
assert run("2 3\n1 2\n2 1\n1 1\n") == "Yes\n", "sample 1"
# simple path
assert run("3 2\n1 2\n2 3\n") == "Yes\n"
# disconnected graph
assert run("4 2\n1 2\n3 4\n") == "No\n"
# single node
assert run("1 0\n") == "Yes\n"
# star graph (no Hamiltonian path)
assert run("5 4\n1 2\n1 3\n1 4\n1 5\n") == "No\n"
| Test input | Expected output | What it validates |
|---|---|---|
| single node | Yes | trivial base case |
| linear chain | Yes | normal DP propagation |
| disconnected pairs | No | component isolation |
| star graph | No | greedy pitfall case |
Edge Cases
One edge case is the single vertex graph. The DP initializes dp[1<<0][0] = true, and since this already corresponds to the full mask, the algorithm immediately succeeds, correctly outputting “Yes”.
Another case is graphs with isolated vertices. For example, if vertex 5 has no edges, it can only appear as a standalone path. The DP correctly handles this because states starting at 5 cannot expand, and no full-mask state becomes reachable.
A more subtle case is dense graphs with missing critical edges that block a full ordering. Even if most vertices are highly connected, the DP still respects exact adjacency constraints, so it never falsely combines incompatible partial paths.