2011-02-01 2 views
3

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

Карта 80x80, а также 10-11 узлов.

Я взял псевдокод здесь Wikipedia

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

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

Что я делаю неправильно? Это целый день, когда я продолжаю смотреть на тот же код.

+1

Каков размер вашей карты? Количество расширений зависит от расстояния до целевого узла и зависит от размера карты. –

+0

Сколько у вас узлов в списке? Что именно «медленно» - сколько времени требуется, чтобы что-то сделать? – Suma

+0

Карта 80x80. –

ответ

16

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

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

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

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

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

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

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

В-четвертых, у меня есть реализация 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); 
    } 
} 

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

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

+2

«Не могли бы вы объяснить нам, почему вы считаете, что расстояние Манхэттена является разумной эвристикой?«Поскольку он не перемещается по диагонали (расширяя четырех соседей, см. Комментарий к http://stackoverflow.com/questions/4864945/wikipedia-a-pathfinding-algorithm-takes-a-lot-of-time/4865257#4865257), Манхэттен - это правильно. – Suma

+0

Что такое использование расстояния и что использует? Используются ли они с использованием евклидова? –

+0

@Vee: «расстояние» - это любая функция, которая принимает два узла и вычисляет * точное расстояние между ними, «оценка» - это функция, которая принимает один узел и оценивает расстояние от этого узла до целевого узла. Как эти методы выполняют свою работу, не имеет никакого отношения к реализации A *, если они выполняют свою работу правильно и эффективно. Это может быть евклидово расстояние, если это имеет смысл. Помните, что A * можно использовать для поиска путей в * произвольных * графах, он не обязательно должен быть картой реального мира. Евклидова метрика имеет смысл только в системе координат. –

0

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

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

1

Вы можете сравнить его с (или просто использовать) А * в реализации quickgraph library:

QuickGraph.Algorithms.ShortestPath.AStarShortestPathAlgorithm<TVertex,TEdge> 
+0

Я не нашел никакой документации о том, как ее использовать. TVertex должен быть моим PGNode, не так ли? Как насчет TEdge? –

+0

@Vee: Вы должны преобразовать вашу карту в виде графика (в основном каждая плитка (узел) соединено с доступными соседями через в ребро, например, 'нового край (источник, цель)') – digEmAll

5

пару нот:

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 также будет немного быстрее, чем перечисление списка по индексу.

0

Вы вычислить стоимость узла обхода, как это:

cost = current.G + 10; 

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

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

+0

current.GetNeighborNodes возвращает 4 смежен уже Существующие узлы. Массив узлов 80x80 создается с помощью карты. –

+0

ОК. Как насчет эвристики? Если расстояние между узлами равно 1, а стоимость - 10, эвристика должна быть равна * 10, а не расстоянию. С недооценкой эвристики ваш поиск будет близок к Djisktra. – Suma

-1

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

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

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

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

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

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

Смежные вопросы