Skip to content

Commit

Permalink
Hamiltonian Cycle (TheAlgorithms#1930)
Browse files Browse the repository at this point in the history
* 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
CSenshi and cclauss authored May 2, 2020
1 parent d62cc35 commit a859934
Showing 1 changed file with 175 additions and 0 deletions.
175 changes: 175 additions & 0 deletions backtracking/hamiltonian_cycle.py
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 []

0 comments on commit a859934

Please sign in to comment.