Оптимизация алгоритма Дейкстры

Мне нужен алгоритм поиска по графу, которого достаточно в нашем приложении для навигации роботов, и я выбрал алгоритм Дейкстры.

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

Поскольку выполнение алгоритма Дейкстры от начала к цели дало бы нам обратный путь, идущий от цели к началу, я решил выполнить алгоритм Дейкстры в обратном порядке, чтобы получить путь от начала к цели.

Начиная с целевой ячейки, у меня будет 8 соседей, стоимость которых по горизонтали и вертикали равна 1, а по диагонали будет sqrt (2), только если ячейки достижимы (т. Е. Не за пределами и свободная ячейка).

Вот правила, которые следует соблюдать при обновлении соседних ячеек, текущая ячейка может предполагать, что доступны только 8 соседних ячеек (например, расстояние 1 или sqrt(2)) со следующими условиями:

  1. Соседняя ячейка не выходит за пределы
  2. Соседняя ячейка не посещается.
  3. Соседняя ячейка является свободной ячейкой, которую можно проверить с помощью двухмерной карты сетки.

Вот моя реализация:

#include <opencv2/opencv.hpp>
#include <algorithm>
#include "Timer.h"

/// CONSTANTS
static const int UNKNOWN_CELL  = 197;
static const int FREE_CELL     = 255;
static const int OCCUPIED_CELL = 0;

/// STRUCTURES for easier management.
struct vertex {
    cv::Point2i id_;
    cv::Point2i from_;

    vertex(cv::Point2i id, cv::Point2i from)
    {
        id_ = id;
        from_ = from;
    }
};

/// To be used for finding an element in std::multimap STL.
struct CompareID
{
    CompareID(cv::Point2i val) : val_(val) {}
    bool operator()(const std::pair<double, vertex> & elem) const {
        return val_ == elem.second.id_;
    }
private:
    cv::Point2i val_;
};

/// Some helper functions for dijkstra's algorithm.
uint8_t get_cell_at(const cv::Mat & image, int x, int y)
{
    assert(x < image.rows);
    assert(y < image.cols);
    return image.data[x * image.cols + y];
}

/// Some helper functions for dijkstra's algorithm.
bool checkIfNotOutOfBounds(cv::Point2i current, int rows, int cols)
{
    return (current.x >= 0 && current.y >= 0 &&
            current.x < cols && current.y < rows);
}

/// Brief: Finds the shortest possible path from starting position to the goal position
/// Param gridMap: The stage where the tracing of the shortest possible path will be performed.
/// Param start: The starting position in the gridMap. It is assumed that start cell is a free cell.
/// Param goal: The goal position in the gridMap. It is assumed that the goal cell is a free cell.
/// Param path: Returns the sequence of free cells leading to the goal starting from the starting cell.
bool findPathViaDijkstra(const cv::Mat& gridMap, cv::Point2i start, cv::Point2i goal, std::vector<cv::Point2i>& path)
{
    // Clear the path just in case
    path.clear();
    // Create working and visited set.
    std::multimap<double,vertex> working, visited;

    // Initialize working set. We are going to perform the djikstra's
    // backwards in order to get the actual path without reversing the path.
    working.insert(std::make_pair(0, vertex(goal, goal)));

    // Conditions in continuing
    // 1.) Working is empty implies all nodes are visited.
    // 2.) If the start is still not found in the working visited set.
    // The Dijkstra's algorithm
    while(!working.empty() && std::find_if(visited.begin(), visited.end(), CompareID(start)) == visited.end())
    {

        // Get the top of the STL.
        // It is already given that the top of the multimap has the lowest cost.
        std::pair<double, vertex> currentPair = *working.begin();
        cv::Point2i current = currentPair.second.id_;
        visited.insert(currentPair);
        working.erase(working.begin());

        // Check all arcs
        // Only insert the cells into working under these 3 conditions:
        // 1. The cell is not in visited cell
        // 2. The cell is not out of bounds
        // 3. The cell is free
        for (int x = current.x-1; x <= current.x+1; x++)
            for (int y = current.y-1; y <= current.y+1; y++)
            {

                if (checkIfNotOutOfBounds(cv::Point2i(x, y), gridMap.rows, gridMap.cols) &&
                        get_cell_at(gridMap, x, y) == FREE_CELL &&
                        std::find_if(visited.begin(), visited.end(), CompareID(cv::Point2i(x, y))) == visited.end())
                {
                    vertex newVertex = vertex(cv::Point2i(x,y), current);
                    double cost = currentPair.first + sqrt(2);
                    // Cost is 1
                    if (x == current.x || y == current.y)
                        cost = currentPair.first + 1;
                    std::multimap<double, vertex>::iterator it =
                            std::find_if(working.begin(), working.end(), CompareID(cv::Point2i(x, y)));
                    if (it == working.end())
                        working.insert(std::make_pair(cost, newVertex));
                    else if(cost < (*it).first)
                    {
                        working.erase(it);
                        working.insert(std::make_pair(cost, newVertex));
                    }
                }
            }
    }

    // Now, recover the path.
    // Path is valid!
    if (std::find_if(visited.begin(), visited.end(), CompareID(start)) != visited.end())
    {
        std::pair <double, vertex> currentPair = *std::find_if(visited.begin(), visited.end(), CompareID(start));
        path.push_back(currentPair.second.id_);
        do
        {
            currentPair = *std::find_if(visited.begin(), visited.end(), CompareID(currentPair.second.from_));
            path.push_back(currentPair.second.id_);
        } while(currentPair.second.id_.x != goal.x || currentPair.second.id_.y != goal.y);
        return true;
    }
    // Path is invalid!
    else
        return false;

}

