-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Add Traveling Salesman Problem (TSP) Algorithms and Tests #536
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
siriak
merged 5 commits into
TheAlgorithms:master
from
ngtduc693:feature/tsp-traveling-salesman-problem
Oct 5, 2025
+394
−65
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d9e2234
The Traveling salesman problem (TSP)
ngtduc693 b930208
Merge branch 'master' into feature/tsp-traveling-salesman-problem
ngtduc693 a9fd194
Add more unit test for TSP
ngtduc693 c471301
Merge branch 'master' into feature/tsp-traveling-salesman-problem
siriak a89eed3
Merge branch 'master' into feature/tsp-traveling-salesman-problem
siriak 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
121 changes: 121 additions & 0 deletions
121
Algorithms.Tests/Problems/TravelingSalesman/TravelingSalesmanSolverTests.cs
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,121 @@ | ||
using Algorithms.Problems.TravelingSalesman; | ||
using NUnit.Framework; | ||
|
||
namespace Algorithms.Tests.Problems.TravelingSalesman; | ||
|
||
/// <summary> | ||
/// Unit tests for TravelingSalesmanSolver. Covers brute-force and nearest neighbor methods, including edge cases and invalid input. | ||
/// </summary> | ||
[TestFixture] | ||
public class TravelingSalesmanSolverTests | ||
{ | ||
/// <summary> | ||
/// Tests brute-force TSP solver on a 4-city symmetric distance matrix with known optimal route. | ||
/// </summary> | ||
[Test] | ||
public void SolveBruteForce_KnownOptimalRoute_ReturnsCorrectResult() | ||
{ | ||
// Distance matrix for 4 cities (symmetric, triangle inequality holds) | ||
double[,] matrix = | ||
{ | ||
{ 0, 10, 15, 20 }, | ||
{ 10, 0, 35, 25 }, | ||
{ 15, 35, 0, 30 }, | ||
{ 20, 25, 30, 0 } | ||
}; | ||
var (route, distance) = TravelingSalesmanSolver.SolveBruteForce(matrix); | ||
// Optimal route: 0 -> 1 -> 3 -> 2 -> 0, total distance = 80 | ||
Assert.That(distance, Is.EqualTo(80)); | ||
Assert.That(route, Is.EquivalentTo(new[] { 0, 1, 3, 2, 0 })); | ||
} | ||
|
||
/// <summary> | ||
/// Tests nearest neighbor heuristic on the same matrix. May not be optimal. | ||
/// </summary> | ||
[Test] | ||
public void SolveNearestNeighbor_Heuristic_ReturnsFeasibleRoute() | ||
{ | ||
double[,] matrix = | ||
{ | ||
{ 0, 10, 15, 20 }, | ||
{ 10, 0, 35, 25 }, | ||
{ 15, 35, 0, 30 }, | ||
{ 20, 25, 30, 0 } | ||
}; | ||
var (route, distance) = TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0); | ||
// Route: 0 -> 1 -> 3 -> 2 -> 0, total distance = 80 | ||
Assert.That(route.Length, Is.EqualTo(5)); | ||
Assert.That(route.First(), Is.EqualTo(0)); | ||
Assert.That(route.Last(), Is.EqualTo(0)); | ||
Assert.That(distance, Is.GreaterThanOrEqualTo(80)); // Heuristic may be optimal or suboptimal | ||
} | ||
|
||
/// <summary> | ||
/// Tests nearest neighbor with invalid start index. | ||
/// </summary> | ||
[Test] | ||
public void SolveNearestNeighbor_InvalidStart_ThrowsException() | ||
{ | ||
double[,] matrix = | ||
{ | ||
{ 0, 1 }, | ||
{ 1, 0 } | ||
}; | ||
Assert.Throws<ArgumentOutOfRangeException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, -1)); | ||
Assert.Throws<ArgumentOutOfRangeException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 2)); | ||
} | ||
|
||
/// <summary> | ||
/// Tests nearest neighbor when no unvisited cities remain (should throw InvalidOperationException). | ||
/// </summary> | ||
[Test] | ||
public void SolveNearestNeighbor_NoUnvisitedCities_ThrowsException() | ||
{ | ||
// Construct a matrix where one city cannot be reached (simulate unreachable city) | ||
double[,] matrix = | ||
{ | ||
{ 0, double.MaxValue, 1 }, | ||
{ double.MaxValue, 0, double.MaxValue }, | ||
{ 1, double.MaxValue, 0 } | ||
}; | ||
// Start at city 0, city 1 is unreachable from both 0 and 2 | ||
Assert.Throws<InvalidOperationException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0)); | ||
} | ||
|
||
/// <summary> | ||
/// Tests brute-force and nearest neighbor with non-square matrix. | ||
/// </summary> | ||
[Test] | ||
public void NonSquareMatrix_ThrowsException() | ||
{ | ||
double[,] matrix = new double[2, 3]; | ||
Assert.Throws<ArgumentException>(() => TravelingSalesmanSolver.SolveBruteForce(matrix)); | ||
Assert.Throws<ArgumentException>(() => TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0)); | ||
} | ||
|
||
/// <summary> | ||
/// Tests brute-force with less than two cities (invalid case). | ||
/// </summary> | ||
[Test] | ||
public void SolveBruteForce_TooFewCities_ThrowsException() | ||
{ | ||
double[,] matrix = { { 0 } }; | ||
Assert.Throws<ArgumentException>(() => TravelingSalesmanSolver.SolveBruteForce(matrix)); | ||
} | ||
|
||
/// <summary> | ||
/// Tests nearest neighbor with only two cities (trivial case). | ||
/// </summary> | ||
[Test] | ||
public void SolveNearestNeighbor_TwoCities_ReturnsCorrectRoute() | ||
{ | ||
double[,] matrix = | ||
{ | ||
{ 0, 5 }, | ||
{ 5, 0 } | ||
}; | ||
var (route, distance) = TravelingSalesmanSolver.SolveNearestNeighbor(matrix, 0); | ||
Assert.That(route, Is.EquivalentTo(new[] { 0, 1, 0 })); | ||
Assert.That(distance, Is.EqualTo(10)); | ||
} | ||
} |
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
136 changes: 136 additions & 0 deletions
136
Algorithms/Problems/TravelingSalesman/TravelingSalesmanSolver.cs
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,136 @@ | ||
namespace Algorithms.Problems.TravelingSalesman; | ||
|
||
/// <summary> | ||
/// Provides methods to solve the Traveling Salesman Problem (TSP) using brute-force and nearest neighbor heuristics. | ||
/// The TSP is a classic optimization problem in which a salesman must visit each city exactly once and return to the starting city, minimizing the total travel distance. | ||
/// </summary> | ||
public static class TravelingSalesmanSolver | ||
{ | ||
/// <summary> | ||
/// Solves the TSP using brute-force search. This method checks all possible permutations of cities to find the shortest possible route. | ||
/// WARNING: This approach is only feasible for small numbers of cities due to factorial time complexity. | ||
/// </summary> | ||
/// <param name="distanceMatrix">A square matrix where element [i, j] represents the distance from city i to city j.</param> | ||
/// <returns>A tuple containing the minimal route (as an array of city indices) and the minimal total distance.</returns> | ||
public static (int[] Route, double Distance) SolveBruteForce(double[,] distanceMatrix) | ||
{ | ||
int n = distanceMatrix.GetLength(0); | ||
if (n != distanceMatrix.GetLength(1)) | ||
{ | ||
throw new ArgumentException("Distance matrix must be square."); | ||
} | ||
|
||
if (n < 2) | ||
{ | ||
throw new ArgumentException("At least two cities are required."); | ||
} | ||
|
||
var cities = Enumerable.Range(0, n).ToArray(); | ||
double minDistance = double.MaxValue; | ||
int[]? bestRoute = null; | ||
|
||
foreach (var perm in Permute(cities.Skip(1).ToArray())) | ||
{ | ||
var route = new int[n + 1]; | ||
route[0] = 0; | ||
for (int i = 0; i < perm.Length; i++) | ||
{ | ||
route[i + 1] = perm[i]; | ||
} | ||
|
||
// Ensure route ends at city 0 | ||
route[n] = 0; | ||
|
||
double dist = 0; | ||
for (int i = 0; i < n; i++) | ||
{ | ||
dist += distanceMatrix[route[i], route[i + 1]]; | ||
} | ||
|
||
if (dist < minDistance) | ||
{ | ||
minDistance = dist; | ||
bestRoute = (int[])route.Clone(); | ||
} | ||
} | ||
|
||
return (bestRoute ?? Array.Empty<int>(), minDistance); | ||
} | ||
|
||
/// <summary> | ||
/// Solves the TSP using the nearest neighbor heuristic. This method builds a route by always visiting the nearest unvisited city next. | ||
/// This approach is much faster but may not find the optimal solution. | ||
/// </summary> | ||
/// <param name="distanceMatrix">A square matrix where element [i, j] represents the distance from city i to city j.</param> | ||
/// <param name="start">The starting city index.</param> | ||
/// <returns>A tuple containing the route (as an array of city indices) and the total distance.</returns> | ||
public static (int[] Route, double Distance) SolveNearestNeighbor(double[,] distanceMatrix, int start = 0) | ||
{ | ||
int n = distanceMatrix.GetLength(0); | ||
if (n != distanceMatrix.GetLength(1)) | ||
{ | ||
throw new ArgumentException("Distance matrix must be square."); | ||
} | ||
|
||
if (start < 0 || start >= n) | ||
{ | ||
throw new ArgumentOutOfRangeException(nameof(start)); | ||
} | ||
|
||
var visited = new bool[n]; | ||
var route = new List<int> { start }; | ||
visited[start] = true; | ||
double totalDistance = 0; | ||
int current = start; | ||
for (int step = 1; step < n; step++) | ||
{ | ||
double minDist = double.MaxValue; | ||
int next = -1; | ||
for (int j = 0; j < n; j++) | ||
{ | ||
if (!visited[j] && distanceMatrix[current, j] < minDist) | ||
{ | ||
minDist = distanceMatrix[current, j]; | ||
next = j; | ||
} | ||
} | ||
|
||
if (next == -1) | ||
{ | ||
throw new InvalidOperationException("No unvisited cities remain."); | ||
} | ||
|
||
route.Add(next); | ||
visited[next] = true; | ||
totalDistance += minDist; | ||
current = next; | ||
} | ||
|
||
totalDistance += distanceMatrix[current, start]; | ||
route.Add(start); | ||
return (route.ToArray(), totalDistance); | ||
} | ||
|
||
/// <summary> | ||
/// Generates all permutations of the input array. | ||
/// Used for brute-force TSP solution. | ||
/// </summary> | ||
private static IEnumerable<int[]> Permute(int[] arr) | ||
{ | ||
if (arr.Length == 1) | ||
{ | ||
yield return arr; | ||
} | ||
else | ||
{ | ||
for (int i = 0; i < arr.Length; i++) | ||
{ | ||
var rest = arr.Where((_, idx) => idx != i).ToArray(); | ||
foreach (var perm in Permute(rest)) | ||
{ | ||
yield return new[] { arr[i] }.Concat(perm).ToArray(); | ||
} | ||
} | ||
} | ||
} | ||
ngtduc693 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
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.