Skip to content

Add Traveling Salesman Problem Algorithms And Tests #12820

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

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
10b1a2a
Add Traveling Salesman Problem algorithms and tests
MapleBauhinia Jul 5, 2025
e39c3ce
Add TSP Problem
MapleBauhinia Jul 5, 2025
c32a022
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2025
85f1401
Fix: TSP rename lambda parameter and add type hints
MapleBauhinia Jul 5, 2025
345d58f
add-tsp-problem
MapleBauhinia Jul 5, 2025
c7be8c0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2025
a766f38
Fix: format and pass all tests
MapleBauhinia Jul 5, 2025
ed8b6e3
add-tsp-problem
MapleBauhinia Jul 5, 2025
13df43e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2025
7bd83fd
Standardize output to int
MapleBauhinia Jul 5, 2025
81fcb2f
Merge branch 'add-tsp-problem' of https://github.com/MapleBauhinia/Py…
MapleBauhinia Jul 5, 2025
80cc148
Fix: Build PR
MapleBauhinia Jul 5, 2025
cef217a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2025
f61f7cf
Fix: format
MapleBauhinia Jul 5, 2025
3042b37
Merge branch 'add-tsp-problem' of https://github.com/MapleBauhinia/Py…
MapleBauhinia Jul 5, 2025
9cc0448
Fix: tsp-greedy
MapleBauhinia Jul 5, 2025
9c9a3e4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2025
4c59775
Fix: ruff check
MapleBauhinia Jul 5, 2025
d913580
Merge branch 'add-tsp-problem' of https://github.com/MapleBauhinia/Py…
MapleBauhinia Jul 5, 2025
86be481
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions graphs/tests/test_traveling_salesman_problem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from graphs.traveling_salesman_problem import tsp_brute_force, tsp_dp, tsp_greedy


def sample_graph_1() -> list[list[int]]:
return [
[0, 29, 20],
[29, 0, 15],
[20, 15, 0],
]


def sample_graph_2() -> list[list[int]]:
return [
[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0],
]


def test_brute_force() -> None:
graph = sample_graph_1()
assert tsp_brute_force(graph) == 64


def test_dp() -> None:
graph = sample_graph_1()
assert tsp_dp(graph) == 64


def test_greedy() -> None:
graph = sample_graph_1()
# The greedy algorithm does not guarantee an optimal solution;
# it is necessary to verify that its output is an integer greater than 0.
# An approximate solution cannot be represented by '==',
# and can only ensure that the result is reasonable.
result = tsp_greedy(graph)
assert isinstance(result, int)
assert result >= 64


def test_dp_larger_graph() -> None:
graph = sample_graph_2()
assert tsp_dp(graph) == 80


def test_brute_force_larger_graph() -> None:
graph = sample_graph_2()
assert tsp_brute_force(graph) == 80


def test_greedy_larger_graph() -> None:
graph = sample_graph_2()
result = tsp_greedy(graph)
assert isinstance(result, int)
assert result >= 80
164 changes: 164 additions & 0 deletions graphs/traveling_salesman_problem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from itertools import permutations


def tsp_brute_force(graph: list[list[int]]) -> int:
"""
Solves TSP using brute-force permutations.

Args:
graph: 2D list representing distances between cities.

Returns:
The minimal total travel distance visiting all cities exactly once,
and then returning to the start.

Example:
>>> tsp_brute_force([[0, 29, 20], [29, 0, 15], [20, 15, 0]])
64
"""
n = len(graph)
# Apart from other cities aside from City 0, City 0 serves as the starting point.
nodes = list(range(1, n))
min_path = float("inf")

# Enumerate all the permutations from city 1 to city n-1.
for perm in permutations(nodes):
# Construct a complete path:
# Starting from point 0, visit in the order of arrangement,
# and then return to point 0.
path = [0, *perm, 0]

# Calculate the total distance of the path.
# Update the shortest path.
total_cost = sum(graph[path[i]][path[i + 1]] for i in range(n))
min_path = min(min_path, total_cost)

return int(min_path)


def tsp_dp(graph: list[list[int]]) -> int:
"""
Solves the Traveling Salesman Problem using Held-Karp dynamic programming.

Args:
graph: A 2D list representing distances between cities (n x n matrix).

Returns:
The minimum cost to visit all cities exactly once and return to the origin.

Example:
>>> tsp_dp([[0, 29, 20], [29, 0, 15], [20, 15, 0]])
64
"""
n = len(graph)
# Create a dynamic programming table of size (2^n) x n.
# Noting: 1 << n = 2^n
# dp[mask][i] represents the shortest path starting from city 0,
# passing through the cities in the mask, and ultimately ending at city i.
dp = [[float("inf")] * n for _ in range(1 << n)]
# Initial state: only city 0 is visited, and the path length is 0.
dp[1][0] = 0

for mask in range(1 << n):
# The mask indicates which cities have been visited.
for u in range(n):
if not (mask & (1 << u)):
# If the city u is not included in the mask, skip it.
continue

for v in range(n):
# City v has not been accessed and is different from city u.
if mask & (1 << v) or u == v:
continue

# New State: Transition to city v
# State Transition: From city u to city v, updating the shortest path.
next_mask = mask | (1 << v)
dp[next_mask][v] = min(dp[next_mask][v], dp[mask][u] + graph[u][v])

