Nano Hash - криптовалюты, майнинг, программирование

Википедия Алгоритм поиска пути A* занимает много времени

Я успешно реализовал поиск пути A* на С#, но он очень медленный, и я не понимаю, почему. Я даже пытался не сортировать список openNodes, но все равно.

Карта 80х80, узлов 10-11.

Я взял псевдокод отсюда Википедия

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

 public static List<PGNode> Pathfind(PGMap mMap, PGNode mStart, PGNode mEnd)
    {
        mMap.ClearNodes();

        mMap.GetTile(mStart.X, mStart.Y).Value = 0;
        mMap.GetTile(mEnd.X, mEnd.Y).Value = 0;

        List<PGNode> openNodes = new List<PGNode>();
        List<PGNode> closedNodes = new List<PGNode>();
        List<PGNode> solutionNodes = new List<PGNode>();

        mStart.G = 0;
        mStart.H = GetManhattanHeuristic(mStart, mEnd);

        solutionNodes.Add(mStart);
        solutionNodes.Add(mEnd);

        openNodes.Add(mStart); // 1) Add the starting square (or node) to the open list.

        while (openNodes.Count > 0) // 2) Repeat the following:
        {
            openNodes.Sort((p1, p2) => p1.F.CompareTo(p2.F));

            PGNode current = openNodes[0]; // a) We refer to this as the current square.)

            if (current == mEnd)
            {
                while (current != null)
                {
                    solutionNodes.Add(current);
                    current = current.Parent;
                }

                return solutionNodes;
            }

            openNodes.Remove(current);
            closedNodes.Add(current); // b) Switch it to the closed list.

            List<PGNode> neighborNodes = current.GetNeighborNodes();
            double cost = 0;
            bool isCostBetter = false;

            for (int i = 0; i < neighborNodes.Count; i++)
            {
                PGNode neighbor = neighborNodes[i];
                cost = current.G + 10;
                isCostBetter = false;

                if (neighbor.Passable == false || closedNodes.Contains(neighbor))
                    continue; // If it is not walkable or if it is on the closed list, ignore it.

                if (openNodes.Contains(neighbor) == false)
                {
                    openNodes.Add(neighbor); // If it isn’t on the open list, add it to the open list.
                    isCostBetter = true;
                }
                else if (cost < neighbor.G)
                {
                    isCostBetter = true;
                }

                if (isCostBetter)
                {
                    neighbor.Parent = current; //  Make the current square the parent of this square. 
                    neighbor.G = cost;
                    neighbor.H = GetManhattanHeuristic(current, neighbor);
                }
            }
        }

        return null;
    }

Вот эвристика, которую я использую:

    private static double GetManhattanHeuristic(PGNode mStart, PGNode mEnd)
    {
        return Math.Abs(mStart.X - mEnd.X) + Math.Abs(mStart.Y - mEnd.Y);
    }

Что я делаю не так? Целый день я смотрю на один и тот же код.


  • Каков размер вашей карты? Количество расширений зависит от расстояния до целевого узла, а это зависит от размера карты. 01.02.2011
  • Сколько узлов у вас в списке? Что именно медленно - сколько времени нужно, чтобы что-то сделать? 01.02.2011
  • Есть 11 комнат и 10 полных путей-соединений. Метод Pathfind занимает около 8-10 секунд. 01.02.2011
  • 10 секунд для 11 узлов? Это определенно слишком много, даже если ваша эвристика плоха. Вы должны исчерпать все узлы в худшем случае за несколько миллисекунд. В этом случае я действительно предлагаю использовать профилировщик для бедных, описанный в stackoverflow.com/questions/266373/ 01.02.2011
  • Пожалуйста; кто-нибудь поправьте меня, если я ошибаюсь, но у вас есть if (current == mEnd) { while (current!= null) { solutionNodes.Add(current); текущий = текущий.Родительский; } вернуть узлы решения; } Разве mEnd и mStart уже не находятся в списке узлов решения? И не будет ли ваша логика добавлять их снова? 01.02.2011
  • @Ben Voigt ответил на мою озабоченность. 01.02.2011