int main()
{
    //    cv::Mat image = cv::imread("filteredmap1.jpg", CV_LOAD_IMAGE_GRAYSCALE);
    cv::Mat image = cv::Mat(100,100,CV_8UC1);
    std::vector<cv::Point2i> path;

    for (int i = 0; i < image.rows; i++)
        for(int j = 0; j < image.cols; j++)
        {
            image.data[i*image.cols+j] = FREE_CELL;

            if (j == image.cols/2 && (i > 3 && i < image.rows - 3))
                image.data[i*image.cols+j] = OCCUPIED_CELL;

            //            if (image.data[i*image.cols+j] > 215)
            //                image.data[i*image.cols+j] = FREE_CELL;
            //            else if(image.data[i*image.cols+j] < 100)
            //                image.data[i*image.cols+j] = OCCUPIED_CELL;
            //            else
            //                image.data[i*image.cols+j] = UNKNOWN_CELL;
        }

    // Start top right
    cv::Point2i goal(image.cols-1, 0);
    // Goal bottom left
    cv::Point2i start(0, image.rows-1);

    // Time the algorithm.
    Timer timer;
    timer.start();
    findPathViaDijkstra(image, start, goal, path);
    std::cerr << "Time elapsed: " << timer.getElapsedTimeInMilliSec() << " ms";


    // Add the path in the image for visualization purpose.
    cv::cvtColor(image, image, CV_GRAY2BGRA);
    int cn = image.channels();
    for (int i = 0; i < path.size(); i++)
    {
        image.data[path[i].x*cn*image.cols+path[i].y*cn+0] = 0;
               image.data[path[i].x*cn*image.cols+path[i].y*cn+1] = 255;
               image.data[path[i].x*cn*image.cols+path[i].y*cn+2] = 0;

    }
    cv::imshow("Map with path", image);
    cv::waitKey();
    return 0;
}

Для реализации алгоритма я решил иметь два набора, а именно посещенный и рабочий набор, каждый элемент которого содержит:

  1. Местоположение самого себя на карте 2D-сетки.
  2. Накопленная стоимость
  3. Через какую ячейку он получил накопленную стоимость (для восстановления пути)

И вот результат:

введите здесь описание изображения

Черные пиксели представляют собой препятствия, белые пиксели представляют собой свободное пространство, а зеленая линия представляет собой вычисленный путь.

В этой реализации я бы искал только минимальное значение в текущем рабочем наборе, и мне НЕ нужно было сканировать всю матрицу стоимости (где изначально первоначальная стоимость всех ячеек установлена ​​​​на бесконечность, а начальная точка - 0). Я думаю, что сохранение отдельного вектора рабочего набора обещает лучшую производительность кода, потому что все ячейки, стоимость которых равна бесконечности, наверняка не будут включены в рабочий набор, а только те ячейки, которые были затронуты.

