Алгоритм кратчайшего пути

У меня есть код, который применяет алгоритм Дейкстры для списка смежности.

Данные берутся из файла. В файле первая строка содержит количество вершин и ребер, а остальные строки содержат по три элемента каждая (первые 2 — это номера вершин, а 3-я — вес ребер).

Вывод записывается в другой файл. Основная проблема — долгое чтение (добавление в список) элементов — 8 секунд против 2 по правилам правильного теста. В файле для чтения таких строк 400 000. Как мне ускорить чтение?

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <fstream>
#include <list>
#include <vector>
#include <queue>
using namespace std;
# define INF 0x3f3f3f3f
typedef pair<int, int> iPair;

class Graph
{
    int V;  
    list< pair<int, int> >* adj;
public:
    Graph(int V);  
    void addEdge(int u, int v, int w);
    void shortestPath(int s);
};

Graph::Graph(int V)
{
    this->V = V;
    adj = new list<iPair>[V];
}

void Graph::addEdge(int u, int v, int w)
{
    adj[u].push_back(make_pair(v, w));
    adj[v].push_back(make_pair(u, w));
}


void Graph::shortestPath(int src)
{
    priority_queue< iPair, vector <iPair>, greater<iPair> > pq;
    vector<int> dist(V, INF);
    pq.push(make_pair(0, src));
    dist[src] = 0;
    while (!pq.empty())
    {
        int u = pq.top().second;
        pq.pop();
        list< pair<int, int> >::iterator i;
        for (i = adj[u].begin(); i != adj[u].end(); ++i)
        {
            int v = (*i).first;
            int weight = (*i).second;
            if (dist[v] > dist[u] + weight)
            {
                dist[v] = dist[u] + weight;
                pq.push(make_pair(dist[v], v));
            }
        }
    }
    ofstream file1("C:\Tests\123.txt");
    for (int i = 0; i < V; ++i)
        cout << dist[i] << " ";
    file1.close();
}

int main()
{
    ifstream file("C:\Tests\21");
    int number_of_veretexes, edges;
    file >> number_of_veretexes >> edges;
    Graph g(number_of_veretexes);
    int _first_, _second_, _third_;
    for (int i = 0; i < edges; i++) {
        file >> _first_ >> _second_ >> _third_;
        g.addEdge( _first_ - 1, _second_ - 1, _third_);
    }
    g.shortestPath(0);
    file.close();
}

3 ответа
3

Избегать using namespace std

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

Использовать INT_MAX вместо INF

Не надо #define INF к некоторому произвольному значению, просто установите его на максимально возможное значение int может иметь. Это уже определено как INT_MAX в <climits>.

Создать struct Edge

С помощью std::pair<int, int> это удобно, тем более что их можно сортировать, но у него есть некоторые недостатки. Просто взглянув на определение этого типа, неясно, является ли первый элемент номером узла, а второй — весом или наоборот. Фактически вы используете оба соглашения в своем коде: список смежности имеет номер вершины первой, тогда как очередь с приоритетом имеет номер вершины второй. Создайте правильный struct чтобы устранить двусмысленность и повысить безопасность типов. Кроме того, поскольку ребро является свойством графа, определите struct Edge внутри class Graph, вот так:

class Graph {
    struct Edge {
        int vertex;
        int weight;
    };

    std::list<Edge>* adj;
    ...
};

Вместо звонка adj[u].push_back(std::make_pair(v, w)), теперь вы должны написать:

adj[u].push_back({v, w});

Для приоритетной очереди вы также хотите создать отдельный struct для элементов. Поскольку только Graph::shortestPath() использует это, вы можете определить это внутри этой функции. Чтобы его можно было сортировать, определите operator<() в этой структуре, вот так:

void Graph::shortestPath(int src) {
    struct VertexDistance {
         int vertex;
         int distance;
         bool operator<(const VertexDistance &other) {
             if (distance != other.distance)
                 return distance > other.distance;
             else
                 return vertex > other.vertex;
         }
    };

    std::priority_queue<VertexDistance> pq;
    ...
}

Избегайте ручного выделения памяти

Немного странно видеть, что вы используете контейнеры STL, такие как std::list и std::vector и std::priority_queue, но затем используйте new выделить массив adj. Почему бы не использовать std::vector для этого тоже? Например, вот так:

std::vector<std::list<Edge>> adj;

Это позволяет избежать необходимости new, и исправляет утечку памяти, которая у вас есть, потому что вы никогда не вызываете delete. Он также позволяет удалить переменную-член V, поскольку вектор уже отслеживает свой размер.

Вы можете облегчить чтение, создав псевдоним для списка ребер:

using AdjacencyList = std::list<Edge>;
std::vector<AdjacencyList> adj;

Использовать std::vector вместо std::list

std::vector и std::list оба могут хранить список элементов, но имеют разные компромиссы в производительности, требованиях к памяти и других аспектах. В вашем коде вы всегда добавляете элементы только в конец списка смежности, вы никогда не вставляете или не удаляете элементы из середины. В этом случае std::vector — это гораздо более эффективная структура данных, поэтому я бы просто написал:

using AdjacencyList = std::vector<Edge>;
std::vector<AdjacencyList> adj;

Использовать на основе диапазона for петли

Если вы не можете использовать какие-либо функции из C ++ 11 или новее, я настоятельно рекомендую вам начать использовать на основе диапазона for-петли. Они экономят набор текста, делают код более читабельным и уменьшают вероятность ошибок. Например, при переборе списка смежности в Graph::shortestPath(), ты можешь написать:

for (auto &edge: adj[u]) {
     int v = edge.vertex;
     int weight = edge.weight;
     ...
}

То же самое касается печати содержимого dist:

for (auto &distance: dist)
    file << distance << " ";

Проверить на наличие ошибок при чтении и записи файлов

Многие вещи могут пойти не так при чтении и записи в файлы: файлы могут не существовать, быть поврежденными, у вас может не быть разрешений на чтение и / или запись, диск может быть заполнен на полпути при записи файла и т. Д. Поэтому вы должны убедиться, что вы полностью прочитали и записали файлы без ошибок.

Вам не нужно проверять каждую операцию ввода-вывода; файловые объекты будут помнить, возникала ли в прошлом ошибка. Таким образом, самый простой способ получить правильную проверку ошибок — это сделать это после чтения или записи всего файла. Это можно сделать, проверив Биты состояния ввода / вывода. В вашем случае просто проверяю, что good() возвращается true должно быть достаточно, например, для входного файла:

int main() {
    ifstream file("...");
    ...
    if (!file.good()) {
        std::cerr << "An error occured while reading the input!n";
        return 1;
    }

    g.shortestPath(0);
}

И вот так для выходного файла:

void Graph::shortestPath(int src) {
    ...
    ofstream file("...");
    ...
    file.close();
    if (!file.good()) {
        std::cerr << "An error occured while writing the output!n";
        // TODO: ensure the error propagates to main, or call exit(1)
    }
}

Важно позвонить close() сначала здесь, поскольку закрытие потока гарантирует, что его выходной буфер будет полностью очищен.

Когда вы обнаруживаете ошибку, важно, чтобы вы не продолжали использовать какие-либо неверные данные, которые вы могли получить, чтобы вы распечатали полезное сообщение об ошибке для std::cerr, и это если ваша программа возвращает ненулевой код выхода. Последнее важно в том случае, если ваша программа вызывается, например, из сценария, поэтому сценарий не будет продолжать работу с поддельными данными.

Переместить вывод файла из Graph::shortestPath()

Следуя принципу разделение проблем, вам следует удалить функцию вывода файлов из shortestPath(). Вместо этого сделайте эту функцию return кратчайший путь, и позвольте вызывающему абоненту записать его на диск. Это можно просто сделать так:

std::vector<int> Graph::shortestPath(int src)
{
    ...
    while (...) {
        ...
    }

    return dist;
}

int main()
{
    ...
    auto path = g.shortestPath(0);
    std::ofstream file1(...);

    for (auto &distance: path)
        file1 << distance < " ";

    file1.close();

    if (!file1.good()) {
        std::cerr << "An error occured while writing the output!n";
        return 1;
    }
}

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

int main() {
    auto graph = readGraph("C:\Tests\21");
    auto path = graph.shortestPath(0);
    writePath(path, "C:\Tests123.txt");
}

    Все @G. — сказал Слипен.

    Небольшое изменение в алгоритме.

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

    Ниже представлена ​​более традиционная реализация Дейкстры.

    vector<int> Graph::shortestPath(int src)
    {
        priority_queue< iPair, vector <iPair>, greater<iPair> > pq;
        vector<int> dist(V, INT_MAX);
    
        pq.push(make_pair(0, src));
    
        while (!pq.empty())
        {
            int u    = pq.top().second;
            int d2u  = pq.top().first;
            pq.pop();
    
            // You were missing this first test.
            // And the following update to `dist[u]`
            if (dist[u] != INT_MAX) {
                // Already done this node we can ignore.
                continue;
            }
    
            // First time we made it to this node.
            // Record the distance (it must be the shortest as pq is priority based).
            dist[u] = d2u;
            
            // Loop over all edges from this node.
            for (auto const& item: adj[u])
            {
                int v      = item.first;
                int weight = item.second;
    
                // Optimization. Don't need this as pq is priority based.
                //               May or may not help.
                if (dist[v] != INT_MAX)
                {
                    pq.push({d2u + weight, v});
                }
            }
        }
        return dist;
    }
    

      Я подозреваю что push_back намного дороже, чем push. (Или, может быть, наоборот?) (Я предполагаю, что это включает в себя массив (вектор), и дешевле прикрепить его к концу, чем сдвинуть все содержимое, чтобы освободить место в начале.)

      Итак, вместо того, чтобы делать push_back, делать push. Затем, когда закончите, переверните содержимое вектора.

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

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