Ответы:


1

Во-первых, используйте профайлер. Используйте инструменты, чтобы сказать вам, что медленно; часто удивляет то, что медленно. И используйте отладчик. Создайте карту с пятью узлами и просмотрите каждую строку кода, пытаясь найти путь. Случилось что-то неожиданное? Если да, то разберитесь, что происходит.

Во-вторых, если оставить в стороне вашу проблему с производительностью, я думаю, что у вас также есть проблема с правильностью. Можете ли вы объяснить нам, почему вы считаете манхэттенское расстояние разумной эвристикой?

(Для тех, кто не знаком с этой метрикой: «расстояние до Манхэттена» или «расстояние на такси» — это расстояние между двумя точками, которое вам пришлось бы преодолеть, если бы вы жили в городе, расположенном на сетке. пройти 14 миль на северо-восток, вам придется пройти 10 миль на север, а затем 10 миль на восток, всего 20 миль.)

Для корректности алгоритма A* требуется, чтобы эвристика всегда занижала фактическое расстояние, необходимое для перемещения между двумя точками. Если на графике есть какие-либо «сокращенные по диагонали» улицы, то манхэттенское расстояние завышает расстояние по этим путям, и поэтому алгоритм не обязательно найдет кратчайший путь.

Почему вы не используете евклидово расстояние в качестве эвристики?

Вы пробовали свой алгоритм, используя «всегда ноль» в качестве эвристики? Это гарантированно занижение. (Это дает вам реализацию алгоритма Дейкстры.)

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

В-четвертых, в моем блоге есть реализация A* на C# 3, которую вы можете использовать; никаких явных или подразумеваемых гарантий, используйте на свой страх и риск.

http://blogs.msdn.com/b/ericlippert/archive/tags/astar/

Мой код очень прост; алгоритм в моей реализации выглядит так:

var closed = new HashSet<Node>();
var queue = new PriorityQueue<double, Path<Node>>();
queue.Enqueue(0, new Path<Node>(start));
while (!queue.IsEmpty)
{
    var path = queue.Dequeue();
    if (closed.Contains(path.LastStep)) continue;
    if (path.LastStep.Equals(destination)) return path;
    closed.Add(path.LastStep);
    foreach(Node n in path.LastStep.Neighbours)
    {
        double d = distance(path.LastStep, n);
        var newPath = path.AddStep(n, d);
        queue.Enqueue(newPath.TotalCost + estimate(n), newPath);
    }
}

Идея состоит в том, что мы поддерживаем приоритетную очередь путей; то есть очередь путей, которая всегда может указать вам путь до сих пор с наименьшим расстоянием. Затем мы проверяем, добрались ли мы до пункта назначения; если да, то мы закончили. Если нет, то мы ставим в очередь кучу новых путей на основе их (недо)оценочного расстояния до цели.

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