Я также воспользовался STL, который предоставляет C++. Я решил использовать std::multimap, так как он может хранить дублирующиеся ключи (что дорого обходится) и автоматически сортирует списки. Однако я был вынужден использовать std::find_if() для поиска идентификатора (который является строкой, столбцом текущей ячейки в наборе) в посещаемом наборе, чтобы проверить, находится ли в нем текущая ячейка, что обещает линейную сложность. Я действительно думаю, что это узкое место алгоритма Дейкстры.

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

Какое улучшение я могу выполнить? Какой STL наиболее подходит для этого алгоритма? В частности, как мне улучшить узкое место?


person Xegara    schedule 16.10.2014    source источник
comment
Если вам действительно нужен эффективный поиск по двум разным ключам, вы можете рассмотреть Boost Multi Index Container с двумя индексами или просто добавить вспомогательный контейнер индексации, который отображает, например. ячейки итераторам в мультикарте.   -  person John Zwinck    schedule 16.10.2014
comment
Я могу использовать это для поиска. Но могу ли я также использовать его для определения наличия элемента в списке? Есть 2 случая, когда мне нужно было узнать, содержит ли рабочий/посещенный набор определенный идентификатор?   -  person Xegara    schedule 16.10.2014


Ответы (2)


Вы используете std::multimap для «работы» и «посещения». Это не здорово.

Первое, что вы должны сделать, это изменить visited на флаг для каждой вершины, чтобы вы могли выполнять свои find_if за постоянное время, а не за линейное время, а также чтобы операции над списком посещенных вершин выполнялись за постоянное, а не за логарифмическое время. Вы знаете, что такое все вершины, и можете тривиально сопоставить их с небольшими целыми числами, поэтому вы можете использовать либо std::vector, либо std::bitset.

Второе, что вы должны сделать, это превратить working в приоритетную очередь, а не в структуру сбалансированного двоичного дерева, чтобы операции выполнялись на (большой) постоянный фактор быстрее. std::priority_queue — это базовая двоичная куча. Куча с более высоким основанием — скажем, четверичная для конкретности — вероятно, будет быстрее на современных компьютерах из-за ее меньшей глубины. Эндрю Голдберг предлагает некоторые структуры данных на основе сегментов; Я могу откопать ссылки для вас, если вы доберетесь до этой стадии. (Они не слишком сложны.)

После того, как вы позаботились об этих двух вещах, вы можете рассмотреть приемы A* или встречи посередине, чтобы еще больше ускорить процесс.

person tmyklebu    schedule 16.10.2014
comment
Здорово, что признано, что моя реализация неоптимальна, потому что это действительно так, но я не знаю, как ее улучшить. Я просто хочу прояснить некоторые вещи: 1.) Что означает флаг для каждой вершины и как он обещает постоянное время вместо линейного времени? Не найдет ли он элементы линейным поиском? 2.) Если я собираюсь преобразовать посещенный набор либо в std::vector, либо в std::bitset, могу ли я сделать его элементы парой, потому что мне нужно сохранить его предыдущую ячейку? Будет ли добавление незначительным в производительности? 3.) Используя priority_queue, я думаю, мне не нужно обновлять расходы? - person Xegara; 16.10.2014
comment
@Xegara: индексация массива целым числом занимает постоянное время. Иметь массив, индексы которого являются вершинами. Сделайте i-й элемент равным 1, если вершина была посещена, и 0 в противном случае. Таким образом, изменения и обновления «посещенных» занимают постоянное время. Если вам нужны обратные указатели, создайте другой массив для хранения обратных указателей. (Или иметь массив структур для хранения как «посещенных или нет», так и обратного указателя.) - person tmyklebu; 16.10.2014
comment
Если вы используете priority_queue, вы не сможете обновить стоимость. Это просто прекрасно — Dijkstra не ломается, если вы не обновляете его; это просто вызовет несколько дополнительных посещений вершин. Если вы напишете свою собственную очередь приоритетов, вы сможете обновлять стоимость, если у вас есть структура данных, которая позволяет это делать. (По моему опыту, на самом деле не стоит заморачиваться.) - person tmyklebu; 16.10.2014
comment
Я применил его, и он успешно вычислил путь всего за 5,319 мс по сравнению с 4237,34 мс! Это примерно в 800 раз быстрее. Спасибо чувак! :) - person Xegara; 16.10.2014

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

Я слышал, как вы спрашивали «как я могу сделать это лучше, не меняя своих мыслей», но, тем не менее, я расскажу вам совершенно другой и лучший подход.

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

