From 0258a68fb669463021cfa70f18be0d063de121bf Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:18:00 +0400 Subject: [PATCH 01/10] add skeleton code --- backtracking/hamiltonian_cycle.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backtracking/hamiltonian_cycle.py diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py new file mode 100644 index 000000000000..5b7639d69ba4 --- /dev/null +++ b/backtracking/hamiltonian_cycle.py @@ -0,0 +1,12 @@ +""" + 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 +""" + + +def hamilton_cycle(graph): + pass From 17f24be6b9858ced2578ecfd88bf99ff29ee2c27 Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:18:36 +0400 Subject: [PATCH 02/10] add doctests --- backtracking/hamiltonian_cycle.py | 48 ++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index 5b7639d69ba4..0aa1d725e36e 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -8,5 +8,51 @@ """ -def hamilton_cycle(graph): +def util_hamilton_cycle(graph): pass + + +def hamilton_cycle(graph): + """ + 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. + Test 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] + Test 2: + 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) + [] + + """ + return [] \ No newline at end of file From 978a4ff6b4e3fee42fadf3a7653767051d56a9ee Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:19:16 +0400 Subject: [PATCH 03/10] add tests for util function + implement wrapper --- backtracking/hamiltonian_cycle.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index 0aa1d725e36e..78eee5c37e95 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -8,8 +8,22 @@ """ -def util_hamilton_cycle(graph): - pass +def util_hamilton_cycle(graph, path, start_index, current_index): + """ + >>> 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, -1] + >>> start_index = 0 + >>> current_index = 3 + >>> util_hamilton_cycle(graph, path, start_index, current_index) + True + >>> print(path) + [0, 1, 2, 4, 3, 0] + """ + return True def hamilton_cycle(graph): @@ -17,7 +31,7 @@ def hamilton_cycle(graph): 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. - Test 1: + 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: @@ -36,7 +50,7 @@ def hamilton_cycle(graph): ... [0, 1, 1, 1, 0]] >>> hamilton_cycle(graph) [0, 1, 2, 4, 3, 0] - Test 2: + Case 2: Following Graph is exactly what it was before, but edge 3-4 is removed. Result is that there is no Hamiltonian Cycle anymore. @@ -55,4 +69,12 @@ def hamilton_cycle(graph): [] """ + # 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] = 0 + + # evaluate and if we find answer return path either return empty array + if util_hamilton_cycle(graph, path, 0, 1): + return path return [] \ No newline at end of file From f6a79ad783fc43d012dd03bad48b062579588c70 Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:20:17 +0400 Subject: [PATCH 04/10] full implementation --- backtracking/hamiltonian_cycle.py | 74 +++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index 78eee5c37e95..cc8b12274cde 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -8,22 +8,79 @@ """ -def util_hamilton_cycle(graph, path, start_index, current_index): +def valid_connection(graph, next_vertex, curr_vertex, path): """ + Checks whether it is possible to add next_vertex 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 + """ + + # 1. Validate that path exists between current and next vertices + if graph[path[curr_vertex - 1]][next_vertex] == 0: + return False + + # 2. Validate that next vertex is not already in path + return not any(vertex == next_vertex for vertex in path) + + +def util_hamilton_cycle(graph, path, curr_vertex): + """ + 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, 2, -1, -1, -1] - >>> start_index = 0 - >>> current_index = 3 - >>> util_hamilton_cycle(graph, path, start_index, current_index) + >>> path = [0, -1, -1, -1, -1, 0] + >>> curr_vertex = 1 + >>> util_hamilton_cycle(graph, path, curr_vertex) + 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_vertex = 3 + >>> util_hamilton_cycle(graph, path, curr_vertex) True >>> print(path) [0, 1, 2, 4, 3, 0] """ - return True + + # Base Case + if curr_vertex == len(graph): + # return whether path exists between current and starting vertices + return graph[path[curr_vertex - 1]][0] == 1 + + # Recursive Step + for next_vertex in range(len(graph)): + if valid_connection(graph, next_vertex, curr_vertex, path): + # Insert current vertex into path as next transition + path[curr_vertex] = next_vertex + # Validate created path + if util_hamilton_cycle(graph, path, curr_vertex + 1): + return True + # Backtrack + path[curr_vertex] = -1 + return False def hamilton_cycle(graph): @@ -67,14 +124,13 @@ def hamilton_cycle(graph): ... [0, 1, 1, 0, 0]] >>> hamilton_cycle(graph) [] - """ + # 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] = 0 - # evaluate and if we find answer return path either return empty array - if util_hamilton_cycle(graph, path, 0, 1): + if util_hamilton_cycle(graph, path, 1): return path return [] \ No newline at end of file From 093e2ad5de14e9380acbe79a3f88fa9ded11dacd Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:21:01 +0400 Subject: [PATCH 05/10] add ability to add starting verex for algorithm --- backtracking/hamiltonian_cycle.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index cc8b12274cde..94289e92e898 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -68,7 +68,7 @@ def util_hamilton_cycle(graph, path, curr_vertex): # Base Case if curr_vertex == len(graph): # return whether path exists between current and starting vertices - return graph[path[curr_vertex - 1]][0] == 1 + return graph[path[curr_vertex - 1]][path[0]] == 1 # Recursive Step for next_vertex in range(len(graph)): @@ -83,7 +83,7 @@ def util_hamilton_cycle(graph, path, curr_vertex): return False -def hamilton_cycle(graph): +def hamilton_cycle(graph, start_index=0): """ Wrapper function to call subroutine called util_hamilton_cycle, which will either return array of vertices indicating hamiltonian cycle @@ -107,7 +107,24 @@ def hamilton_cycle(graph): ... [0, 1, 1, 1, 0]] >>> hamilton_cycle(graph) [0, 1, 2, 4, 3, 0] - Case 2: + + 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. @@ -122,14 +139,14 @@ def hamilton_cycle(graph): ... [0, 1, 0, 0, 1], ... [1, 1, 0, 0, 0], ... [0, 1, 1, 0, 0]] - >>> hamilton_cycle(graph) + >>> 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] = 0 + path[0] = path[-1] = start_index # evaluate and if we find answer return path either return empty array if util_hamilton_cycle(graph, path, 1): return path From a8954186f05b068d5fe3d1ce48cef13d289c1fd2 Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:21:28 +0400 Subject: [PATCH 06/10] add static type checking --- backtracking/hamiltonian_cycle.py | 38 +++++++++++++++++-------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index 94289e92e898..b1fc0c845869 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -1,3 +1,5 @@ +from typing import List + """ A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle through a graph that visits each node exactly once. @@ -8,9 +10,11 @@ """ -def valid_connection(graph, next_vertex, curr_vertex, path): +def valid_connection( + graph: List[List[int]], next: int, curr: int, path: List[int] +) -> bool: """ - Checks whether it is possible to add next_vertex into path by validating 2 statements + 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 @@ -18,14 +22,14 @@ def valid_connection(graph, next_vertex, curr_vertex, path): """ # 1. Validate that path exists between current and next vertices - if graph[path[curr_vertex - 1]][next_vertex] == 0: + if graph[path[curr - 1]][next] == 0: return False # 2. Validate that next vertex is not already in path - return not any(vertex == next_vertex for vertex in path) + return not any(vertex == next for vertex in path) -def util_hamilton_cycle(graph, path, curr_vertex): +def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr: int) -> bool: """ Pseudo-Code Base Case: @@ -46,8 +50,8 @@ def util_hamilton_cycle(graph, path, curr_vertex): ... [1, 1, 0, 0, 1], ... [0, 1, 1, 1, 0]] >>> path = [0, -1, -1, -1, -1, 0] - >>> curr_vertex = 1 - >>> util_hamilton_cycle(graph, path, curr_vertex) + >>> curr = 1 + >>> util_hamilton_cycle(graph, path, curr) True >>> print(path) [0, 1, 2, 4, 3, 0] @@ -58,32 +62,32 @@ def util_hamilton_cycle(graph, path, curr_vertex): ... [1, 1, 0, 0, 1], ... [0, 1, 1, 1, 0]] >>> path = [0, 1, 2, -1, -1, 0] - >>> curr_vertex = 3 - >>> util_hamilton_cycle(graph, path, curr_vertex) + >>> curr = 3 + >>> util_hamilton_cycle(graph, path, curr) True >>> print(path) [0, 1, 2, 4, 3, 0] """ # Base Case - if curr_vertex == len(graph): + if curr == len(graph): # return whether path exists between current and starting vertices - return graph[path[curr_vertex - 1]][path[0]] == 1 + return graph[path[curr - 1]][path[0]] == 1 # Recursive Step - for next_vertex in range(len(graph)): - if valid_connection(graph, next_vertex, curr_vertex, path): + for next in range(0, len(graph)): + if valid_connection(graph, next, curr, path): # Insert current vertex into path as next transition - path[curr_vertex] = next_vertex + path[curr] = next # Validate created path - if util_hamilton_cycle(graph, path, curr_vertex + 1): + if util_hamilton_cycle(graph, path, curr + 1): return True # Backtrack - path[curr_vertex] = -1 + path[curr] = -1 return False -def hamilton_cycle(graph, start_index=0): +def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]: """ Wrapper function to call subroutine called util_hamilton_cycle, which will either return array of vertices indicating hamiltonian cycle From 74d8e1b64c70b798e6f92a33f4866dbaac7aa044 Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:23:47 +0400 Subject: [PATCH 07/10] add doc tests to validation method --- backtracking/hamiltonian_cycle.py | 50 +++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index b1fc0c845869..1d58fe1beb99 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -11,25 +11,43 @@ def valid_connection( - graph: List[List[int]], next: int, curr: int, path: List[int] + 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 + 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 - 1]][next] == 0: + 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 for vertex in path) + return not any(vertex == next_ver for vertex in path) -def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr: int) -> bool: +def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr_ind: int) -> bool: """ Pseudo-Code Base Case: @@ -50,8 +68,8 @@ def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr: int) -> b ... [1, 1, 0, 0, 1], ... [0, 1, 1, 1, 0]] >>> path = [0, -1, -1, -1, -1, 0] - >>> curr = 1 - >>> util_hamilton_cycle(graph, path, curr) + >>> curr_ind = 1 + >>> util_hamilton_cycle(graph, path, curr_ind) True >>> print(path) [0, 1, 2, 4, 3, 0] @@ -62,28 +80,28 @@ def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr: int) -> b ... [1, 1, 0, 0, 1], ... [0, 1, 1, 1, 0]] >>> path = [0, 1, 2, -1, -1, 0] - >>> curr = 3 - >>> util_hamilton_cycle(graph, path, curr) + >>> curr_ind = 3 + >>> util_hamilton_cycle(graph, path, curr_ind) True >>> print(path) [0, 1, 2, 4, 3, 0] """ # Base Case - if curr == len(graph): + if curr_ind == len(graph): # return whether path exists between current and starting vertices - return graph[path[curr - 1]][path[0]] == 1 + return graph[path[curr_ind - 1]][path[0]] == 1 # Recursive Step for next in range(0, len(graph)): - if valid_connection(graph, next, curr, path): + if valid_connection(graph, next, curr_ind, path): # Insert current vertex into path as next transition - path[curr] = next + path[curr_ind] = next # Validate created path - if util_hamilton_cycle(graph, path, curr + 1): + if util_hamilton_cycle(graph, path, curr_ind + 1): return True # Backtrack - path[curr] = -1 + path[curr_ind] = -1 return False @@ -154,4 +172,4 @@ def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]: # evaluate and if we find answer return path either return empty array if util_hamilton_cycle(graph, path, 1): return path - return [] \ No newline at end of file + return [] From d690f0d53775b008ec4838aa7ce57a592cab67ab Mon Sep 17 00:00:00 2001 From: CSenshi Date: Sat, 2 May 2020 20:34:45 +0400 Subject: [PATCH 08/10] bug fix: doctests expected failing --- backtracking/hamiltonian_cycle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index 1d58fe1beb99..b8b1d3f10306 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -31,6 +31,7 @@ def valid_connection( >>> 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 @@ -73,6 +74,7 @@ def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr_ind: int) 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], @@ -146,6 +148,7 @@ def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]: ... [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. From edc9e7503e2b889f954efd730880435bf1be9d05 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 2 May 2020 21:05:28 +0200 Subject: [PATCH 09/10] Update hamiltonian_cycle.py --- backtracking/hamiltonian_cycle.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index b8b1d3f10306..90a4c704417c 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -1,5 +1,3 @@ -from typing import List - """ A Hamiltonian cycle (Hamiltonian circuit) is a graph cycle through a graph that visits each node exactly once. @@ -8,6 +6,7 @@ Wikipedia: https://en.wikipedia.org/wiki/Hamiltonian_path """ +from typing import List def valid_connection( @@ -173,6 +172,4 @@ def hamilton_cycle(graph: List[List[int]], start_index: int = 0) -> List[int]: # 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 - if util_hamilton_cycle(graph, path, 1): - return path - return [] + return path if util_hamilton_cycle(graph, path, 1) else [] From 5cca52179a28658f8373478f140be74a67e40929 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 2 May 2020 21:13:30 +0200 Subject: [PATCH 10/10] Update hamiltonian_cycle.py --- backtracking/hamiltonian_cycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtracking/hamiltonian_cycle.py b/backtracking/hamiltonian_cycle.py index 90a4c704417c..e4f2c62d2341 100644 --- a/backtracking/hamiltonian_cycle.py +++ b/backtracking/hamiltonian_cycle.py @@ -107,7 +107,7 @@ def util_hamilton_cycle(graph: List[List[int]], path: List[int], curr_ind: int) 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.