01.02.2011
  • Можете ли вы объяснить нам, почему вы считаете манхэттенское расстояние разумной эвристикой? Поскольку он не движется по диагонали (расширяя четырех соседей, см. комментарий к stackoverflow.com/questions/4864945/), Манхэттен верен. 01.02.2011
  • Что такое расстояние и что такое оценка? Они оба используют евклидов? 01.02.2011
  • @Vee: расстояние — это любая функция, которая берет два узла и вычисляет точное расстояние между ними; оценка — это функция, которая берет один узел и оценивает расстояние от этого узла до целевого узла. То, как эти методы выполняют свою работу, не имеет отношения к реализации A*, если они работают правильно и эффективно. Они могли бы быть евклидовым расстоянием, если бы это имело смысл. Помните, что A* можно использовать для поиска путей в произвольных графах; это не обязательно должна быть карта реального мира. Евклидова метрика имеет смысл только в системе координат. 01.02.2011
  • Если диагональное движение разрешено с той же стоимостью, что и движение по сетке, евклидово расстояние (2-норма) не является правильной метрикой. Бесконечность-норма (max(abs(x1-x2),abs(y1-y2))) есть. Когда диагональное движение запрещено, манхэттенское расстояние (1-норма, abs(x1-x2)+abs(y1-y2)) является правильным. 02.02.2011

  • 2

    Пара замечаний:

    List<T> не оптимизирован для удаления первого элемента. Лучше было бы отсортировать в обратном порядке и взять последний элемент. Или используйте Stack<T> или Queue<T>.

    List.Remove(current) крайне неэффективен. Вы уже знаете индекс, который хотите удалить, не ищите элемент по всему списку.

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

    Основная операция, которую вы выполняете на closedNodes, — это проверка присутствия, closedNodes.Contains(). Используйте структуру данных, оптимизированную для этого, например. Set<T>. Или еще лучше, поместите закрытое поле флага в каждый узел и очистите их все в начале каждого прохода. Это будет значительно быстрее, чем линейный поиск по closedNodes на каждой итерации.

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

    neighborNodes может быть IEnumerable<T> вместо List<T>, так как вам никогда не понадобится весь список сразу. Использование foreach также будет немного быстрее, чем перечисление списка по индексу.

    01.02.2011


    4

    Вы вычисляете стоимость узла обхода следующим образом:

    cost = current.G + 10;
    

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

    Еще одна "деталь", которая может быть неправильной: current.GetNeighborNodes. Как это реализовано? Возвращает ли он тот же узел, что и должен, для одного и того же местоположения, так что один и тот же узел на разных путях является общим, или он всегда выделяет новый узел, используя new?

    01.02.2011
  • current.GetNeighborNodes возвращает 4 соседних уже существующих узла. Вместе с картой создается массив узлов 80x80. 01.02.2011
  • ХОРОШО. А эвристика? Если расстояние между узлами равно 1, а стоимость равна 10, эвристика должна быть расстоянием * 10, а не расстоянием. С занижением эвристики ваш поиск будет близок к Джисктре. 01.02.2011

  • 5

    Каково потребление памяти? Загрузите инструменты Red Gate. Используйте профилировщик производительности, чтобы увидеть, на что тратится больше всего времени, и профилировщик памяти, чтобы определить, есть ли у вас какие-либо проблемы с утечками памяти или недостаточно быстрым удалением объекта.

    Как отметил @Rodrigo, у вас может быть большая карта для работы. Вложенные циклы никогда не должны быть производительными.

    01.02.2011

    6

    Используете ли вы сетку для представления местности? Если это так, то лучшей эвристикой в ​​​​этом случае является Octile:

    Эвристическая стоимость = (минимум (разница в x, разница в y) * квадратный корень из 2 + max (разница в x, разница в y) - min (разница в x, разница в y))

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

    Еще один полезный совет — выбрать структуру данных для открытого списка в соответствии с размером вашей карты. Если ваша карта относительно небольшая (100 x 100), то несортированный вектор будет самым быстрым способом. Чтобы удалить элементы, просто замените итератором последний элемент на тот, который вы хотите удалить, и вызовите pop_back. Если у вас большая карта, используйте кучу. Вы заботитесь только о самом дешевом узле, поэтому сортировка всего остального не принесет пользы. Куча вставляет и сортирует со сложностью log n, идеально подходящей для средних и больших наборов данных, но медленной для маленьких.

    Наконец, если скорость так важна, используйте поиск точки перехода. Это в среднем в 20-30 раз быстрее, чем поиск пути A *, без накладных расходов на память (или, как утверждается в исследовательской статье, не было найдено никаких доказательств этого). По сути, он заменит шаг «найти соседей» в A* на «найти преемников», поэтому включение его в ваш код должно быть относительно простым.

    Надеюсь, это поможет.

    25.06.2013
    Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

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

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..