Алгоритм выглядит следующим образом: (0) Представьте свои препятствия в виде многоугольников, перечислив углы. Работайте с реальными числами, чтобы сделать их такими тонкими, как вам нравится. (1) Попробуйте провести прямую линию между конечными точками. (2) Проверьте, проходит ли эта линия через препятствие или нет. Чтобы сделать это для любой линии, покажите, что все углы любого конкретного препятствия лежат по одну сторону от линии. Для этого переместите все точки на (-X,-Y) одного конца линии так, чтобы эта точка находилась в начале координат, а затем поверните, пока другая точка не окажется на оси X. Теперь все углы должны иметь одинаковый знак Y, если нет препятствий. Там может быть более быстрый способ, просто используя градиенты. (3) Если есть препятствие, предложите N двухсегментных путей, проходящих через N углов препятствия. (4) Рекурсия для всех сегментов, отбраковка всех путей с сегментами, выходящими за границы. Это не будет проблемой, если у вас нет препятствий, которые выходят за пределы. (5) Когда он перестанет повторяться, у вас должен быть список локально оптимизированных путей, из которых вы можете выбрать самый короткий. (6) Если вы действительно хотите ограничить пеленги кратными 45 градусам, то вы можете сначала выполнить этот алгоритм, а затем заменить каждый сегмент любой волнистой версией, состоящей только из 45, которая позволяет избежать препятствий. Мы знаем, что такая версия существует, потому что вы можете оставаться очень близко к исходной линии, очень часто покачиваясь. Мы также знаем, что все такие извилистые пути имеют одинаковую длину.

person Adrian May    schedule 16.10.2014
comment
Это не совсем работает. Проблема в вашей жадной эвристике, позволяющей избежать любых препятствий, которые мешают мне идти прямо к цели. Вы можете заставить это работать сколь угодно плохо с препятствиями неприятной формы. Тем не менее, ваше общее представление о том, что вы делаете, довольно хорошее. Вы можете изменить этот подход (первый шаг — уродливая геометрическая задача, второй шаг — Дейкстра на уменьшенном графе), чтобы он работал лучше. - person tmyklebu; 16.10.2014
comment
Пожалуйста, приведите пример - я не знаю формы, которая нарушает этот алгоритм. - person Adrian May; 16.10.2014
comment
Если я вас правильно понял, вы говорите, что мой алгоритм можно улучшить, отбрасывая путь A-P-Q-Z, если A-Q-Z уже добрался до дома. Верно? Это не превратит APZ в APQZ, если первый уже вернулся домой, потому что он возвращается только при наличии препятствия, но может превратить AQZ в APQZ, если Q не находится в поле зрения A или Z. Поэтому нам нужно отсеять этот путь раньше. . Это то, что вы имели в виду? - person Adrian May; 16.10.2014
comment
Чистая сетка, за исключением чего-то с двумя наборами зубов посередине. Левый и правый наборы зубов имеют соответствующий, взаимосвязанный набор, что делает путь между зубами длинным. Оставьте свободный путь вокруг всего рта, чтобы общее кратчайшее расстояние пути было коротким. - person tmyklebu; 16.10.2014
comment
Мой алгоритм оценит путь между зубами, но сделает это довольно эффективно. Я предполагаю, что это может прервать рекурсию по путям, которые уже длиннее, чем какой-то уже успешный путь, потому что мы знаем, что рекурсия не делает их короче. Если бы мы хотели этого, то это была бы итеративная, идущая в ширину вещь, где каждая итерация делит пополам все заблокированные сегменты, а затем отбрасывает все, что хуже, чем глобальное лучшее на данный момент число. - person Adrian May; 16.10.2014
comment
Он возьмет путь между зубами, что является проблемой. - person tmyklebu; 16.10.2014
comment
Я так не думаю. При первом препятствии на прямом пути генерирует пути через ВСЕ вершины препятствия. В том числе и подбородок. - person Adrian May; 16.10.2014
comment
Здесь есть три препятствия. - person tmyklebu; 16.10.2014
comment
Тогда я не понимаю твою картину. Я представлял себе две заглавные буквы E, обращенные друг к другу, с переплетенными зубцами, так что между ними есть извилистый маршрут. Что это за фигура с тремя препятствиями? - person Adrian May; 16.10.2014
comment
Давайте продолжим обсуждение в чате. - person Adrian May; 16.10.2014