-
Notifications
You must be signed in to change notification settings - Fork 30
Feat: solve TSP using branch and bound method #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
fillipe-gsm
merged 27 commits into
fillipe-gsm:master
from
luanleonardo:luan-solve-TSP-using-branch-and-bound-algorithm
Aug 2, 2023
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
746fd19
feat: add function to compute the reduced matrix and its tests
75dc99d
chore: undo changes in checkup_scripts.sh
a602381
Update checkup_scripts.sh
luanleonardo bb52f58
feat: add solve_tsp_branch_and_bound draft
e4f5eb3
chore: Node class for space state tree
a49fa65
refac: refactor Node class and add NodePriorityQueue class
859cb9e
nitpick: solve Pycharm warning
e27e7f4
nitpick: remove unnecessary docstrings
7e16b9d
doc: remove unnecessary notes
bf9622b
doc: remove unnecessary comments
b790c1e
feat: add branch and bound TSP algorithm
bf2020a
style: remove some extra code
38454e5
fix: fill only the main diagonal of the cost matrix with INF
c6d6d88
refac: TSP solver as package
e65a171
doc: add docstrings
a1a913b
nitpick
a4020a2
doc: update README.rst
2b0002d
doc: update tips on deciding the exact solver
b5b0800
nitpick: update __init__.py
luanleonardo 5203f0f
fix: solver working with float distance matrix
54a1b42
doc: update docstring
luanleonardo 84a64f5
test: update teste case
5830804
nitpick: update test_solver.py
luanleonardo 92f303a
nitpick: update priority_queue.py
luanleonardo 150137a
very nitpicking: update priority_queue.py
luanleonardo 866b9d8
doc: update solver reference
9d24a3c
Update .gitignore
luanleonardo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,3 @@ | ||
from .node import Node # noqa: F401 | ||
from .priority_queue import PriorityQueue # noqa: F401 | ||
from .solver import solve_tsp_branch_and_bound # noqa: F401 |
This file contains hidden or 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,150 @@ | ||
from __future__ import annotations | ||
|
||
from dataclasses import dataclass | ||
from math import inf | ||
from typing import List, Tuple | ||
|
||
import numpy as np | ||
|
||
|
||
@dataclass | ||
class Node: | ||
""" | ||
Represents a node in the search tree for the Traveling Salesperson Problem. | ||
|
||
Attributes | ||
---------- | ||
level | ||
The level of the node in the search tree. | ||
index | ||
The index of the current city in the path. | ||
path | ||
The list of city indices visited so far. | ||
cost | ||
The total cost of the path up to this node. | ||
cost_matrix | ||
The cost matrix representing the distances between cities. | ||
|
||
Methods | ||
------- | ||
compute_reduced_matrix | ||
Compute the reduced matrix and the cost of reducing it. | ||
from_cost_matrix | ||
Create a Node object from a given cost matrix. | ||
from_parent | ||
Create a new Node object based on a parent node and a city index. | ||
""" | ||
|
||
level: int | ||
index: int | ||
path: List[int] | ||
cost: float | ||
cost_matrix: np.ndarray | ||
|
||
@staticmethod | ||
def compute_reduced_matrix(matrix: np.ndarray) -> Tuple[np.ndarray, float]: | ||
""" | ||
Compute the reduced matrix and the cost of reducing it. | ||
|
||
Parameters | ||
---------- | ||
matrix | ||
The cost matrix to compute the reductions. | ||
|
||
Returns | ||
------- | ||
Tuple | ||
A tuple containing the reduced matrix and the total | ||
cost of reductions. | ||
""" | ||
mask = matrix != inf | ||
reduced_matrix = np.copy(matrix) | ||
|
||
min_rows = np.min(reduced_matrix, axis=1, keepdims=True) | ||
min_rows[min_rows == inf] = 0 | ||
if np.any(min_rows != 0): | ||
reduced_matrix = np.where( | ||
mask, reduced_matrix - min_rows, reduced_matrix | ||
) | ||
|
||
min_cols = np.min(reduced_matrix, axis=0, keepdims=True) | ||
min_cols[min_cols == inf] = 0 | ||
if np.any(min_cols != 0): | ||
reduced_matrix = np.where( | ||
mask, reduced_matrix - min_cols, reduced_matrix | ||
) | ||
|
||
return reduced_matrix, np.sum(min_rows) + np.sum(min_cols) | ||
|
||
@classmethod | ||
def from_cost_matrix(cls, cost_matrix: np.ndarray) -> Node: | ||
""" | ||
Create a Node object from a given cost matrix. | ||
|
||
Parameters | ||
---------- | ||
cost_matrix | ||
The cost matrix representing the distances between cities. | ||
|
||
Returns | ||
------- | ||
Node | ||
A new Node object initialized with the reduced cost matrix. | ||
""" | ||
_cost_matrix, _cost = cls.compute_reduced_matrix(matrix=cost_matrix) | ||
return cls( | ||
level=0, | ||
index=0, | ||
path=[0], | ||
cost=_cost, | ||
cost_matrix=_cost_matrix, | ||
) | ||
|
||
@classmethod | ||
def from_parent(cls, parent: Node, index: int) -> Node: | ||
""" | ||
Create a new Node object based on a parent node and a city index. | ||
|
||
Parameters | ||
---------- | ||
parent | ||
The parent node. | ||
index | ||
The index of the new city to be added to the path. | ||
|
||
Returns | ||
------- | ||
Node | ||
A new Node object with the updated path and cost. | ||
""" | ||
matrix = np.copy(parent.cost_matrix) | ||
matrix[parent.index, :] = inf | ||
matrix[:, index] = inf | ||
matrix[index][0] = inf | ||
_cost_matrix, _cost = cls.compute_reduced_matrix(matrix=matrix) | ||
return cls( | ||
level=parent.level + 1, | ||
index=index, | ||
path=parent.path[:] + [index], | ||
cost=( | ||
parent.cost + _cost + parent.cost_matrix[parent.index][index] | ||
), | ||
cost_matrix=_cost_matrix, | ||
) | ||
|
||
def __lt__(self: Node, other: Node): | ||
""" | ||
Compare two Node objects based on their costs. | ||
|
||
Parameters | ||
---------- | ||
other | ||
The other Node object to compare with. | ||
|
||
Returns | ||
------- | ||
bool | ||
True if this Node's cost is less than the other Node's | ||
cost, False otherwise. | ||
""" | ||
return self.cost < other.cost |
This file contains hidden or 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,67 @@ | ||
from dataclasses import dataclass, field | ||
from heapq import heappop, heappush | ||
from typing import List | ||
|
||
from python_tsp.exact.branch_and_bound import Node | ||
|
||
|
||
@dataclass | ||
class PriorityQueue: | ||
""" | ||
A priority queue implementation using a binary heap | ||
for efficient element retrieval. | ||
|
||
Attributes | ||
---------- | ||
_container | ||
The list that holds the elements in the priority queue. | ||
|
||
Methods | ||
------- | ||
empty | ||
Check if the priority queue is empty. | ||
push | ||
Push an item into the priority queue. | ||
pop | ||
Pop the item with the highest priority from the priority queue. | ||
""" | ||
|
||
_container: List[Node] = field(default_factory=list) | ||
|
||
@property | ||
def empty(self) -> bool: | ||
""" | ||
Check if the priority queue is empty. | ||
|
||
Returns | ||
------- | ||
bool | ||
True if the priority queue is empty, False otherwise. | ||
""" | ||
return not self._container | ||
|
||
def push(self, item: Node) -> None: | ||
""" | ||
Push an item into the priority queue. | ||
|
||
Parameters | ||
---------- | ||
item | ||
The item to be pushed into the priority queue. | ||
|
||
Returns | ||
------- | ||
None | ||
""" | ||
heappush(self._container, item) | ||
|
||
def pop(self) -> Node: | ||
""" | ||
Pop the item with the highest priority from the priority queue. | ||
|
||
Returns | ||
------- | ||
Node | ||
The node with the highest priority. | ||
""" | ||
return heappop(self._container) |
This file contains hidden or 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,68 @@ | ||
from math import inf | ||
from typing import List, Tuple | ||
|
||
import numpy as np | ||
|
||
from python_tsp.exact.branch_and_bound import Node, PriorityQueue | ||
|
||
|
||
def solve_tsp_branch_and_bound( | ||
distance_matrix: np.ndarray, | ||
) -> Tuple[List[int], float]: | ||
""" | ||
Solve the Traveling Salesperson Problem (TSP) using the | ||
Branch and Bound algorithm. | ||
|
||
Parameters | ||
---------- | ||
distance_matrix | ||
The distance matrix representing the distances between cities. | ||
|
||
Returns | ||
------- | ||
Tuple | ||
A tuple containing the optimal path (list of city indices) and its | ||
total cost. If the TSP cannot be solved, an empty path and a cost | ||
of positive infinity will be returned. | ||
|
||
Notes | ||
----- | ||
The `distance_matrix` should be a square matrix with non-negative | ||
values. The element `distance_matrix[i][j]` represents the distance from | ||
city `i` to city `j`. If two cities are not directly connected, the | ||
distance should be set to a float value of positive infinity | ||
(float('inf')). | ||
|
||
The path is represented as a list of city indices, and the total cost is a | ||
float value indicating the sum of distances in the optimal path. | ||
|
||
If the TSP cannot be solved (e.g., due to disconnected cities), the | ||
function will return an empty path ([]) and a cost of positive infinity | ||
(float('inf')). | ||
|
||
References | ||
---------- | ||
.. [1] Horowitz, E., Sahni, S., & Rajasekaran, S. (1997). | ||
Computer Algorithms. Chapter 8 - Branch and Bound. Section 8.3. | ||
W. H. Freeman and Company. | ||
""" | ||
num_cities = len(distance_matrix) | ||
cost_matrix = np.copy(distance_matrix).astype(float) | ||
np.fill_diagonal(cost_matrix, inf) | ||
|
||
root = Node.from_cost_matrix(cost_matrix=cost_matrix) | ||
pq = PriorityQueue([root]) | ||
|
||
while not pq.empty: | ||
min_node = pq.pop() | ||
|
||
if min_node.level == num_cities - 1: | ||
return min_node.path, min_node.cost | ||
|
||
for index in range(num_cities): | ||
is_live_node = min_node.cost_matrix[min_node.index][index] != inf | ||
if is_live_node: | ||
live_node = Node.from_parent(parent=min_node, index=index) | ||
pq.push(live_node) | ||
|
||
return [], inf |
Empty file.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.