A quick note: I haven't tested any of the code in this answer, so there may be bugs hiding in there.
OK, first things first. Let's figure out what we're looking at:
You want to write a function that solves a given instance of the traveling salesman problem. The algorithm you've decided to use is to enumerate the permutations of cities, terminating consideration of a permutation prefix if its partial cost exceeds the cost of the best full permutation so far.
I'll touch more on the choice of algorithms below, but let's start by writing a class to represent an instance of the problem.
/**
* Represents an instance of the traveling salesman problem.
*
* Any solution must start and finish at city 0.
*/
class TravelingSalesmanProblem {
public:
template <std::size_t N>
TravelingSalesmanProblem(const int (&distance_matrix)[N][N]);
int distance(std::size_t city1, std::size_t city2) const;
std::size_t num_cities() const { return num_cities_; }
private:
std::vector<std::vector<int>> distances_;
std::size_t num_cities_;
};
One reason to write a class instead of passing around handles to the distance matrix directly is that I can imagine two reasonable implementations of this class, and I'd need a profiler to choose one over the other.
Version 1:
template <std::size_t N>
TravelingSalesmanProblem::TravelingSalesmanProblem(const int (&distance_matrix)[N][N])
: distances_(N, std::vector<int>(N)), num_cities_(N) {
for (std::size_t i = 0; i < N; ++i) {
for (std::size_t j = i; j < N; ++j) {
assert(distance_matrix[i][j] == distance_matrix[j][i]); // You might rewrite this using exceptions instead.
distances_[i][j] = distances_[j][i] = distance_matrix[i][j];
}
}
}
int TravelingSalesmanProblem::distance(std::size_t city1, std::size_t city2) const {
return distances_.at(city1).at(city2);
}
Version 2 requires changing the declaration of TravelingSalesmanProblem::distance_
to
std::vector<int> distance_;
Then the implementation of TravelingSalesmanProblem
is:
template <std::size_t N>
TravelingSalesmanProblem::TravelingSalesmanProblem(const int (&distance_matrix)[N][N])
: distances_(N * (N + 1) / 2), num_cities_(N) {
for (std::size_t i = 0; i < N; ++i) {
for (std::size_t j = 0; j <= i; ++j) {
assert(distance_matrix[i][j] == distance_matrix[j][i]);
distances_[i * (i + 1) / 2 + j] = distance_matrix[i][j];
}
}
}
int TravelingSalesmanProblem::distance(std::size_t city1, std::size_t city2) const {
std::size_t i, j;
std::tie(j, i) = std::minmax(city1, city2);
return distances_.at(i * (i + 1) / 2 + j);
}
Version 2 requires less indirection and less memory but requires a little extra computation at each call to distance
. You might also make TravelingSalesmanProblem
a template class parameterized on N
so that you can make distance_
an int[N][N]
, but this will require that the problem size is known at compile time (which is fine for this example but makes the code less reusable).
Now let's write solve_tsp
(I'm writing it here as a standalone function, but you could reasonably make it a member of TravelingSalesmanProblem
).
The first question is which data structures to use for various purposes. The first is the path concept. Here, I suggest std::vector<int>
. You used QList<int>
, but that includes memory indirection and likely non-local allocations of elements in memory, which I think may be a significant factor in running time. std::vector
allocates a single block of contiguous memory where its members are stored; this means that access to elements requires only one memory indirection (as opposed to two for QList
) and that contiguous elements are contiguous in memory as well, which may improve memory caching.
You also used QList<int>
for the list of remaining cities. QList
doesn't really buy you anything over std::vector
here, since you never prepend; see also my arguments about path. Therefore, I also choose std::vector<int>
for this purpose.
solve_tsp
is just a sort of wrapper function that initializes the data structures that we'll need and calls out to a helper, which uses recursion to iterate over all permutations of the cities.
std::vector<int> solve_tsp(const TravelingSalesmanProblem& problem) {
std::vector<int> path;
path.reserve(problem.num_cities() + 1);
path.push_back(0);
std::vector<int> remaining_cities(problem.num_cities() - 1);
std::iota(remaining_cities.begin(), remaining_cities.end(), 1);
solve_tsp_helper(path, remaining_cities, 0, std::numeric_limits<int>::max());
return path;
}
// path is an in-out parameter; if there is any solution with path as a prefix whose
// cost is less than best_previous_path, on return path will contain the best such
// solution, and the return value will be the cost of that solution. If no such
// solution exists, path will be unmodified after return and the return value will
// be at least as large as best_previous_path.
int solve_tsp_helper(const TravelingSalesmanProblem& problem,
std::vector<int>& path,
const std::vector<int>& remaining_cities,
const int cost_so_far, int best_previous_path_cost) {
const int last_city = path.back();
const auto current_path_length = path.size();
const int full_path_cost = cost_so_far + problem.distance(last_city, 0);
if (remaining_cities.empty()) {
path.push_back(0);
return full_path_cost;
}
// Note: The validity of this early exit relies on problem adhering to the triangle inequality.
if (full_path_cost >= best_previous_path_cost) {
return full_path_cost;
}
auto best_path = path;
// This next bit of code is a bit more complicated than the naive
// "for (int next_city : remaining_cities)" to prevent copying
// remaining_cities more than once.
std::vector<int> other_remaining_cities(std::next(remaining_cities.begin()),
remaining_cities.end());
for (std::size_t i = 0; i < remaining_cities.size(); ++i) {
// This loop maintains the invariant that either best_previous_path_cost has its
// original value and best_path has path's original value, or
// best_previous_path_cost has reduced in value and best_path is a full path
// with cost best_previous_path_cost.
const int next_city = remaining_cities[i];
if (i != 0) {
other_remaining_cities[i - 1] = remaining_cities[i - 1];
}
path.resize(current_path_length);
path.push_back(next_city);
const auto cost = solve_tsp_helper(
problem, path, other_remaining_cities,
cost_so_far + problem.distance(last_city, next_city), best_previous_path_cost);
if (cost < best_previous_path) {
best_previous_path_cost = cost;
swap(path, best_path);
}
}
// Because of the loop invariant, either best_previous_path_cost has not changed
// and best_path is the original value of path, or best_path is a solution to problem
// with cost equal to best_previous_path_cost. In either case, we fulfill the
// contract of the function by swapping best_path into path and returning
// best_previous_path_cost.
swap(path, best_path);
return best_previous_path_cost;
}
Some notes on this implementation. First of all, it avoids any calls to new
or delete
. It also copies remaining_cities
and path
once per call to solve_tsp_helper
instead of once per element of remaining_cities
. So even though the algorithm hasn't changed (much -- see next paragraph), the code should be faster. With some effort, you could probably eliminate any copies of path
except when a better path is found.
I've also slightly changed your early exit condition, from cost_so_far > best_previous_path_cost
to cost_so_far + distance(last_city, 0) >= best_previous_path_cost
. This is valid whenever the triangle inequality holds for your data (that is, for any three cities A, B, C, dist(A, C) <= dist(A, B) + dist(B, C)
; this condition usually holds).
As for the algorithm, this SO question discusses faster exact algorithms. They are considerably more complicated than this algorithm but can yield much better performance. You can also consider heuristic answers, which can produce reasonably good solutions though are not guaranteed to provide the exact optimal solution.