forked from TheAlgorithms/Python
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Hamiltonian Cycle (TheAlgorithms#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]>
- Loading branch information
Showing
1 changed file
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
""" | ||
A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle | ||
through a graph that visits each node exactly once. | ||
Determining whether such paths and cycles exist in graphs | ||
is the 'Hamiltonian path problem', which is NP-complete. | ||
Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path | ||
""" | ||
from typing import List | ||
|
||
|
||
def valid_connection( | ||
graph: List[List[int]], next_ver: int, curr_ind: int, path: List[int] | ||
) -> bool: | ||
""" | ||
Checks whether it is possible to add next into path by validating 2 statements | ||
1. There should be path between current and next vertex | ||
2. Next vertex should not be in path | ||
If both validations succeeds we return true saying that it is possible to connect this vertices | ||
either we return false | ||
Case 1:Use exact graph as in main function, with initialized values | ||
>>> graph = [[0, 1, 0, 1, 0], | ||
... [1, 0, 1, 1, 1], | ||
... [0, 1, 0, 0, 1], | ||
... [1, 1, 0, 0, 1], | ||
... [0, 1, 1, 1, 0]] | ||
>>> path = [0, -1, -1, -1, -1, 0] | ||
>>> curr_ind = 1 | ||
>>> next_ver = 1 | ||
>>> valid_connection(graph, next_ver, curr_ind, path) | ||
True | ||
Case 2: Same graph, but trying to connect to node that is already in path | ||
>>> path = [0, 1, 2, 4, -1, 0] | ||
>>> curr_ind = 4 | ||
>>> next_ver = 1 | ||
>>> valid_connection(graph, next_ver, curr_ind, path) | ||
False | ||
""" | ||
|
||
# 1. Validate that path exists between current and next vertices | ||
if graph[path[curr_ind - 1]][next_ver] == 0: | ||
return False | ||
|
||
# 2. Validate that next vertex is not already in path | ||
return not any(vertex == next_ver for vertex in path) | ||
|
||
|
||
def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr_ind: int) -> bool: | ||
""" | ||
Pseudo-Code | ||
Base Case: | ||
1. Chceck if we visited all of vertices | ||
1.1 If last visited vertex has path to starting vertex return True either return False | ||
Recursive Step: | ||
2. Iterate over each vertex | ||
Check if next vertex is valid for transiting from current vertex | ||
2.1 Remember next vertex as next transition | ||
2.2 Do recursive call and check if going to this vertex solves problem | ||
2.3 if next vertex leads to solution return True | ||
2.4 else backtrack, delete remembered vertex | ||
Case 1: Use exact graph as in main function, with initialized values | ||
>>> graph = [[0, 1, 0, 1, 0], | ||
... [1, 0, 1, 1, 1], | ||
... [0, 1, 0, 0, 1], | ||
... [1, 1, 0, 0, 1], | ||
... [0, 1, 1, 1, 0]] | ||
>>> path = [0, -1, -1, -1, -1, 0] | ||
>>> curr_ind = 1 | ||
>>> util_hamilton_cycle(graph, path, curr_ind) | ||
True | ||
>>> print(path) | ||
[0, 1, 2, 4, 3, 0] | ||
Case 2: Use exact graph as in previous case, but in the properties taken from middle of calculation | ||
>>> graph = [[0, 1, 0, 1, 0], | ||
... [1, 0, 1, 1, 1], | ||
... [0, 1, 0, 0, 1], | ||
... [1, 1, 0, 0, 1], | ||
... [0, 1, 1, 1, 0]] | ||
>>> path = [0, 1, 2, -1, -1, 0] | ||
>>> curr_ind = 3 | ||
>>> util_hamilton_cycle(graph, path, curr_ind) | ||
True | ||
>>> print(path) | ||
[0, 1, 2, 4, 3, 0] | ||
""" | ||
|
||
# Base Case | ||
if curr_ind == len(graph): | ||
# return whether path exists between current and starting vertices | ||
return graph[path[curr_ind - 1]][path[0]] == 1 | ||
|
||
# Recursive Step | ||
for next in range(0, len(graph)): | ||
if valid_connection(graph, next, curr_ind, path): | ||
# Insert current vertex into path as next transition | ||
path[curr_ind] = next | ||
# Validate created path | ||
if util_hamilton_cycle(graph, path, curr_ind + 1): | ||
return True | ||
# Backtrack | ||
path[curr_ind] = -1 | ||
return False | ||
|
||
|
||
def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]: | ||
r""" | ||
Wrapper function to call subroutine called util_hamilton_cycle, | ||
which will either return array of vertices indicating hamiltonian cycle | ||
or an empty list indicating that hamiltonian cycle was not found. | ||
Case 1: | ||
Following graph consists of 5 edges. | ||
If we look closely, we can see that there are multiple Hamiltonian cycles. | ||
For example one result is when we iterate like: | ||
(0)->(1)->(2)->(4)->(3)->(0) | ||
(0)---(1)---(2) | ||
| / \ | | ||
| / \ | | ||
| / \ | | ||
|/ \| | ||
(3)---------(4) | ||
>>> graph = [[0, 1, 0, 1, 0], | ||
... [1, 0, 1, 1, 1], | ||
... [0, 1, 0, 0, 1], | ||
... [1, 1, 0, 0, 1], | ||
... [0, 1, 1, 1, 0]] | ||
>>> hamilton_cycle(graph) | ||
[0, 1, 2, 4, 3, 0] | ||
Case 2: | ||
Same Graph as it was in Case 1, changed starting index from default to 3 | ||
(0)---(1)---(2) | ||
| / \ | | ||
| / \ | | ||
| / \ | | ||
|/ \| | ||
(3)---------(4) | ||
>>> graph = [[0, 1, 0, 1, 0], | ||
... [1, 0, 1, 1, 1], | ||
... [0, 1, 0, 0, 1], | ||
... [1, 1, 0, 0, 1], | ||
... [0, 1, 1, 1, 0]] | ||
>>> hamilton_cycle(graph, 3) | ||
[3, 0, 1, 2, 4, 3] | ||
Case 3: | ||
Following Graph is exactly what it was before, but edge 3-4 is removed. | ||
Result is that there is no Hamiltonian Cycle anymore. | ||
(0)---(1)---(2) | ||
| / \ | | ||
| / \ | | ||
| / \ | | ||
|/ \| | ||
(3) (4) | ||
>>> graph = [[0, 1, 0, 1, 0], | ||
... [1, 0, 1, 1, 1], | ||
... [0, 1, 0, 0, 1], | ||
... [1, 1, 0, 0, 0], | ||
... [0, 1, 1, 0, 0]] | ||
>>> hamilton_cycle(graph,4) | ||
[] | ||
""" | ||
|
||
# Initialize path with -1, indicating that we have not visited them yet | ||
path = [-1] * (len(graph) + 1) | ||
# initialize start and end of path with starting index | ||
path[0] = path[-1] = start_index | ||
# evaluate and if we find answer return path either return empty array | ||
return path if util_hamilton_cycle(graph, path, 1) else [] |