# After completing visits to all cities,
# return to city 0 and obtain the minimum value.
return int(min(dp[(1 << n) - 1][i] + graph[i][0] for i in range(1, n)))


def tsp_greedy(graph: list[list[int]]) -> int:
"""
Solves TSP approximately using the nearest neighbor heuristic.
Warming: This algorithm is not guaranteed to find the optimal solution!
But it is fast and applicable to any input size.

Args:
graph: 2D list representing distances between cities.

Returns:
The total distance of the approximated TSP route.

Example:
>>> tsp_greedy([[0, 29, 20], [29, 0, 15], [20, 15, 0]])
64
"""
n = len(graph)
visited = [False] * n # Mark whether each city has been visited.
path = [0]
total_cost = 0
visited[0] = True # Start from city 0.
current = 0 # Current city.

for _ in range(n - 1):
# Find the nearest city to the current location that has not been visited.
next_city = min(
(
(city, cost)
for city, cost in enumerate(graph[current])
if not visited[city] and city != current
),
key=lambda cost: cost[1],
default=(None, float("inf")),
)[0]

# If no such city exists, break the loop.
if next_city is None:
break

# Update the total cost and the current city.
# Mark the city as visited.
# Append the city to the path.
total_cost += graph[current][next_city]
visited[next_city] = True
current = next_city
path.append(current)

# Back to start
total_cost += graph[current][0]
path.append(0)

return int(total_cost)


def test_tsp_example() -> None:
graph = [[0, 29, 20], [29, 0, 15], [20, 15, 0]]

result = tsp_brute_force(graph)
if result != 64:
raise Exception("tsp_brute_force Incorrect result")
else:
print("Test passed")

result = tsp_dp(graph)
if result != 64:
raise Exception("tsp_dp Incorrect result")
else:
print("Test passed")

result = tsp_greedy(graph)
if result != 64:
if result < 0:
raise Exception("tsp_greedy Incorrect result")
else:
print("tsp_greedy gets an approximate result.")
else:
print("Test passed")


if __name__ == "__main__":
test_tsp_example()
8 changes: 5 additions & 3 deletions greedy_methods/fractional_knapsack.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ def frac_knapsack(vl, wt, w, n):
return (
0
if k == 0
else sum(vl[:k]) + (w - acc[k - 1]) * (vl[k]) / (wt[k])
if k != n
else sum(vl[:k])
else (
sum(vl[:k]) + (w - acc[k - 1]) * (vl[k]) / (wt[k])
if k != n
else sum(vl[:k])
)
)


Expand Down
2 changes: 1 addition & 1 deletion machine_learning/frequent_pattern_growth.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def ascend_tree(leaf_node: TreeNode, prefix_path: list[str]) -> None:
ascend_tree(leaf_node.parent, prefix_path)


def find_prefix_path(base_pat: frozenset, tree_node: TreeNode | None) -> dict: # noqa: ARG001
def find_prefix_path(_base_pat: frozenset, tree_node: TreeNode | None) -> dict:
"""
Find the conditional pattern base for a given base pattern.

Expand Down
8 changes: 5 additions & 3 deletions matrix/matrix_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,11 @@ def cofactors(self) -> Matrix:
return Matrix(
[
[
self.minors().rows[row][column]
if (row + column) % 2 == 0
else self.minors().rows[row][column] * -1
(
self.minors().rows[row][column]
if (row + column) % 2 == 0
else self.minors().rows[row][column] * -1
)
for column in range(self.minors().num_columns)
]
for row in range(self.minors().num_rows)
Expand Down
3 changes: 2 additions & 1 deletion quantum/quantum_teleportation.py.DISABLED.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ https://qiskit.org/textbook/ch-algorithms/teleportation.html

import numpy as np
import qiskit
from qiskit import Aer, ClassicalRegister, QuantumCircuit, QuantumRegister, execute
from qiskit import (Aer, ClassicalRegister, QuantumCircuit, QuantumRegister,
execute)


def quantum_teleportation(
Expand Down
4 changes: 3 additions & 1 deletion sorts/bead_sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
https://en.wikipedia.org/wiki/Bead_sort
"""

from itertools import pairwise


def bead_sort(sequence: list) -> list:
"""
Expand Down Expand Up @@ -31,7 +33,7 @@ def bead_sort(sequence: list) -> list:
if any(not isinstance(x, int) or x < 0 for x in sequence):
raise TypeError("Sequence must be list of non-negative integers")
for _ in range(len(sequence)):
for i, (rod_upper, rod_lower) in enumerate(zip(sequence, sequence[1:])): # noqa: RUF007
for i, (rod_upper, rod_lower) in enumerate(pairwise(sequence)):
if rod_upper > rod_lower:
sequence[i] -= rod_upper - rod_lower
sequence[i + 1] += rod_upper - rod_lower
Expand Down
2 changes: 1 addition & 1 deletion strings/min_cost_string_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]:
elif op[0] == "R":
string[i] = op[2]

file.write("%-16s" % ("Replace %c" % op[1] + " with " + str(op[2]))) # noqa: UP031
file.write(f"{'Replace ' + op[1] + ' with ' + str(op[2]):<16}")
file.write("\t\t" + "".join(string))
file.write("\r\n")

Expand Down
Loading