mirror of
https://hub.njuu.cf/TheAlgorithms/Python.git
synced 2023-10-11 13:06:12 +08:00
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 <cclauss@me.com>
This commit is contained in:
parent
d62cc35268
commit
a859934105
175
backtracking/hamiltonian_cycle.py
Normal file
175
backtracking/hamiltonian_cycle.py
Normal file
@ -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 []
|
Loading…
Reference in New Issue
Block a user