Skip to content

Commit a859934

Browse files
CSenshicclauss
andauthored
Hamiltonian Cycle (#1930)
* add skeleton code * add doctests * add tests for util function + implement wrapper * full implementation * add ability to add starting verex for algorithm * add static type checking * add doc tests to validation method * bug fix: doctests expected failing * Update hamiltonian_cycle.py * Update hamiltonian_cycle.py Co-authored-by: Christian Clauss <[email protected]>
1 parent d62cc35 commit a859934

File tree

1 file changed

+175
-0
lines changed

1 file changed

+175
-0
lines changed

backtracking/hamiltonian_cycle.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""
2+
A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle
3+
through a graph that visits each node exactly once.
4+
Determining whether such paths and cycles exist in graphs
5+
is the 'Hamiltonian path problem', which is NP-complete.
6+
7+
Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path
8+
"""
9+
from typing import List
10+
11+
12+
def valid_connection(
13+
graph: List[List[int]], next_ver: int, curr_ind: int, path: List[int]
14+
) -> bool:
15+
"""
16+
Checks whether it is possible to add next into path by validating 2 statements
17+
1. There should be path between current and next vertex
18+
2. Next vertex should not be in path
19+
If both validations succeeds we return true saying that it is possible to connect this vertices
20+
either we return false
21+
22+
Case 1:Use exact graph as in main function, with initialized values
23+
>>> graph = [[0, 1, 0, 1, 0],
24+
... [1, 0, 1, 1, 1],
25+
... [0, 1, 0, 0, 1],
26+
... [1, 1, 0, 0, 1],
27+
... [0, 1, 1, 1, 0]]
28+
>>> path = [0, -1, -1, -1, -1, 0]
29+
>>> curr_ind = 1
30+
>>> next_ver = 1
31+
>>> valid_connection(graph, next_ver, curr_ind, path)
32+
True
33+
34+
Case 2: Same graph, but trying to connect to node that is already in path
35+
>>> path = [0, 1, 2, 4, -1, 0]
36+
>>> curr_ind = 4
37+
>>> next_ver = 1
38+
>>> valid_connection(graph, next_ver, curr_ind, path)
39+
False
40+
"""
41+
42+
# 1. Validate that path exists between current and next vertices
43+
if graph[path[curr_ind - 1]][next_ver] == 0:
44+
return False
45+
46+
# 2. Validate that next vertex is not already in path
47+
return not any(vertex == next_ver for vertex in path)
48+
49+
50+
def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr_ind: int) -> bool:
51+
"""
52+
Pseudo-Code
53+
Base Case:
54+
1. Chceck if we visited all of vertices
55+
1.1 If last visited vertex has path to starting vertex return True either return False
56+
Recursive Step:
57+
2. Iterate over each vertex
58+
Check if next vertex is valid for transiting from current vertex
59+
2.1 Remember next vertex as next transition
60+
2.2 Do recursive call and check if going to this vertex solves problem
61+
2.3 if next vertex leads to solution return True
62+
2.4 else backtrack, delete remembered vertex
63+
64+
Case 1: Use exact graph as in main function, with initialized values
65+
>>> graph = [[0, 1, 0, 1, 0],
66+
... [1, 0, 1, 1, 1],
67+
... [0, 1, 0, 0, 1],
68+
... [1, 1, 0, 0, 1],
69+
... [0, 1, 1, 1, 0]]
70+
>>> path = [0, -1, -1, -1, -1, 0]
71+
>>> curr_ind = 1
72+
>>> util_hamilton_cycle(graph, path, curr_ind)
73+
True
74+
>>> print(path)
75+
[0, 1, 2, 4, 3, 0]
76+
77+
Case 2: Use exact graph as in previous case, but in the properties taken from middle of calculation
78+
>>> graph = [[0, 1, 0, 1, 0],
79+
... [1, 0, 1, 1, 1],
80+
... [0, 1, 0, 0, 1],
81+
... [1, 1, 0, 0, 1],
82+
... [0, 1, 1, 1, 0]]
83+
>>> path = [0, 1, 2, -1, -1, 0]
84+
>>> curr_ind = 3
85+
>>> util_hamilton_cycle(graph, path, curr_ind)
86+
True
87+
>>> print(path)
88+
[0, 1, 2, 4, 3, 0]
89+
"""
90+
91+
# Base Case
92+
if curr_ind == len(graph):
93+
# return whether path exists between current and starting vertices
94+
return graph[path[curr_ind - 1]][path[0]] == 1
95+
96+
# Recursive Step
97+
for next in range(0, len(graph)):
98+
if valid_connection(graph, next, curr_ind, path):
99+
# Insert current vertex into path as next transition
100+
path[curr_ind] = next
101+
# Validate created path
102+
if util_hamilton_cycle(graph, path, curr_ind + 1):
103+
return True
104+
# Backtrack
105+
path[curr_ind] = -1
106+
return False
107+
108+
109+
def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]:
110+
r"""
111+
Wrapper function to call subroutine called util_hamilton_cycle,
112+
which will either return array of vertices indicating hamiltonian cycle
113+
or an empty list indicating that hamiltonian cycle was not found.
114+
Case 1:
115+
Following graph consists of 5 edges.
116+
If we look closely, we can see that there are multiple Hamiltonian cycles.
117+
For example one result is when we iterate like:
118+
(0)->(1)->(2)->(4)->(3)->(0)
119+
120+
(0)---(1)---(2)
121+
| / \ |
122+
| / \ |
123+
| / \ |
124+
|/ \|
125+
(3)---------(4)
126+
>>> graph = [[0, 1, 0, 1, 0],
127+
... [1, 0, 1, 1, 1],
128+
... [0, 1, 0, 0, 1],
129+
... [1, 1, 0, 0, 1],
130+
... [0, 1, 1, 1, 0]]
131+
>>> hamilton_cycle(graph)
132+
[0, 1, 2, 4, 3, 0]
133+
134+
Case 2:
135+
Same Graph as it was in Case 1, changed starting index from default to 3
136+
137+
(0)---(1)---(2)
138+
| / \ |
139+
| / \ |
140+
| / \ |
141+
|/ \|
142+
(3)---------(4)
143+
>>> graph = [[0, 1, 0, 1, 0],
144+
... [1, 0, 1, 1, 1],
145+
... [0, 1, 0, 0, 1],
146+
... [1, 1, 0, 0, 1],
147+
... [0, 1, 1, 1, 0]]
148+
>>> hamilton_cycle(graph, 3)
149+
[3, 0, 1, 2, 4, 3]
150+
151+
Case 3:
152+
Following Graph is exactly what it was before, but edge 3-4 is removed.
153+
Result is that there is no Hamiltonian Cycle anymore.
154+
155+
(0)---(1)---(2)
156+
| / \ |
157+
| / \ |
158+
| / \ |
159+
|/ \|
160+
(3) (4)
161+
>>> graph = [[0, 1, 0, 1, 0],
162+
... [1, 0, 1, 1, 1],
163+
... [0, 1, 0, 0, 1],
164+
... [1, 1, 0, 0, 0],
165+
... [0, 1, 1, 0, 0]]
166+
>>> hamilton_cycle(graph,4)
167+
[]
168+
"""
169+
170+
# Initialize path with -1, indicating that we have not visited them yet
171+
path = [-1] * (len(graph) + 1)
172+
# initialize start and end of path with starting index
173+
path[0] = path[-1] = start_index
174+
# evaluate and if we find answer return path either return empty array
175+
return path if util_hamilton_cycle(graph, path, 1) else []

0 commit comments

Comments
 (0)