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

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

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

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

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

Граф с 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 {
    int** d = new int*[n];
    for (int i = 0; i < n; ++i) {
        d[i] = new int[n];
        for (int j = 0; j < n; ++j) {
            d[i][j] = data[i][j];
        }
    }
    return d;
}

// количество вершин графа
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:
    Graph graph;                                // граф

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;
}

// алгоритм Дейкстры
Path* Solution::Dijkstra(int v, Path* data) {
    data = (Path*)calloc(graph.Get_n(), sizeof(Path));      //пути от вершины v до других вершин графа
    int distaces[graph.Get_n()];                            //минимальное расстояние от вершины v до других
    int out[graph.Get_n()];                                 //посещенные вершины
    for (int i = 0; i < graph.Get_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 < graph.Get_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 < graph.Get_n(); ++i) {
                if (graph.Get_data()[index][i] > 0) {
                    int temp = min + graph.Get_data()[index][i];
                    if (temp < distaces[i]) {
                        distaces[i] = temp;
                        data[i] = data[index];
                        data[i].push_back(i, graph.Get_data()[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(graph.Get_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;
}

Итоги

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

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

Комментарии (8)


  1. desertkun
    09.02.2022 15:55
    +6

    Хорошая статья, пишите еще, например, решение задачи по сортировке пузырьком


  1. MarazmDed
    09.02.2022 16:02
    +8

    Опять фейспалм...

    Сказано про "решение далеко от идеального" и ни слова о сложности самой задачи коммивояжера! Так-то она NP-трудная. И за идеальное решение вы получите миллион долларов - решение задачи тысячилетия.

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

    Не описано вообще ничего: ни что такое графы, ни как их можно представить...


  1. DSarovsky
    09.02.2022 16:42
    +7

    Игнорируя, что в сети итак полно разборов этой задачи, допускаю, что начинающие C++ программисты вполне могут на Вашу статью натолкнуться и подчерпнуть, помимо, возможно, полезной (что, честно говоря, сомнительно) информации, ряд недочетов, связанных именно с языком программирования:

    • лишние включения в заголовочных файлах, которые нужны только в соответствующем *.cpp;
    • using namespace std; в заголовочном файле;
    • хардкод и магические числа (например, конструктор класса Graph);
    • возврат неконстатного указателя на данные в константном методе класса;
    • смешивание кода на языке C с кодом на C++. Если в вашем решении важно, что в одном классе используются именно массивы, а в другом подошел и std::vector, то это стоит пояснить, так как статья явно ориентирована на начинающих;
    • сложилось впечатление, что не было оценки возможного количества вершин: почему-то матрица у вас в динамической области, а путь (поле класса Path) в стеке. Если так задумано, то стоит пояснить;
    • возврат контейнеров по значению — слишком жирно и вряд ли нужно;
    • странная композиция классов, почему внутри Solution своя матрица для графа, а не поле типа Graph?
    • в конструкторе Solution присваивание просто указателя (то есть создание плоской копии). Это так задумано, что нигде деструкторов нет и поэтому работает или все-таки недоработка?
    • как это в методе Solution::Dijkstra вы создаете статический массив неизвестного размера? Раз материалы и код ориентирован на начинающих, хорошо бы, чтобы проект все же компилировался.

    Даже после беглого просмотра кода (а кроме него в статье, в общем-то, ничего нет) получилось найти очевидные замечания (про стиль не стал писать, это уже более субъективно).
    Еще раз хочется отметить, что наличие подобных материалов — это хорошо, потому что у тех, кто начинает изучать программирование (и в частности C++) больший выбор. Вместе с тем появляется дополнительная ответственность: не научить человека писать код неправильно.


    1. holydel
      09.02.2022 19:12

      У меня почему-то тоже названия методов типа Get_data вызывают отторжение. GetData норм. get_data тоже норм. Что не так с Get_data-ой - не понимаю.


      1. DSarovsky
        09.02.2022 19:25
        +2

        Возможно, это отсылка к "Маленькому принцу", только тут змея съела не слона, а верблюда :)


    1. Muzahim Автор
      09.02.2022 21:28

      Спасибо за комментарий! Это моя первая статья и я учту все замечания.


      1. DSarovsky
        10.02.2022 09:08

        Это здорово, тем более, хабр предоставляет возможность редактирования (то есть их можно учесть прямо в этой статье).


  1. cr0nk
    10.02.2022 13:46

    ... Path Solution::calc(vector<int> v, int start) ...

    Какой нигодяй Вас учил передавать массив он же вектор копией?