Пути на графе. Задача коммивояжёра на C++

от автора

Постановка задачи

Имется связанный граф. Необходимо найти оптимальный путь начинающийся и заканчивающийся в данной точке, также необходимо посетить подмножество вершин графа.

Для решения данной задачи, сначала найдём минимальные расстояния между всеми необходимыми для посещения вершинами и остальными с помощью алгоритма Дейкстры. Затем будем искать минимальную сумму расстояний изменяя порядок посещения вершин.

Эту задачу можно встретить и на практике. Например, поиск маршрута для полета квадрокоптера-курьера.

Реализация графа

Граф с N вершинами будет храниться в матрице размера NxN.

По умолчанию зададим граф показанный на рисунке ниже:

Граф по умолчанию
Граф по умолчанию

Graph.h:

#ifndef GRAPH_H #define GRAPH_H  #include <iostream> #include <iomanip>  using namespace std;  class Graph { private:     int n;                          // количество вершин графа     int** data;                     // данные о ребрах графа: -1 - отсутствует ребро; >0 - расттояние между вершинами i и j      public:     Graph(int n);                   // создание графа с n вершинами     Graph();                        // граф по умолчанию     int** Get_data() const;         // данные о графе     int Get_n() const;              // количество вершин графа  };  ostream& operator<<(ostream& os, const Graph& g);  #endif 

Graph.cpp:

#include "Graph.h"  Graph::Graph() {     this->n = 5;     data = new int*[5];     for (int i = 0; i < 5; i++) {         data[i] = new int[5];     }      // граф по умолчанию     data[0][0] = 0;    data[0][1] = 2;    data[0][2] = -1;    data[0][3] = 3;    data[0][4] = -1;     data[1][0] =2;     data[1][1] = 0;    data[1][2] = 4;     data[1][3] = 4;    data[1][4] = -1;     data[2][0] = -1;   data[2][1] = 4;    data[2][2] = 0;     data[2][3] = 5;    data[2][4] = 6;     data[3][0] = 3;    data[3][1] = 4;    data[3][2] = 5;    data[3][3] = 0;    data[3][4] = -1;     data[4][0] = -1;   data[4][1] = -1;   data[4][2] = 6;     data[4][3] = -1;   data[4][4] = 0; }  // создание графа с n вершинами Graph::Graph(int n) {     this->n = n;     data = new int*[n];     for (int i = 0; i < n; i++) {         data[i] = new int[n];     }     for (int i = 0; i < n; i++) {         for (int j = i + 1; j < n; j++) {             cout << "Input " << i << '-' << j << ": ";             cin >> data[i][j];             data[j][i] = data[i][j];         }     } }  // данные о графе int** Graph::Get_data() const { return data; }  // количество вершин графа int Graph::Get_n() const { return n; }  // вывод графа ostream& operator<<(ostream& os, const Graph& g) {     int** d = g.Get_data();          for (int i = 0; i < g.Get_n(); i++) {         for (int j = 0; j < g.Get_n(); j++) {             os << setw(4) << d[i][j] << ' ';         }         os << endl;     }     return os; } 

Реализация пути

Путь хранится в виде вектора, также я храню длину всего пути. Реализуем также функции добавления в конец пути одной вершины и другого пути (это будет использоваться дальше в алгоритме Дейкстры).

Path.h:

#ifndef PATH_H #define PATH_H  #include <iostream> #include <set> #include <vector>  using namespace std;  class Path { private:     vector<int> path;                   // путь     int length;                         // длина пути  public:     Path(int start);     Path& operator= (const Path& p);     vector<int> Get_path() const;       // путь     int Get_length() const;             // длина пути     void push_back(int v, int l);       // добавление вершины в конец     void push_back(Path p);             // добавление пути в конец  };  ostream& operator<<(ostream& os, const Path& p);  #endif 

Path.cpp:

#include "Path.h"  Path::Path(int start) {     path.push_back(start);     length = 0; }  Path& Path::operator=(const Path& p) {     path = p.Get_path();     length = p.Get_length();     return *this; }  // путь vector<int> Path::Get_path() const { return path; }  // длина пути int Path::Get_length() const { return length; }  // добавление вершины в конец void Path::push_back(int v, int l) {     path.push_back(v);     length += l; }  // добавление пути в конец void Path::push_back(Path p) {     for (size_t i = 0; i < p.Get_path().size(); ++i) {         int last = path[path.size() - 1];         if (last != p.Get_path()[i]) {             path.push_back(p.Get_path()[i]);         }     }     length += p.Get_length(); }  ostream& operator<<(ostream& os, const Path& p) {     size_t n = p.Get_path().size();     for (size_t i = 0; i < n; i++) {         os << p.Get_path()[i] + 1;         if (i != n - 1) {             os << "->";         } else {             os << " (";         }     }     os << p.Get_length() << ')' << endl;     return os; } 

Алгоритм Дейкстры

Обозначим все вершины бесконечностью, это будет означать, что вершина ещё не посещена, а начальную 0. Очевидно, что расстояние до самой себя равно 0. Теперь найдем расстояние из исходной точки в соседние, затем отметим её. Далее на каждом шаге находим минимальную не отмеченную точку и находим сумму с соседними, не отмеченными и сравниваем ее со значением вершины, если полученное расстояние меньше, то заменяем его.

Реализация алгоритма Дейкстры и решения задачи коммивояжёра

Алгоритм Дейкстры мы уже разобрали, но при его выполнении я также сразу сохраняю кратчайший путь от данной вершины к другим. Если вес вершины оказался больше, чем новый, то кроме нового веса, мы также заменяем путь: он будет равен пути к предыдущей вершине плюс сама вершина.

Все пути я записываю в словарь, ключом является вершина, а значением массив путей (i-ый элемент соответсвует кротчайшему пути до (i+1)-ой вершины).

После выполнения алгоритма Дейкстры с помощью next_permutation я перебираю порядок посещения вершин и нахожу путь минимальной длины.

Solution.h:

#ifndef SOLUTION_H #define SOLUTION_H  #include "Graph.h" #include "Path.h" #include <iostream> #include <limits> #include <vector> #include <map> #include <algorithm>  using namespace std;  class Solution { private:     int n;                                      // количество вершин     int** graph;                                // граф в виде двоичного массива n на n  public:     Solution(const Graph& g);     Path* Dijkstra(int v, Path* data);          // алгоритм Дейкстры     Path calc(vector<int> v, int start);        // вычисление оптимального пути  };  #endif 

Solution.cpp:

#include "Solution.h"  Solution::Solution(const Graph& g) {     graph = g.Get_data();     n = g.Get_n(); }  // алгоритм Дейкстры Path* Solution::Dijkstra(int v, Path* data) {     data = (Path*)calloc(n, sizeof(Path));      //пути от вершины v до других вершин графа     int distaces[n];                            //минимальное расстояние от вершины v до других     int out[n];                                 //посещенные вершины     for (int i = 0; i < n; ++i) {         if (i == v) {             distaces[i] = 0;             data[i] = Path(v);         } else {             distaces[i] = numeric_limits<int>::max();         }         out[i] = 0;     }      int min = numeric_limits<int>::max(), index = -1;     do {         min = numeric_limits<int>::max();         for (int i = 0; i < n; ++i) {             if ((out[i] == 0) && (distaces[i] < min)) {                 min = distaces[i];                 index = i;             }         }         if (min != numeric_limits<int>::max()) {             for (int i = 0; i < n; ++i) {                 if (graph[index][i] > 0) {                     int temp = min + graph[index][i];                     if (temp < distaces[i]) {                         distaces[i] = temp;                         data[i] = data[index];                         data[i].push_back(i, graph[index][i]);                     }                 }             }             out[index] = 1;         }     } while (min < numeric_limits<int>::max());      return data; }  // вычисление оптимального пути Path Solution::calc(vector<int> v, int start) {     map<int, Path*> distaces;     Path* temp = (Path*)calloc(n, sizeof(Path));     temp = Dijkstra(start, temp);     distaces[start] = temp;     for (size_t i = 0; i < v.size(); ++i) {         temp = Dijkstra(v[i], temp);         distaces[v[i]] = temp;     }      Path p = Path(start);     sort(v.begin(), v.end());     for (size_t i = 0; i < v.size(); ++i) {         int last = p.Get_path()[p.Get_path().size() - 1];         p.push_back(distaces[last][v[i]]);     }     int last = p.Get_path()[p.Get_path().size() - 1];     p.push_back(distaces[last][start]);     next_permutation(v.begin(), v.end());      do {         Path temp = Path(start);         for (size_t i = 0; i < v.size(); ++i) {             int last = temp.Get_path()[temp.Get_path().size() - 1];             temp.push_back(distaces[last][v[i]]);             if (temp.Get_length() > p.Get_length())                 break;         }         int last = temp.Get_path()[temp.Get_path().size() - 1];         temp.push_back(distaces[last][start]);          if (temp.Get_length() < p.Get_length()) {             p = temp;         }     } while (next_permutation(v.begin(), v.end()));      return p; } 

Итоги

Данное решение далеко от идеального, необходимо придумать более оптимальный метод нахождения порядка обхода вершин. Также в дальнейшем я оптимизирую алгоритм Дейкстры, уменьшив его сложность с помощью Фибоначчиевой кучи.

Проект задачи Вы можете скачать на GitHub.


ссылка на оригинал статьи https://habr.com/ru/post/650643/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *