-задачи патфайндинга

advertisement
ОЛИМПИАДА ШКОЛЬНИКОВ «ШАГ В БУДУЩЕЕ»
НАУЧНО-ОБРАЗОВАТЕЛЬНОЕ СОРЕВНОВАНИЕ «ШАГ В
БУДУЩЕЕ, МОСКВА»
регистрационный номер
_____________________________________________________________
название факультета
название кафедры
__________________________________________________________
__________________________________________________________
__________________________________________________________
______
название работы
Автор:
_____________________________________
фамилия, имя, отчество
___________________________________
наименование учебного заведения, класс
Научный руководитель:
_____________________________________
фамилия, имя, отчество
___________________________________
место работы
___________________________________
звание, должность
Москва - 2012
0
Содержание
1. Введение ……………………………………………………………………..2
1.1 Постановка проблемы ……………………………………………....2
1.2 Цели и задачи ………………………………………………………..2
1.3 Актуальность ………………………………………………………..2
2. Основные алгоритмы поиска пути …………………………………………3
2.1 Алгоритм Дейкстры …………………………………………………4
2.2 Волновой алгоритм …………………………………………………18
2.3 Алгоритм A* ………………………………………………………..27
2.4 Навигационная сетка ……………………………………………….44
2.5 Эвристические алгоритмы…………………………………………45
3. Сравнительный анализ алгоритмов поиска пути ……………………….46
4. Практическое применение алгоритмов поиска пути ……………………56
5. Заключение ……………………………………………………………........ 57
Список используемой литературы ………………………………………….. 58
1
Введение
Постановка проблемы
Поиск пути (англ. Pathfinding) — определение компьютерной
программой наилучшего, оптимального маршрута между двумя точками.
Поиск пути является одной из наиболее часто встречающихся задач
искусственного интеллекта, и такой спрос вызвал достаточно мощное
развитие алгоритмов, направленных на решение этой задачи
Цели и задачи
В предстоящей работе предполагается:
1. изучение и описание (использую языки программирования
Python 2.7 и Turbo Pascal 7.1) основные алгоритмы поиска пути,
применяемые на данный момент;
2. модификация описанных алгоритмов;
3. сравнительный анализ алгоритмов поиска пути, определение
сильных и слабых сторон каждого алгоритма.
Актуальность
Алгоритмы поиска пути активно применяются как на модели реальной
местности (например, GPS) и на менее реальной местности многочисленных
видеоиграх, так, наконец, и на любом графе (ярчайший пример – протокол
маршрутизации OSPF).
При этом, существует множество различных подходов к решению
задачи поиска пути, таким образом сравнительный анализ алгоритмов
поиска пути является необходимым условием оптимизации их применения
на практике.
2
Основные алгоритмы поиска пути.
На данный момент существует множество алгоритмов поиска пути,
сохраняющих актуальность и практическую применяемость.
По своей сути, каждый алгоритм поиска пути ищет на графе (в любой
его интерпретации), начиная с одной (стартовой) точки и исследуя смежные
узлы до тех пор, пока не будет достигнута точка назначения (конечный узел)
или множество таковых.
Многообразие таких алгоритмов обусловлено тем, что входные данные
и требования к результатам их работы в значительной мере зависят от
области применения таких алгоритмов.
Наиболее эффективными и популярными из известных алгоритмов
поиска пути являются:
Алгоритм поиска A*
Алгоритм Дейкстры
Волновой алгоритм
Навигационная сетка (Navmesh)
Эвристические алгоритмы
3
Алгоритм Дейкстры
Алгори́тм Де́йкстры (Dijkstra’s algorithm) — алгоритм поиска пути на
графах, изобретённый нидерландским ученым Э. Дейкстрой в 1959 году.
Находит кратчайшее расстояние от одной из вершин графа до всех остальных
или до заданной конечной. Алгоритм работает только для графов без рёбер
отрицательного веса. Алгоритм широко применяется в программировании и
технологиях, например, его использует протокол OSPF для устранения
кольцевых маршрутов.
Принцип работы
Каждой вершине сопоставим метку — минимальное известное
расстояние от этой вершины до a. Алгоритм работает пошагово — на каждом
шаге он «посещает» одну вершину и пытается уменьшать метки. Работа
алгоритма завершается, когда все вершины посещены.
Инициализация. Метка самой вершины a полагается равной 0, метки
остальных вершин — бесконечности. Это отражает то, что расстояния от a до
других вершин пока неизвестны. Все вершины графа помечаются как
непосещённые.
Шаг алгоритма. Если доступные вершины посещены, алгоритм
завершается. В противном случае, выбирается одна из непосещённых вершин
Для каждого соседа данной вершины, кроме отмеченных как посещённые,
рассмотрим новую длину пути, равную сумме значений пути от начала до
вершины (метки вершины) и длины ребра, соединяющего её с этим соседом.
Если полученное значение длины меньше значения метки соседа, заменим
значение метки полученным значением длины. Рассмотрев всех соседей,
пометим вершину как посещенную и повторим шаг алгоритма для другой
вершины.
Эффективность алгоритма
В области нахождения пути от конкретной вершины графа до всех его
вершин алгоритм Дейкстры является лучшим на данный момент.
Данный алгоритм также отлично справляется с поиском пути только
между 2 вершинами на графе, в котором невозможно составить
эвристическую функцию, находящую примерное расстояние между двумя
его вершинами (текущей и конечной).
В противном случае, алгоритм Дейкстры по всем параметрам уступает
своей модификации – алгоритму A*(A-star, см. подраздел «Алгоритм A*»).
4
Причиной различий служит то, что алгоритм Дейкстры будет проверять узлы
графа равномерно в порядке удаления от начального, а A* отдаёт
предпочтения тем узлам, которые по результатам эвристических расчётов
ближе к конечному узлу, а значит, с большей вероятностью, будут
принадлежать конечному пути. Таким образом, за счёт использования не
затратной эвристической функции, A* будет проверять не больше, а на
практике – значительно меньше узлов графа, чем алгоритм Дейкстры, а
значит, будет работать быстрее.
Практика
Ниже представлен алгоритм Дейкстры для взвешенного графа,
реализованный на языке Python 2.7
Для начала, следует определиться со способом хранения взвешенного
графа. Актуальная информация для хранения – узлы, связи, вес связей. В
Python предпочтительным мне кажется вариант хранения этой информации
при помощи «Словарей». Словарь, по сути, является аналогом массива, в
котором вместо индекса выступает ключ.
Таким образом, граф будет представлен словарём словарей, где:
- сама структура отображает граф в целом;
- ключи внешнего словаря являются узлами графа;
- значения по этим ключам являются множеством связей графа;
- ключи в этом множестве являются конечными точками связей,
выходящих из данного узла;
- значения множества связей по данным ключам являются весом таких
связей.
Изобразим полученный граф схематически:
5
Создадим для примера простой граф по этой схеме:
>>> graph={
'A': {'B': 2, 'D': 4},
'C': {'B': 4, 'F': 2},
'B': {'A': 2, 'C': 4, 'D': 3},
'E': {'D': 9, 'F': 3},
'D': {'A': 4, 'B': 3, 'E': 9},
'F': {'C': 2, 'E': 3}}
Таким образом, узлы графа вызываются встроенным в словарь методом
keys():
>>> graph.keys()
['A', 'C', 'B', 'E', 'D', 'F']
Или простым пробегом цикла for:
>>> for i in graph:
print i
A
C
B
E
6
D
F
Рёбра, исходящие из заданного узла можно вывести, просто применив к
внешнему словарю в качестве ключа данный узел:
>>> print graph['D']
{'A': 4, 'B': 3, 'E': 9}
Вес данных связей можно получить, вызвав множество рёбер узла, из
которого исходит связь по ключу – конечному узлу связи:
>>> print graph['D']['B']
3
Также, данный способ делает возможным описывать несколько более
сложные структуры, например орграф (граф, рёбра которого имеют помимо
веса направление и актуальны только в этом направлении) .
Создадим для дальнейших применений алгоритма Дейкстры схожий с
предыдущим графф, у которого связь C-F будет актуальна только от C к F:
>>> graph2={
'A': {'B': 2, 'D': 4},
'C': {'B': 4, 'F': 2},
'B': {'A': 2, 'C': 4, 'D': 3},
'E': {'D': 9, 'F': 3},
'D': {'A': 4, 'B': 3, 'E': 9},
'F': {'E': 3}}
7
Схема данного орграфа:
Теперь перейдём к самому алгоритму Дейкстры. Данный алгоритм
будет описан как для нахождения кратчайшего пути между 2 точками, так и
для нахождения такого пути от заданной точки ко всем точкам графа. Ниже
следует описание алгоритма Дейкстры для поиска пути между 2 точками
графа:
def Dijkstra(graph,start,end):
class NodeRecord:
node=None
connection=None
costSoFar=None
startRecord =NodeRecord()
startRecord.node = start
startRecord.connection = None
startRecord.costSoFar = 0
olist= [startRecord]
clist = []
while len(olist) > 0:
8
tlist=[i.costSoFar for i in olist]
current=olist[tlist.index(min(tlist))]
if current.node == end:
break
connections = graph[current.node]
for connection in connections:
endNode = connection
endNodeCost = current.costSoFar +
graph[current.node][connection]
if endNode in [clist[ite].node for ite
in range(len(clist))]:
endNodeRecord =
clist[[clist[ite].node for ite in
range(len(clist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
clist.pop(clist.index(endNodeRecord))
elif endNode in [olist[ite].node for ite
in range(len(olist))]:
endNodeRecord =
olist[[olist[ite].node for ite in
range(len(olist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
else:
endNodeRecord = NodeRecord()
endNodeRecord.node = endNode
endNodeRecord.costSoFar = endNodeCost
endNodeRecord.connection = current
if not endNodeRecord in olist :
olist.append(endNodeRecord)
clist.append(current)
olist.pop(olist.index(current))
else:
return -1
path = [end]
while current.node!=start:
current=current.connection
path.insert(0,current.node)
return path
9
Основное отличие данного алгоритма от классического описания
алгоритма Дейкстры заключается в том, что вместо создания статического
массива необработанных вершин графа используются динамические списки
тех вершин, которые, не будучи обработанными, имеют связи с уже
обработанными вершинами. Такой приём позволит не тратить ресурсы ПК на
хранение информации о тех узлах, которые обрабатывать не потребуется., в
случае с поиском расстояния между двумя точками.
Рассмотрим его в деталях:
def Dijkstra(graph,start,end):
Сам алгоритм будет оформлен в качестве функции для удобства
дальнейшего использования.
class NodeRecord:
node=None
connection=None
costSoFar=None
Создадим структуру данных, хранящую техническую информацию об
узле графа в процессе обработки его алгоритмом Дейкстры. Она включает в
себя идентификатор узла (его рабочее название), связь, через которую
проходит кратчайший путь к этому узлу, а также стоимость такого пути и
суммарный вес всех рёбер в его составе.
startRecord =NodeRecord()
startRecord.node = start
startRecord.connection = None
startRecord.costSoFar = 0
Инициализируем начальный узел.
olist= [startRecord]
clist = []
Далее алгоритм будет работать с 2 списками (массивами) узлов
(NodeRecord):
- Открытый список (olist) – множество доступных (полученных через
связи от проверенных), но непроверенных узлов.
- Закрытый список (clist) – множество проверенных узлов.
10
Алгоритм будет обрабатывать узлы открытого списка до тех пор, пока
они остались, т.е. пока в графе есть непроверенные доступные узлы.
while len(olist) > 0:
tlist=[i.costSoFar for i in olist]
current=olist[tlist.index(min(tlist))]
На каждой итерации обрабатывается узел из открытого списка. Для
равномерности выбирается узел наименее удалённый от начала.
if current.node == end:
break
Поскольку мы рассматриваем узлы в порядке отдаления от начала,
первый же случай нахождения конца пути позволит сразу сгенерировать
верный путь до него, поэтому при нахождении конца пути стоит выйти из
цикла обработки графа.
connections = graph[current.node]
for connection in connections:
endNode = connection
endNodeCost = current.costSoFar +
graph[current.node][connection]
Далее алгоритм проходит по связям, исходящим из данного узла.
if endNode in [clist[ite].node for ite
in range(len(clist))]:
endNodeRecord =
clist[[clist[ite].node for ite in
range(len(clist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
clist.pop(clist.index(endNodeRecord))
Если связь ведёт к узлу закрытого списка, то проверяется, не является ли
маршрут к этому узлу через выбранную связь короче, чем уже полученный
для него. Если это так, то запись об этом узле будет обновлена, иначе цикл
переходит к следующей итерации.
elif endNode in [olist[ite].node for ite
in range(len(olist))]:
11
endNodeRecord =
olist[[olist[ite].node for ite in
range(len(olist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
Аналогично с открытым списком, но обновление информации об узле
происходит проще.
else:
endNodeRecord = NodeRecord()
endNodeRecord.node = endNode
endNodeRecord.costSoFar = endNodeCost
endNodeRecord.connection = current
if not endNodeRecord in olist :
olist.append(endNodeRecord)
Если узел, к которому ведёт данная связь отсутствует в обоих списках,
то он добавляется в открытый список по данным от этой связи.
clist.append(current)
olist.pop(olist.index(current))
Завершающая часть обработки узла – он переходит из открытого списка
закрытый.
else:
return -1
Оператор else для цикла while срабатывает, если цикл был завершён изза нарушения условия, таким образом если мы в процессе обработки
обнаружили конец пути, цикл завершится через break и в действия
описанные после else не выполняются. В противном же случае мы не нашли
конец пути, значит следует вернуть значение -1 вместо пути.
path = [end]
while current.node!=start:
current=current.connection
path.insert(0,current.node)
return path
Наконец, если путь был найден, происходит генерация пути по
закрытому списку. Берётся узел конца пути, перед ним в путь вставляется
узел, из которого к нему ведёт оптимальный путь от начала согласно
алгоритму Дейкстры, аналогичное повторяется до тех пор, пока не будет
12
достигнут узел начала пути. Полученная последовательность – искомый
путь.
Примеры использования данного алгоритма на обычном взвешенном
графе, который мы рассмотрели ранее:
>>> graph={
'A': {'B': 2, 'D': 4},
'C': {'B': 4, 'F': 2},
'B': {'A': 2, 'C': 4, 'D': 3},
'E': {'D': 9, 'F': 3},
'D': {'A': 4, 'B': 3, 'E': 9},
'F': {'C': 2, 'E': 3}}
>>> print Dijkstra(graph,'A','E')
['A', 'B', 'C', 'F', 'E']
>>> print Dijkstra(graph,'D','C')
['D', 'B', 'C']
>>> print Dijkstra(graph,'E','A')
['E', 'F', 'C', 'B', 'A']
13
>>> graph2={
'A': {'B': 2, 'D': 4},
'C': {'B': 4, 'F': 2},
'B': {'A': 2, 'C': 4, 'D': 3},
'E': {'D': 9, 'F': 3},
'D': {'A': 4, 'B': 3, 'E': 9},
'F': {'E': 3}}
print Dijkstra(graph2,'A','E')
['A', 'B', 'C', 'F', 'E']
>>> print Dijkstra(graph2,'E','A')
['E', 'D', 'A']
Теперь изменим описание алгоритма Дейкстры для поиска пути из
заданной точки до всех точек графа:
def Dijkstra2(graph,start):
class NodeRecord:
node=None
connection=None
costSoFar=None
startRecord =NodeRecord()
startRecord.node = start
startRecord.connection = None
startRecord.costSoFar = 0
olist= [startRecord]
14
clist = []
while len(olist) > 0:
current=olist[0]
connections = graph[current.node]
for connection in connections:
endNode = connection
endNodeCost = current.costSoFar +
graph[current.node][connection]
if endNode in [clist[ite].node for ite
in range(len(clist))]:
endNodeRecord =
clist[[clist[ite].node for ite in
range(len(clist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
clist.pop(clist.index(endNodeRecord))
elif endNode in [olist[ite].node for ite
in range(len(olist))]:
endNodeRecord =
olist[[olist[ite].node for ite in
range(len(olist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
else:
endNodeRecord = NodeRecord()
endNodeRecord.node = endNode
endNodeRecord.costSoFar = endNodeCost
endNodeRecord.connection = current
if not endNodeRecord in olist :
olist.append(endNodeRecord)
clist.append(current)
olist.pop(olist.index(current))
paths=[]
for end in clist:
if end.node==start:continue
current=end
path = [end.node]
while current.node!=start:
current=current.connection
path.insert(0,current.node)
path.insert(0,start+':'+end.node)
paths.append(path)
15
return paths
Вместо пошагового объяснения описания алгоритма будет разумнее
просто перечислить отличия от исходного:
1)
Алгоритм теперь не требует узла конца пути, поскольку в
роли конца пути будет выступать каждая вершина графа кроме
начальной.
2)
Теперь обработка графа всегда идёт до тех пор, пока есть
необработанные доступные элементы. Алгоритм не предусматривает
выход при достижении какого-либо узла.
3)
Теперь генерация пути идёт по очереди для всех элементов
графа кроме начальной точки поиска пути.
Примеры использования данного алгоритма:
>>> graph={
'A': {'B': 2, 'D': 4},
'C': {'B': 4, 'F': 2},
'B': {'A': 2, 'C': 4, 'D': 3},
'E': {'D': 9, 'F': 3},
'D': {'A': 4, 'B': 3, 'E': 9},
'F': {'C': 2, 'E': 3}}
>>> print Dijkstra2(graph,'A')
16
[['A:B', 'A', 'B'], ['A:D', 'A', 'D'], ['A:C', 'A',
'B', 'C'], ['A:F', 'A', 'B', 'C', 'F'], ['A:E', 'A',
'B', 'C', 'F', 'E']]
print Dijkstra2(graph,'D')
[['D:B', 'D', 'B'], ['D:A', 'D', 'A'], ['D:C', 'D',
'B', 'C'], ['D:E', 'D', 'E'], ['D:F', 'D', 'B', 'C',
'F']]
В данном случае, в неориентированном графе (graph), кратчайший путь
от узла E до A проходит через связь F-C, а поскольку во втором случае
(graph2) она актуальна только из вершины C в F, алгоритм выдаёт более
длинный путь через связь E-D.
17
Волновой алгоритм
Волновой алгоритм, как и предыдущие, ищет путь между двумя
заданными точками. Сначала, в стороны от исходной точки распространяется
волна.
Начальное значение волны - ноль.
На первой итерации, начальное значение волны – 0. Граничащие с
волной точки(в начале – одна начальная точка), получают значение волны +
некоторый модификатор проходимости этой точки. Чем он больше - тем
медленнее преодоление данного участка. Значение волны увеличивается на 1.
Далее на каждой итерации значение волны увеличивается на 1 и схожим
образом обрабатываются все узлы графа, в которые можно перейти из уже
обработанных, и которые ещё не затронуты волной. Цикл останавливается,
когда будет обработан узел конца пути.
Наконец, по результатам обработки выводится кратчайший путь.
Восстановить его можно следующим образом: среди связей конечной
вершины найдем любую вершину с волновой меткой на 1 ниже метки
конечной вершины, среди вершин, соседствующих с последней - веpшину с
меткой на 2 ниже начальной, и т.д., пока не достигнем начальной вершины.
Найденная последовательность вершин определяет один из кратчайших
путей между двумя вершинами. Некоторые модификации волнового
алгоритма предполагают сохранение информации о том, из какой вершины
волна перешла в данную, что ускоряет генерацию пути, но занимает больше
памяти.
Практика
Алгоритм реализован на языке Паскаль для двумерного массива,
представляющего область поиска. Ниже следует общий вид алгоритма:
Program wave;
Uses Crt;
Const
Map : array [1..10, 1..10] of Byte =
(
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
18
(1,
(1,
(1,
(1,
(1,
(1,
(1,
(1,
(1,
0,
0,
1,
0,
0,
0,
1,
1,
1,
0,
0,
0,
0,
1,
0,
0,
0,
1,
0,
1,
0,
0,
1,
1,
1,
0,
1,
0,
1,
0,
1,
1,
0,
0,
0,
1,
1,
1,
1,
1,
0,
0,
0,
0,
1,
0,
0,
0,
1,
0,
1,
1,
0,
1,
0,
0,
0,
0,
0,
0,
1,
0,
1,
1,
1,
1,
1,
0,
0,
0,
0,
1,
1),
1),
1),
1),
1),
1),
1),
1),
1)
);
var
XS, YS, XE, YE : Byte;
X, Y, I : Byte;
MapM : array [1..10, 1..10] of Byte;
Moves : Byte;
MovesX : array [1..100] of Byte;
MovesY : array [1..100] of Byte;
Procedure Next(Var X, Y : Byte);
Begin
If (X <10) and (MapM[X, Y] - MapM[X + 1, Y] =
1) then
Begin
X := X + 1;
Exit;
End;
If (X >1) and (MapM[X, Y] - MapM[X - 1, Y] = 1)
then
Begin
X := X - 1;
Exit;
End;
If (Y <10) and (MapM[X, Y] - MapM[X, Y + 1] =
1) then
Begin
Y := Y + 1;
Exit;
End;
If (Y >1) and (MapM[X, Y] - MapM[X, Y - 1] = 1)
then
Begin
Y := Y - 1;
Exit;
End;
End;
Begin
19
ClrScr;
For Y := 1 to 10 do
Begin
For X := 1 to 10 do Write(Map[X, Y], '
');
WriteLn;
End;
WriteLn('Vvedite X i Y nachala');
ReadLn(XS, YS);
WriteLn('Vvedite X i Y konsa puti: ');
ReadLn(XE, YE);
If (Map[XS, YS] = 1) or (Map[XE, YE] = 1) then
Begin
WriteLn('Koordinati vvedeni neverno');
ReadLn;
Halt;
End;
MapM[XS, YS] := 1;
I := 1;
Repeat
I := I + 1;
For Y := 1 to 10 do
For X := 1 to 10 do
If MapM[X, Y] = I - 1 then
Begin
If (Y <10) and (MapM[X, Y + 1] =
0)
and (Map[X, Y+1] = 0) Then MapM[X, Y+1]
If (Y >1)
and (MapM[X, Y-1] = 0) and (Map[X, Y-1]
MapM[X, Y-1] := I;
If (X <10)
and (MapM[X+1, Y] = 0) and (Map[X+1, Y]
MapM[X+1, Y] := I;
If (X >1)
and (MapM[X-1, Y] = 0) and (Map[X-1, Y]
MapM[X-1, Y] := I;
End;
If I = 100 then
Begin
WriteLn('Nevozmozhno
kontsa puti');
ReadLn;
Halt;
End;
:= I;
= 0) Then
= 0) Then
= 0) Then
dostich
20
Until MapM[XE, YE] >0;
Moves := I - 1;
X := XE;
Y := YE;
I := Moves;
Map[XE, YE] := 4;
Repeat
MovesX[I] := X;
MovesY[I] := Y;
Next(X, Y);
Map[X, Y] := 3;
I := I - 1;
Until (X = XS) and (Y = YS);
Map[XS, YS] := 2;
For I := 1 to Moves do
begin
WriteLn('(', MovesX[I],';', MovesY[I],')');
Map[MovesX[I],MovesY[I]]:=2
end;
WriteLn('Vsego: ', Moves, 'shagov');
WriteLn;
WriteLn('Grafik:');
For Y := 1 to 10 do
Begin
For X := 1 to 10 do Write(Map[X, Y], '
');
WriteLn;
End;
WriteLn('2 - elementi puti.');
ReadLn;
End.
Теперь рассмотрим его поэтапно:
Program wave;
Uses Crt;
Const
Map : array [1..10, 1..10]
(
(1, 1, 1, 1, 1,
(1, 0, 0, 0, 0,
(1, 0, 0, 1, 1,
(1, 1, 0, 0, 0,
of Byte =
1,
1,
1,
1,
1,
0,
0,
0,
1,
0,
0,
0,
1,
1,
1,
1,
1),
1),
1),
1),
21
(1,
(1,
(1,
(1,
(1,
(1,
0,
0,
0,
1,
1,
1,
0,
1,
0,
0,
0,
1,
0,
1,
1,
1,
0,
1,
1,
1,
0,
0,
0,
1,
1,
0,
0,
0,
0,
1,
1,
0,
1,
1,
0,
1,
0,
0,
0,
1,
0,
1,
1,
0,
0,
0,
0,
1,
1),
1),
1),
1),
1),
1)
);
Таким образом будет выглядеть область поиска. Нули – проходимые
элементы, единицы – непроходимые, реализация разных коэффициентов
проходимости не вносит принципиальные различия в алгоритм, поэтому она
не будет представлена на данном примере.
var
XS, YS, XE, YE : Byte;
X, Y, I : Byte;
MapM : array [1..10, 1..10] of Byte;
Moves : Byte;
MovesX : array [1..100] of Byte;
MovesY : array [1..100] of Byte;
Инициируем необходимые переменные. Весьма непривычный процесс
после изящества Python 2.7
Procedure Next(Var X, Y : Byte);
Begin
If (X <10) and (MapM[X, Y] - MapM[X + 1, Y] =
1) then
Begin
X := X + 1;
Exit;
End;
If (X >1) and (MapM[X, Y] - MapM[X - 1, Y] = 1)
then
Begin
X := X - 1;
Exit;
End;
If (Y <10) and (MapM[X, Y] - MapM[X, Y + 1] =
1) then
Begin
Y := Y + 1;
Exit;
End;
22
If (Y >1) and (MapM[X, Y] - MapM[X, Y - 1] = 1)
then
Begin
Y := Y - 1;
Exit;
End;
End;
Создаём функцию, которая будет регламентировать распространение
«волны».
Begin
ClrScr;
For Y := 1 to 10 do
Begin
For X := 1 to 10 do Write(Map[X, Y], '
');
WriteLn;
End;
Изобразим перед пользователем исходное состояние лабиринта.
WriteLn('Vvedite X i Y nachala');
ReadLn(XS, YS);
WriteLn('Vvedite X i Y konsa puti: ');
ReadLn(XE, YE);
If (Map[XS, YS] = 1) or (Map[XE, YE] = 1) then
Begin
WriteLn('Koordinati vvedeni neverno');
ReadLn;
Halt;
End;
Попросим пользователя ввести координаты начала и конца пути,
проверим их допустимость.
MapM[XS, YS] := 1;
I := 1;
Repeat
I := I + 1;
For Y := 1 to 10 do
For X := 1 to 10 do
If MapM[X, Y] = I - 1 then
Begin
23
If (Y <10) and (MapM[X, Y + 1] =
0)
and (Map[X, Y+1] = 0) Then MapM[X, Y+1]
If (Y >1)
and (MapM[X, Y-1] = 0) and (Map[X, Y-1]
MapM[X, Y-1] := I;
If (X <10)
and (MapM[X+1, Y] = 0) and (Map[X+1, Y]
MapM[X+1, Y] := I;
If (X >1)
and (MapM[X-1, Y] = 0) and (Map[X-1, Y]
MapM[X-1, Y] := I;
End;
:= I;
= 0) Then
= 0) Then
= 0) Then
Распространяем «волну» по области поиска согласно концепции
алгоритма. На каждом шаге цикла увеличиваем значение волны на 1.
If I = 100 then
Begin
WriteLn('Nevozmozhno dostich
kontsa puti');
ReadLn;
Halt;
End;
Поскольку лабиринт имеет размеры 10*10, достижение волной значения
100 до нахождения точки конца пути можно считать признаком неудачи
поиска. В этом случае выводим пользователю информацию о том, что путь
найден не был.
Until MapM[XE, YE]
Moves := I - 1;
X := XE;
Y := YE;
I := Moves;
Map[XE, YE] := 4;
Repeat
MovesX[I] :=
MovesY[I] :=
Next(X, Y);
Map[X, Y] :=
I := I - 1;
Until (X = XS) and
Map[XS, YS] := 2;
>0;
X;
Y;
3;
(Y = YS);
24
В противном случае алгоритм достигнет точки конца пути, а значит,
путь существует. Генерируем искомый путь по обработанным точкам.
For I := 1 to Moves do
begin
WriteLn('(', MovesX[I],';', MovesY[I],')');
Map[MovesX[I],MovesY[I]]:=2
end;
WriteLn('Vsego: ', Moves, 'shagov');
WriteLn;
Выводим пользователю полученную информацию в виде
последовательности координат точек в составе искомого пути и общего
числа шагов в его составе. Отмечаем точки пути на области поиска.
WriteLn('Grafik:');
For Y := 1 to 10 do
Begin
For X := 1 to 10 do Write(Map[X, Y], '
');
WriteLn;
End;
WriteLn('2 - elementi puti.');
ReadLn;
End.
Для наглядности выводим результат графически – в виде цифр «2» от
точки начала пути до его окончания. Конец алгоритма.
Использование алгоритма:
25
26
Алгоритм A*
История создания
В 1964 году Нильс Нильсон изобрел эвристический подход к
увеличению скорости алгоритма Дейкстры. Этот алгоритм был назван А1. В
1967 году Бертрам Рафаэль сделал значительные улучшения по этому
алгоритму, но ему не удалось достичь оптимальности. Он назвал этот
алгоритм A2. Тогда в 1968 году Петр Э. Харт представил аргументы, которые
доказывали, что A2 был оптимальным при использовании последовательной
эвристики лишь с незначительными изменениями. В его доказательство
алгоритма также включен раздел, который показывал, что новый алгоритм
A2 был, возможно, лучшим алгоритмом, учитывая условия.
Принцип работы
Практически, алгоритм A* отличается от алгоритма Дейкстры
направленностью обхода узлов графа за счёт использования эвристической
функции, определяющей ориентировочное расстояние между данным узлом
и концом пути. Иными словами, приоритет отдаётся тем узлам, которые
согласно эвристической функции находятся ближе к концу пути.
A* пошагово просматривает все пути, ведущие от начальной вершины в
конечную, пока не найдёт минимальный. Сначала рассматриваются те
маршруты, которые «кажутся» ведущими к цели. В начале работы
просматриваются узлы, смежные с начальным; выбирается тот из них,
который имеет минимальное значение эвристической функции, после чего
этот узел раскрывается.
В случае с графом, алгоритм продолжает свою работу до тех пор, пока
значение f(x) целевой вершины не окажется меньшим, чем любое значение в
очереди (либо пока всё дерево не будет просмотрено). Из множественных
решений выбирается решение с наименьшей стоимостью.
В случае с двумерным массивом, A* действует подобно направленному
волновому алгоритму, поэтому при достижении им конечной точки,
формирование кратчайшего пути уже становится возможным и совершается
незамедлительно.
27
Эффективность.
Алгоритм A* на данный момент является оптимальным способом
поиска пути между двумя точками в тех случаях, когда существует
сравнительно простой эвристический метод оценки расстояния между
элементами области поиска. Если такого метода не существует, A*
идентичен либо алгоритму Дейкстры в вариации для двух точек, либо
волновому алгоритму в зависимости от вида области поиска.
Также алгоритм A* не оптимален, если область поиска статична и поиск
пути на ней осуществляется множество раз, поскольку в таком случае все
пути можно заранее рассчитать при помощи алгоритма Дейкстры для всех
точек.
Практика
На данном примере алгоритм A* будет реализован для двумерного
массива на Python 2.7.
Теперь о тонкостях реализации: то, что отличает A* от алгоритма
Дейкстры и Волнового алгоритма – эвристическая функция оценки
расстояния от текущего узла до конечного в данном случае легко выводится
из координат этих точек в области поиска, по сути – индексов этих точек в
двумерном массиве, представляющем эту область. Поскольку функция
эвристическая и точных значений от неё не требуется, можно использовать
даже не формулу расстояния между точками в системе координат а просто
модуль разности этих координат. Результат в подавляющем большинстве
случаев будет одинаковый.
Теперь что касается области поиска. Двумерный массив как и при
реализации волнового алгоритма состоит из полностью проходимых и
полностью непроходимых элементов. В данном случае проходимые
элементы будут представлены символом « » (пробел), а непроходимые – «#».
Элемент начала будет представлен буквой «А», конца – «B», а положение
исполнителя –«*» . Для удобства создания лабиринтов и создадим функцию,
позволяющую вводить их поэлементно с текстовым интерфейсом ввода:
def stepbystep():
length=input('Ширина лабиринта(без учёта
границ):')
higth=input('Высота лабиринта(без учёта
границ):')
lab=[]
numrow=['_','@','@']
28
abc=['A','B','C','D','E','F','G','H','I','J','K','L','M
','N','O','P','R','Q','S','T','U','V','W','X','Y','Z']
for i in range(1,length+1):
numrow.insert(2,abc[length-i])
labhigh=0
border=[]
start=0
orda=length
absa=higth
finorda=0
finabsa=0
fin=0
while len(border)<2+length :
border+='#'
lab+=[border]
while labhigh<higth :
row=['#']
rowleng=0
while rowleng<length :
print numrow
for i in xrange(0,len(lab)):
print i, ': ', lab[i]
print len(lab), ': ', row, '\n 1 препятствие \n 2 - начало(обязательно одно) \n 3 –
конец лабиринта(один) \n другой символ - пустота \n
просьба не делать колонн'
elem = input('Следуюий эллемент:')
if elem==1:
row+='#'
rowleng+=1
elif elem ==2 and start==0:
row+='A'
absa=startabs=len(row)-1
orda=startord=len(lab)
rowleng+=1
start=1
elif elem == 3 and fin==0:
row+='B'
fin=1
finabsa=(len(row)-1)
finorda=(len(lab))
rowleng+=1
else:
row+=' '
29
rowleng+=1
row+='#'
lab+=[row]
labhigh+=1
lab+=[border]
print "Конечный вариант\n",numrow
for j in xrange(0,len(lab)):
print j, ': ',[lab[j][i] for i in xrange
(0,len(lab[j])) ]
print '\n \n'
return lab,[orda,absa],[finorda,finabsa]
Данная функция непосредственно к процессу поиска пути отношения не
имеет, кроме чего весьма примитивна, посему комментарии к ней на мой
взгляд излишни.
Теперь было бы рационально заранее описать функцию, которая будет
генерировать список связей заданного в качестве аргумента элемента области
поиска.
def getConnections(massiv,y,x):
connections=[]
class cct:
getCost=None
getToNode=None
getFromNode=None
if massiv[y][x-1]!='#' and massiv[y1][x]!='#'and massiv[y-1][x-1]!='#':
connection=cct()
connection.getToNode=[y-1,x-1]
connection.getFromNode=[y,x]
connection.getCost=14
connections.append(connection)
if massiv[y][x-1]!='#' and
massiv[y+1][x]!='#'and massiv[y+1][x-1]!='#':
connection=cct()
connection.getToNode=[y+1,x-1]
connection.getFromNode=[y,x]
connection.getCost=14
connections.append(connection)
if massiv[y][x+1]!='#' and massiv[y1][x]!='#'and massiv[y-1][x+1]!='#':
connection=cct()
connection.getToNode=[y-1,x+1]
connection.getFromNode=[y,x]
30
connection.getCost=14
connections.append(connection)
if massiv[y][x+1]!='#' and
massiv[y+1][x]!='#'and massiv[y+1][x+1]!='#':
connection=cct()
connection.getToNode=[y+1,x+1]
connection.getFromNode=[y,x]
connection.getCost=14
connections.append(connection)
if massiv[y][x+1]!='#':
connection=cct()
connection.getToNode=[y,x+1]
connection.getFromNode=[y,x]
connection.getCost=10
connections.append(connection)
if massiv[y][x-1]!='#':
connection=cct()
connection.getToNode=[y,x-1]
connection.getFromNode=[y,x]
connection.getCost=10
connections.append(connection)
if massiv[y-1][x]!='#':
connection=cct()
connection.getToNode=[y-1,x]
connection.getFromNode=[y,x]
cjnnection.getCost=10
connections.append(connection)
if massiv[y+1][x]!='#':
connection=cct()
connection.getToNode=[y+1,x]
connection.getFromNode=[y,x]
connection.getCost=10
connections.append(connection)
return connections
Данная функция возвращает список экземпляров класса cct, т.е.
информации о связи. Она будет применяться при обработке узла в процессе
основного алгоритма. Помимо движений по вертикали или горизонтали,
данная функция будет поддерживать движение в диагональных
направлениях, при этом не позволяет такое движение, если в его процессе
исполнитель будет проходить через непроходимую область.
Наконец, создадим функцию, отвечающую за взаимодействие с
пользователем:
31
def printAstar():
graph,start,end=stepbystep()
rslt= AStar(graph, start, end)
if rslt==-1:
return 'Непроходимо!'
graph[rslt[0][0]][rslt[0][1]]='*'
print '\n\nНачало прохождения'
for j in xrange(0,len(graph)):
print [graph[j][k] for k in xrange
(0,len(graph[j])) ]
for i in range(len(rslt)-1):
graph[rslt[i+1][0]][rslt[i+1][1]]='*'
graph[rslt[i][0]][rslt[i][1]]=' '
print "Следующий шаг #",i+1
for j in xrange(0,len(graph)):
print [graph[j][k] for k in xrange
(0,len(graph[j])) ]
print '\n \n'
return 'Прохождение заверщено!'
Данная функция позволяет пользователю создать область поиска и
выбрать точки начала и конца пути, после чего запускает поиск пути и
выводит его результаты в наглядном пошаговом виде пользователю.
Теперь создадим непосредственно саму функцию поиска пути в
заданной среде. Поскольку A* является модификацией алгоритма Дейкстры,
описанного немного ранее, его описание будет также получено путём
модификации созданного ранее алгоритма Дейкстры.
Полученный алгоритм будет выглядеть следующим образом:
def AStar(graph, start, end):
class NodeRecord:
node=None
connection=None
costSoFar=None
estimatedTotalCost=None
startRecord =NodeRecord()
startRecord.node = start
startRecord.connection = None
startRecord.costSoFar = 0
startRecord.estimatedTotalCost =((sum(end)sum(start))**2)**0.5
32
olist= [startRecord]
clist = []
while len(olist) > 0:
current=olist[-1]
for nodenum in range(len(olist)):
if
olist[nodenum].estimatedTotalCost<current.estimatedTota
lCost:
current = olist[nodenum]
if current.node == end:
break
connections =
getConnections(graph,current.node[0],current.node[1])
for connection in connections:
endNode = connection.getToNode
endNodeCost = current.costSoFar +
connection.getCost
if endNode in [clist[ite].node for ite
in range(len(clist))]:
endNodeRecord =
clist[[clist[ite].node for ite in
range(len(clist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
clist.pop(clist.index(endNodeRecord))
endNodeHeuristic =
endNodeRecord.estimatedTotalCost endNodeRecord.costSoFar
elif endNode in [olist[ite].node for ite
in range(len(olist))]:
endNodeRecord =
olist[[olist[ite].node for ite in
range(len(olist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
endNodeHeuristic =
endNodeRecord.estimatedTotalCost endNodeRecord.costSoFar
else:
endNodeRecord = NodeRecord()
endNodeRecord.node = endNode
33
endNodeHeuristic = ((sum(end)sum(endNode))**2)**0.5
endNodeRecord.costSoFar = endNodeCost
endNodeRecord.connection =
connection.getFromNode
endNodeRecord.estimatedTotalCost =
endNodeCost + endNodeHeuristic
if not endNodeRecord in olist :
olist.append(endNodeRecord)
clist.append(current)
olist.pop(olist.index(current))
if current.node != end:
return -1
else:
path = [end]
while current.node!=start:
for i in clist:
if current.connection==i.node:
current=i
path.insert(0,current.node)
break
return path
Рассмотрим его поэтапно, акцентируя внимание на отличиях от
алгоритма Дейкстры:
def AStar(graph, start, end):
class NodeRecord:
node=None
connection=None
costSoFar=None
estimatedTotalCost=None
В описание структуры данных, хранящей информацию об обработанном
элементе области поиска, мы добавляем переменную estimatedTotalCost,
хранящую ориентировочное расстояние от данного элемента до конечной
точки пути, вычисленное эвристически. Кроме того, вследствие перехода с
графа на двумерный массив, переменная node будет хранить не название
данного элемента (никаких особых названий они не имеют), а его
координаты в области поиска. Координаты эти инвертированы для удобства
подстановки в массив, обозначающий область поиска.
startRecord =NodeRecord()
startRecord.node = start
startRecord.connection = None
34
startRecord.costSoFar = 0
startRecord.estimatedTotalCost =((sum(end)sum(start))**2)**0.5
Как и в алгоритме Дейкстры, вначале обрабатывается отдельно узел
начала пути, но в данном алгоритме мы также вычисляем для него значение
ориентировочного расстояния до концапути.
olist= [startRecord]
clist = []
В алгоритме A* также используются два списка узлов:
1)
Открытый список (olist) – множество необработанных
узлов, к которым можно перейти из уже обработанных.
2)
Закрытый список (clist) – множество уже обработанных
узлов.
Различия состоят лишь в том, что элементы этих списков будут
несколько другими структурами данных, о чём было подробно рассказано
при описании этих структур.
while len(olist) > 0:
Алгоритм также будет работать до тех пор, пока остались доступные
необработанные узлы, но теперь данный цикл также сворачивается при
достижении узла конца пути, что допустимо в силу специфики области
поиска.
current=olist[-1]
for nodenum in range(len(olist)):
if
olist[nodenum].estimatedTotalCost<current.estimatedTota
lCost:
current = olist[nodenum]
На данном этапе выбирается узел из открытого списка, который
согласно эвристике будет ближе всего к концу пути.
if current.node == end:
break
Как и было ранее заявлено, фаза обработки узлов будет закончена, как
только будет достигнут узел конца пути. Дальнейшая обработка области
поиска не имеет смысла, поскольку A* рассчитан исключительно на поиск
пути между двумя точками.
35
connections =
getConnections(graph,current.node[0],current.node[1])
for connection in connections:
endNode = connection.getToNode
endNodeCost = current.costSoFar +
connection.getCost
if endNode in [clist[ite].node for ite
in range(len(clist))]:
endNodeRecord =
clist[[clist[ite].node for ite in
range(len(clist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
clist.pop(clist.index(endNodeRecord))
endNodeHeuristic =
endNodeRecord.estimatedTotalCost endNodeRecord.costSoFar
elif endNode in [olist[ite].node for ite
in range(len(olist))]:
endNodeRecord =
olist[[olist[ite].node for ite in
range(len(olist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
endNodeHeuristic =
endNodeRecord.estimatedTotalCost endNodeRecord.costSoFar
else:
endNodeRecord = NodeRecord()
endNodeRecord.node = endNode
endNodeHeuristic = ((sum(end)sum(endNode))**2)**0.5
endNodeRecord.costSoFar = endNodeCost
endNodeRecord.connection =
connection.getFromNode
endNodeRecord.estimatedTotalCost =
endNodeCost + endNodeHeuristic
if not endNodeRecord in olist :
olist.append(endNodeRecord)
clist.append(current)
olist.pop(olist.index(current))
36
Далее, А* как и алгоритм Дейкстры проходит по связям выбранного
узла и актуализирует информацию об узлах. Единственное отличие –
операции с эвристическими расчётами. Стоит заметить, что хотя в данном
случае это и малозаметно, но в случае с более сложными эвристическими
функциями выгоднее при возможности вместо перевычисления таких
функций просто откатывать их значения до предыдущих.
if current.node != end:
return -1
else:
path = [end]
while current.node!=start:
for i in clist:
if current.connection==i.node:
current=i
path.insert(0,current.node)
break
return path
После окончания фазы обработки графа, путь всё так же генерируется по
закрытому списку. Различия в коде обусловлены лишь изменением в способе
хранения информации о связи. Также в данный алгоритм встроен счётчик
суммарной длины пути.
Взаимодействие пользователя с программой при этом выглядит
примерно следующим образом:
Введите новый лабиринт
Ширина лабиринта(без учёта границ):3
Высота лабиринта(без учёта границ):3
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой не по-диагонали)
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:2
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой не по-диагонали)
другой символ - пустота
37
просьба не делать колонн
Следуюий эллемент:4
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:4
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ', ' ', '#']
2 : ['#']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:1
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ', ' ', '#']
2 : ['#', '#']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:1
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ', ' ', '#']
2 : ['#', '#', '#']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:4
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ', ' ', '#']
2 : ['#', '#', '#', ' ', '#']
не по-диагонали)
не по-диагонали)
не по-диагонали)
не по-диагонали)
38
3 : ['#']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой не по-диагонали)
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:3
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ', ' ', '#']
2 : ['#', '#', '#', ' ', '#']
3 : ['#', 'B']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой не по-диагонали)
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:4
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ', ' ', '#']
2 : ['#', '#', '#', ' ', '#']
3 : ['#', 'B', ' ']
1 - препятствие
2 - начало(обязательно одно)
3 - (желательно рядом со стенкой не по-диагонали)
другой символ - пустота
просьба не делать колонн
Следуюий эллемент:4
Конечный вариант
['_', '@', 'A', 'B', 'C', '@']
0 : ['#', '#', '#', '#', '#']
1 : ['#', 'A', ' ', ' ', '#']
2 : ['#', '#', '#', ' ', '#']
3 : ['#', 'B', ' ', ' ', '#']
4 : ['#', '#', '#', '#', '#']
Начало прохождения
['#', '#', '#', '#', '#']
['#', '*', ' ', ' ', '#']
['#', '#', '#', ' ', '#']
39
['#', 'B', ' ',
['#', '#', '#',
Следующий шаг #
['#', '#', '#',
['#', ' ', '*',
['#', '#', '#',
['#', 'B', ' ',
['#', '#', '#',
' ',
'#',
1
'#',
' ',
' ',
' ',
'#',
'#']
'#']
Следующий шаг #
['#', '#', '#',
['#', ' ', ' ',
['#', '#', '#',
['#', 'B', ' ',
['#', '#', '#',
2
'#',
'*',
' ',
' ',
'#',
'#']
'#']
'#']
'#']
'#']
Следующий шаг #
['#', '#', '#',
['#', ' ', ' ',
['#', '#', '#',
['#', 'B', ' ',
['#', '#', '#',
3
'#',
' ',
'*',
' ',
'#',
'#']
'#']
'#']
'#']
'#']
Следующий шаг #
['#', '#', '#',
['#', ' ', ' ',
['#', '#', '#',
['#', 'B', ' ',
['#', '#', '#',
4
'#',
' ',
' ',
'*',
'#',
'#']
'#']
'#']
'#']
'#']
Следующий шаг #
['#', '#', '#',
['#', ' ', ' ',
['#', '#', '#',
['#', 'B', '*',
['#', '#', '#',
5
'#',
' ',
' ',
' ',
'#',
'#']
'#']
'#']
'#']
'#']
'#']
'#']
'#']
'#']
'#']
40
Следующий шаг #
['#', '#', '#',
['#', ' ', ' ',
['#', '#', '#',
['#', '*', ' ',
['#', '#', '#',
6
'#',
' ',
' ',
' ',
'#',
'#']
'#']
'#']
'#']
'#']
Прохождение заверщено!
Дальнейшая оптимизация
Алгоритм A* может быть дополнительно оптимизирован для более
эффективного поиска пути в конкретных условиях. Отдельный пласт
изменений касается эвристики, используемой в данном алгоритме. Чем
обширнее область поиска по сравнению с длиной конечного пути, тем
больше лишних узлов позволит не обрабатывать использование
эвристической функции, но и само эвристическое вычисление расстояния
также требует мощностей процессора.
В самом простом случае, когда мы имеем дело с однородной в плане
проходимости местностью, эвристические функции оценки расстояния на
ней сводятся либо к вычислению его по формуле длины отрезка с
известными координатами крайних точек, либо к простому суммированию
расстояния между точками по горизонтали и по вертикали. Первый способ
несколько точнее, но значительно дольше второго (возведение в степень
против сложения), отчего в большинстве случаев приоритетнее именно
второй способ. К слову, эти функции легко изменить при переходе в nмерное пространство, например при n=5, формула примет вид
AB=((Ax-Bx)^2+(Ay-By)^2+(Az-Bz)^2+(Ap-Bp)^2+(AqBq)^2)^0.5
Где x,y,z,p,q – оси в координатном пространстве, Ax, Ay и т.д. – координата
точки А на соответствующей оси.
Если местность начинает иметь значительно более неоднородную структуру,
т.е. вес связи между двумя вершинами графа его представляющими будет не
пропорционален геометрическому расстоянию между ними, то
представленные функции всё ещё актуальны, но уже сильно теряют в
41
точности. Ещё меньше точность у этих функций становится, если граф теряет
привязку к местности. Разумеется, в теории всякий граф можно представить
в виде геометрической фигуры и взять координаты, но построение большого
графа, который к тому же строится в не меньше, чем, скажем, пятимерном
пространстве – задача даже более сложная, чем нахождение пути по этому
графу, поэтому простая эвристика уже неактуальна.
Существует несколько способов замены эвристики для таких случаев. Вопервых, можно ввести простую систему приоритетов, ведь с точки зрения
теории вероятности, шанс нахождения на кратчайшем пути больше у тех
узлов, которые имеют больше исходящих связей, а узел, не имеющий связей
кроме той, по которой к нему перешла обработка, если этот узел не является
узлом конца пути, является тупиком и может вовсе не обрабатываться.
Также можно воспользоваться принципом базы данных. Допустим, у нас есть
статический граф или любая область поиска. На ней несколько раз
запускается поиск пути для разных точек. Каждый раз при этом сохраняются
данные о точках начала и конца пути, а также их суммарной длине. Для
ускорения процесса получения таких данных, можно проводить также после
каждого использования поиска пути полный анализ закрытого списка для тех
точек, для которых путь ещё не изучен. При таком применении алгоритма,
мы можем использовать следующую эвристическую функцию:
 Если путь между текущей точкой и точкой конца пути уже вычислялся,
то мы можем смело считать длину найденного пути точным
расстоянием между этими точками (при том, что граф не динамичен
или не менялся с момента того расчёта; менее точная версия – если
изменения были, но не затронули узлы вычисленного пути и их связи).
 Иначе если точки, для которых вызвана эвристика, числятся среди
связей одного из вычисленных путей, причём вес этих связей меньше,
чем, скажем, треть этого пути, то эвристическим расстоянием можно
считать сумму длины такого пути и веса ребра/рёбер, которые ведут из
его края/краёв в необходимые узлы.
 Иначе неизвестно.
Проблема здесь в том, что пока не получена достаточно полная база
результатов, использование такой эвристики будет лишь замедлять поиск
пути, а даже при сборе достаточного числа данных, применение эвристики
будет куда дороже, чем в случае с привязкой графа к местности. С другой
стороны, в некоторых условиях возможно сразу во входных данных получить
некоторые расстояния, что облегчит сбор данных. Замечу также, что
поскольку эвристические функции становятся сложнее, возрастает
актуальность расписанного ранее отката их значений (см. подзаголовок
«Практика» раздела «Алгоритм А*») вместо перевычисления результатов.
42
Кроме того, иногда бывает возможно постоянно получать данные, которые
должны генерироваться эвристикой, как входные данные. Например, если
исполнитель может самостоятельно измерять такие данные, что существенно
облегчит задачу.
Другой аспект оптимизации – оптимизация хранения информации, и тут
основные изменения имеют место быть в том случае, если длина пути имеет
ощутимые размеры в плане числа узлов. Во-первых, повышается
актуальность работы алгоритма через два динамических списка, что было
мною уже применено в описании данного алгоритма и алгоритма Дейкстры
(см. подзаголовок «Практика» разделов «Алгоритм А*» и «Алгоритм
Дейкстры»), в то время как каноническая версия предполагает изначальное
введение массива всех необработанных узлов. Использование двух
динамических списков позволяет не хранить в памяти огромное число
структур данных, описывающих необработанный узел, а добавлять их по
мере надобности. Это также позволяет алгоритму работать в том случае, если
не весь граф изначально известен.
Также серьёзным поводом для оптимизации служит то, что если узел уже не
будет требовать переобработки, то для него имеет смысл хранить только его
название и тот узел, из которого в него перешли. Тем не менее, в
канонической версии для таких узлов хранится также стоимость
перемещения в них из начальной точки и эвристическое значение суммарной
длины пути. Вывод прост – начиная с определённой длины пути (допустим,
20 узлов) имеет смысл удалять вышеуказанную информацию для тех узлов,
которые, имеют связи только с узлами из списка уже обработанных узлов
(задача несколько усложняется для орграфов).
43
Навигационная сетка
Навигационная сетка, или navmesh – приём, применяемый при поиске
пути по заранее полностью известной местности. Ключевым отличием от
предыдущих методов является отсутствие чёткой дискретизации этого
пространства поиска, тогда как в вышеизложенных методах пространство
заменяют узлы графов или ячейки массивов. Практически это позволяет
строить более реалистичные траектории движения, что наиболее
востребовано при моделировании движения живых организмов, поскольку
зная всю проходимую область можно выстраивать кривые траектории
движения без риска получения неактуального пути.
Также пространство, представленное навигационной сеткой, может
содержать дополнительную информацию, влияющую на поведение
исполнителя, что особенно полезно при конструировании AI(Artificial
Intelligence), чьё назначение не ограничивается поиском пути. В частности,
AI управляющий персонажами видеоигр зачастую требует для оптимизации
своей деятельности данные о тактических характеристиках пространства, а
AI, автоматизирующий работу бытового пылесоса зачастую использует
навигационную сетку для структурирования информации о запылённости
помещения.
Тем не менее, navmesh является лишь модификацией для поиска пути в
определённых практических ситуациях, и изначальный поиск пути между
двумя точками в условиях навигационной сетки осуществляется по одному
из вышеописанных алгоритмов, как правило, A*.
44
Эвристические алгоритмы поиска пути
Существует множество алгоритмов, описывающих поиск пути проще,
чем уже рассмотренные в данной работе алгоритмы, но выдаваемый ими
результат, как правило, неточен в большей или меньшей степени вплоть до
риска вовсе не обнаружить путь при его наличии.
Ввиду большого количества таких алгоритмов, в рамках данной работы
рассмотрен будет лишь один из основных алгоритмов этой группы –
алгоритм поворота Креша (Crash)
Данный алгоритм оценивает путь не весь сразу, а только на один шаг,
что немного расширяет его практическое применение. Неформальное
описание данного алгоритма будет выглядеть следующим образом:
1. Постройте прямую линию до конечной точки. Надо всегда запомнить
координаты секции в которой Путник был ( на один шаг ).
2. Если Путник встретился с препятствием он пробует правило правой
руки. Перемещайте его вправо, до тех пор пока не встретится
свободный проход. И затем двигайте его на эту секцию.
3. Повторяйте пункты 1-2 до достижения конечного пункта движения.
Или если Путник оказался в предыдущей секции.
4. Если Путник попал в предыдущую секции, надо сменить правило
правой руки на правило левой руки и повторить всю процедуру
снова.
Также возможна несколько усложненная версия алгоритма, когда при
обнаружении препятствия исполнитель двигается в одну сторону вдоль края
препятствия, пока движение к концу пути по прямой не станет эквивалентно
движению от препятствия.
Очевидным минусом алгоритма поворота Креша является то, что его
применение на лабиринтоподобной местности практически никогда не
позволяет найти какой-либо путь.
45
Сравнительный анализ алгоритмов поиска пути
Как уже было замечено в начале работы, многообразие алгоритмов
поиска пути обусловлено многообразием их применений, для каждого из
которых эффективнее работает определённый алгоритм поиска пути.
Алгоритм Дейкстры приоритетен в случаях поиска пути до всех
точек области поиска, а также в случае отсутствия сколь либо
эффективной эвристической функции оценки расстояния между
элементами области поиска.
Волновой алгоритм эффективен, если область поиска имеет
неравномерную проходимость, что затрудняет эвристические
вычисления для A*.
Алгоритм A* эффективен при одиночном поиске пути между
двумя точками, если возможно эффективно эвристически получать
примерную дистанцию между элементами области поиска.
Навигационная сетка с использованием алгоритма A*
эффективна при создании AI, в чьи задачи входит не только поиск
пути. Также этот метод эффективен при необходимости построить
реалистичную сглаженную траекторию движения между двумя
точками. Данный метод предполагает наличие детальной информации
об области поиска или возможности получения такой информации.
Эвристические алгоритмы поиска пути применимы и
оптимальны, если необходим максимально простой алгоритм, при этом
область поиска достаточно проста, а применения алгоритма допускают
неточность полученного пути.
В данном разделе будет приведено практическое подтверждение
некоторых из данных утверждений на основании скорости выполнения
представленных в данной работе алгоритмов, а именно:
Алгоритм Дейкстры против алгоритма A* при поиске пути между
двумя точками по двумерному массиву.
Алгоритм Дейкстры против алгоритма A* при поиске пути от
данной точки для всех точек двумерного массива.
Волновой алгоритм для двумерного массива практически эквивалентен
алгоритму Дейкстры для поиска пути между 2 точками, посему результаты,
результаты, верные для алгоритма Дейкстры можно считать верными для
Волнового алгоритма, а природа эвристических алгоритмов сравнительно
проста для понимания.
46
Итак, для подобного анализа сначала необходимо адаптировать
алгоритм Дейкстры для двумерного массива. Для определения связей
обрабатываемых точек массива можно использовать созданную для A*
функцию getConnections.(см. подраздел «Алгоритм A*» - практика) Весь
объём изменений обеих алгоритмов Дейкстры касается изменения операций
над областью поиска, поэтому отдельно комментировать каждую часть
алгоритма ещё раз попросту нецелесообразно.
Сам алгоритм при этом выглядит следующим образом:
def Dijkstramass(graph,start,end):
class NodeRecord:
node=None
connection=None
costSoFar=None
startRecord =NodeRecord()
startRecord.node = start
startRecord.connection = None
startRecord.costSoFar = 0
olist= [startRecord]
clist = []
while len(olist) > 0:
tlist=[i.costSoFar for i in olist]
current=olist[tlist.index(min(tlist))]
if current.node == end:
break
connections =
getConnections(graph,current.node[0],current.node[1])
for connection in connections:
# прикидываем текущую стоимость
endNode = connection.getToNode
endNodeCost = current.costSoFar +
connection.getCost
if endNode in [clist[ite].node for ite
in range(len(clist))]:
endNodeRecord =
clist[[clist[ite].node for ite in
range(len(clist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
clist.pop(clist.index(endNodeRecord))
47
elif endNode in [olist[ite].node for ite
in range(len(olist))]:
endNodeRecord =
olist[[olist[ite].node for ite in
range(len(olist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
else:
endNodeRecord = NodeRecord()
endNodeRecord.node = endNode
endNodeRecord.costSoFar = endNodeCost
endNodeRecord.connection = connection
if not endNodeRecord in olist :
olist.append(endNodeRecord)
clist.append(current)
olist.pop(olist.index(current))
else:
return -1
path = [end]
while current.node!=start:
for i in clist:
if
current.connection.getFromNode==i.node:
current=i
path.insert(0,current.node)
break
return path
Также потребуется аналогичным образом адаптировать алгоритм
Дейкстры для всех точек под двумерный массив:
def Dijkstra2mass(graph,start):
class NodeRecord:
node=None
connection=None
costSoFar=None
startRecord =NodeRecord()
startRecord.node = start
startRecord.connection = None
startRecord.costSoFar = 0
olist= [startRecord]
clist = []
while len(olist) > 0:
48
tlist=[i.costSoFar for i in olist]
current=olist[tlist.index(min(tlist))]
connections =
getConnections(graph,current.node[0],current.node[1])
for connection in connections:
endNode = connection.getToNode
endNodeCost = current.costSoFar +
connection.getCost
if endNode in [clist[ite].node for ite
in range(len(clist))]:
endNodeRecord =
clist[[clist[ite].node for ite in
range(len(clist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
clist.pop(clist.index(endNodeRecord))
elif endNode in [olist[ite].node for ite
in range(len(olist))]:
endNodeRecord =
olist[[olist[ite].node for ite in
range(len(olist))].index(endNode)]
if endNodeRecord.costSoFar <=
endNodeCost:
continue
else:
endNodeRecord = NodeRecord()
endNodeRecord.node = endNode
endNodeRecord.costSoFar = endNodeCost
endNodeRecord.connection = connection
if not endNodeRecord in olist :
olist.append(endNodeRecord)
clist.append(current)
olist.pop(olist.index(current))
paths=[]
for end in clist:
if end.node==start:continue
current=end
path = [end.node]
while current.node!=start:
for i in clist:
if
current.connection.getFromNode==i.node:
current=i
49
path.insert(0,current.node)
break
path.insert(0,str(start)+':'+str(end.node))
paths.append(path)
return paths
Итак, для начала сравним алгоритм Дейкстры для 2 точек двумерного
массива (первый из представленных в этом разделе алгоритмов) и алгоритм
A* (представлен в подразделе «Алгоритм A*» - Практика).
Код программы-измерителя будет состоять из двух данных алгоритмов,
функции getConnections, выдающей список связей для данной точки в
области поиска, а также функции, отвечающей непосредственно за
измерение:
def pftimer (function):
graph=[['#', '#', '#',
'#', '#', '#', '#'],
['#', ' ', ' ',
' ', ' ', '#', '#'],
['#', '#', '#',
'#', '#', ' ', '#'],
['#', ' ', ' ',
' ', ' ', ' ', '#'],
['#', ' ', '#',
'#', '#', ' ', '#'],
['#', ' ', '#',
'#', '#', ' ', '#'],
['#', '#', ' ',
' ', '#', ' ', '#'],
['#', ' ', ' ',
'#', '#', ' ', '#'],
['#', ' ', '#',
'#', ' ', ' ', '#'],
['#', ' ', ' ',
' ', ' ', ' ', '#'],
['#', 'B', ' ',
'#', '#', ' ', '#'],
['#', '#', '#',
'#', '#', '#', '#']]
start=[6,6]
end=[10,1]
import time
starttime=time.time()
for i in range(1000):
'#', '#', '#', '#', '#',
' ', '#', '#', ' ', ' ',
' ', ' ', ' ', '#', ' ',
' ', '#', ' ', '#', ' ',
'#', '#', ' ', ' ', ' ',
'#', ' ', ' ', ' ', ' ',
' ', '#', '#', 'A', '#',
'#', ' ', ' ', ' ', '#',
'#', '#', ' ', ' ', ' ',
'#', ' ', ' ', ' ', ' ',
' ', ' ', '#', '#', '#',
'#', '#', '#', '#', '#',
50
tmp=function(graph,start,end)
print 'Большой лабиринт: ',(time.time()starttime)/1000.0
graph=[['#', '#', '#', '#', '#'],
['#', 'A', ' ', ' ', '#'],
['#', ' ', '#', ' ', '#'],
['#', ' ', ' ', 'B', '#'],
['#', '#', '#', '#', '#']]
start=[1,1]
end=[3,3]
starttime=time.time()
for i in range(1000):
tmp=function(graph,start,end)
print 'Малый лабиринт : ',(time.time()starttime)/1000.0
graph=[['#', '#', '#', '#', '#'],
['#', 'A', ' ', ' ', '#'],
['#', '#', '#', ' ', '#'],
['#', 'B', ' ', ' ', '#'],
['#', '#', '#', '#', '#']]
start=[1,1]
end=[3,1]
starttime=time.time()
for i in range(1000):
tmp=function(graph,start,end)
print 'Прямой лабиринт : ',(time.time()starttime)/1000.0
Измерения проводятся соответственно на 3 типах лабиринтов:
Большой лабиринт, где путь до цели составляет незначительную
долю от всех проходимых точек лабиринта.
Малый лабиринт, где путь до цели составляет 50% от всех
проходимых точек лабиринта.
Прямая дорога, где путь до цели занимает всю проходимую часть
лабиринта.
Для каждого случая функция вызывается 1000 раз и для вычисления
времени, затраченного на 1 выполнение, вычисленный промежуток делится
на 1000. Функция-счётчик выполняется последовательно для алгоритма
Дейкстры и A*. Такие меры необходимы для того, чтобы влияние прочих
процессов на вычисленную скорость работы функции было минимальным.
51
Все вычисления будут проводиться на ПК, укомплектованном
процессором Intel Core i5-2410M (4-ядерный, частота ядра - 2.30ГГц) при
минимальной и относительно неизменной загрузке его прочими задачами(04% в спокойном состоянии по показаниям с Диспетчера задач в течение
минуты).
Итак, результаты:
Алгоритм Дейкстры
>>> pftimer(Dijkstramass)
Большой лабиринт: 0.00363999986649
Малый лабиринт : 0.000248000144958
Прямой лабиринт : 0.000137000083923
>>> pftimer(Dijkstramass)
Большой лабиринт: 0.00358099985123
Малый лабиринт : 0.00022000002861
Прямой лабиринт : 0.000140000104904
>>> pftimer(Dijkstramass)
Большой лабиринт: 0.00360800004005
Малый лабиринт : 0.000248999834061
Прямой лабиринт : 0.000141000032425
Алгоритм A*
>>> pftimer(AStar)
Большой лабиринт:
Малый лабиринт :
Прямой лабиринт :
>>> pftimer(AStar)
Большой лабиринт:
Малый лабиринт :
Прямой лабиринт :
>>> pftimer(AStar)
Большой лабиринт:
Малый лабиринт :
Прямой лабиринт :
0.000667000055313
0.000141000032425
0.000157999992371
0.000709999799728
0.000140000104904
0.000150000095367
0.000717000007629
0.000156000137329
0.000155999898911
Результаты вычислений подтверждают теоретические данные:
1)
Для большого лабиринта, A* справляется с задачей в 3 раза
быстрее алгоритма Дейкстры, поскольку алгоритм Дейкстры при своём
выполнении будет равномерно обрабатывать всю область поиска в
порядке удаления от начала. Согласно показаниям интерпретатора
Python, алгоритм Дейкстры обследовал 45 узлов в большом лабиринте.
52
Алгоритм A* позволяет уменьшить число обрабатываемых узлов за
счёт эвристики, т.е. данный алгоритм обрабатывает узлы в порядке
убывания вероятности их появления в искомом пути, и согласно тем же
данным, алгоритм A* в большом лабиринте обрабатывает 8 точек +
конец пути, что равно длине конечного пути.
2)
Для малого лабиринта A* работает примерно в 1.6 раз
быстрее алгоритма Дейкстры, поскольку он обрабатывает в 2раза
меньше узлов, но при каждой обработке узла делает дополнительные
эвристические вычисления, а также выполняет все прочие этапы
алгоритма в тех же условиях, что и алгоритм Дейкстры.
3)
Для прямой дороги алгоритм Дейкстры работает несколько
быстрее (порядка 1.1 раз) алгоритма A*, поскольку оба алгоритма
будут обрабатывать все проходимые точки лабиринта (все они лежат
на конечном пути), но алгоритм A* дополнительно производит
эвристические расчёты расстояния до конца пути.
Аналогичным образом, несколько изменив функцию-счётчик, можно
произвести схожие измерения для вычисления расстояния от заданной точки
до всех точек в области поиска.
A* в данном случае используется через вспомогательную функцию:
def Astar2all(graph,start):
paths=[]
for i in graph:
for j in graph[i]:
if graph[i][j]!='#' and [i,j]!=start:
path=AStar(graph,start,[i,j])
if path != -1:
path.insert(0,
(str(start)+':'+str([i:j])))
paths.append(path)
return paths
Функция-счётчик принимает следующий вид:
def pftimer2 (function):
graph=[['#', '#', '#', '#', '#',
'#', '#', '#', '#'],
['#', ' ', ' ', ' ', '#',
' ', ' ', '#', '#'],
['#', '#', '#', ' ', ' ',
'#', '#', ' ', '#'],
['#', ' ', ' ', ' ', '#',
' ', ' ', ' ', '#'],
'#', '#', '#',
'#', ' ', ' ',
' ', '#', ' ',
' ', '#', ' ',
53
['#', ' ', '#', '#', '#', ' ', ' ',
'#', '#', ' ', '#'],
['#', ' ', '#', '#', ' ', ' ', ' ',
'#', '#', ' ', '#'],
['#', '#', ' ', ' ', '#', '#', 'A',
' ', '#', ' ', '#'],
['#', ' ', ' ', '#', ' ', ' ', ' ',
'#', '#', ' ', '#'],
['#', ' ', '#', '#', '#', ' ', ' ',
'#', ' ', ' ', '#'],
['#', ' ', ' ', '#', ' ', ' ', ' ',
' ', ' ', ' ', '#'],
['#', ' ', ' ', ' ', ' ', '#', '#',
'#', '#', ' ', '#'],
['#', '#', '#', '#', '#', '#', '#',
'#', '#', '#', '#']]
start=[6,6]
import time
starttime=time.time()
for i in range(100):
tmp=function(graph,start)
print 'Большой лабиринт: ',(time.time()starttime)/100.0
graph=[['#', '#', '#', '#', '#'],
['#', 'A', ' ', ' ', '#'],
['#', ' ', '#', ' ', '#'],
['#', ' ', ' ', ' ', '#'],
['#', '#', '#', '#', '#']]
start=[1,1]
starttime=time.time()
for i in range(100):
tmp=function(graph,start)
print 'Малый лабиринт : ',(time.time()starttime)/100.0
graph=[['#', '#', '#', '#', '#'],
['#', 'A', ' ', ' ', '#'],
['#', '#', '#', ' ', '#'],
['#', ' ', ' ', ' ', '#'],
['#', '#', '#', '#', '#']]
start=[1,1]
starttime=time.time()
for i in range(100):
tmp=function(graph,start)
print 'Прямой лабиринт : ',(time.time()starttime)/100.0
' ',
' ',
'#',
'#',
' ',
' ',
'#',
'#',
54
Результаты вычислений получились следующие:
>>> pftimer2(Dijkstra2mass)
Большой лабиринт: 0.00671000003815
Малый лабиринт : 0.000309998989105
Прямой лабиринт : 0.000160000324249
>>> pftimer2(Dijkstra2mass)
Большой лабиринт: 0.00640000104904
Малый лабиринт : 0.000150001049042
Прямой лабиринт : 0.000160000324249
>>> pftimer2(Dijkstra2mass)
Большой лабиринт: 0.00609999895096
Малый лабиринт : 0.000300002098083
Прямой лабиринт : 0.000199999809265
>>> pftimer2(Astar2all)
Большой лабиринт: 0.0404799985886
Малый лабиринт : 0.00125
Прямой лабиринт : 0.00047000169754
>>> pftimer2(Astar2all)
Большой лабиринт: 0.0410400009155
Малый лабиринт : 0.00125
Прямой лабиринт : 0.000460000038147
>>> pftimer2(Astar2all)
Большой лабиринт: 0.0402600002289
Малый лабиринт : 0.00125
Прямой лабиринт : 0.000469999313354
Во всех случаях, алгоритм A* работает значительно (в 2 и более раз)
медленнее алгоритма Дейкстры, при выполнении алгоритма A*, для каждой
точки заново производится обработка области поиска, в то время как
алгоритм Дейкстры производит обработку только 1 раз. Полученные
результаты полностью подтверждают теоретическую информацию: алгоритм
А*, как модификация алгоритма Дейкстры, оптимизирован для вычисления
расстояния между 2 точками, поэтому он менее полезен при вычислении
пути до всех точек области поиска.
55
Практическое применение алгоритмов поиска пути.
Алгоритмы, позволяющие найти путь между 2 точками, имеют весьма
широкий спектр применений, при этом для каждого из них, как правило,
используется своя модификация алгоритмов поиска пути.
Во-первых, у этих алгоритмов есть прямое назначение – поиск пути на
модели реальной местности (как правило, граф или навигационная сетка), что
позволяет активно использовать эти алгоритмы и их модификации в
навигационных программах, наиболее простым примером которых является
система навигации GPS(Global Positioning System – глобальная система
позиционирования).
Также, поиск пути по реальной местности актуален в роботостроении,
поскольку в функции многих роботов входит перемещение по местности, а
если эта местность не имеет форму выпуклой однородной геометрической
фигуры, то для поиска пути по ней придётся использовать более или менее
сложный алгоритм поиска пути.
Весьма популярны алгоритмы поиска пути и в сфере видеоигр, являясь
частью игрового ИИ. Наиболее актуальной для таких ИИ является система
navmesh (навигационная сетка), поскольку для таких ИИ доступна вся
информация о пространстве поиска, а навигационная сетка помимо поиска
отлично справляется с структурированием этой информации, облегчая её
использования другими элементами игрового ИИ.
Поиск пути также может происходить в условиях любой структуры,
которую можно представить, например, графом. Наиболее известным из
применений алгоритмов патфайндинга не связанных напрямую с местностью
является OSPF(Open Shortest Path First) – протокол динамической
маршрутизации по сети, использующий для нахождения кратчайшего пути
Алгоритм Дейкстры.
56
Заключение
В процессе работы были рассмотрены и воссозданы основные
алгоритмы поиска пути, был проведён сравнительный анализ этих
алгоритмов как на теоретическом уровне (анализ принципа работы), так и на
практике (вычисления скорости работы в разных условиях). К сожалению,
формальное описание и практический анализ некоторых методов поиска
пути, таких как navmesh, было затруднено особенностями их работы, однако
основная задача работы при этом была выполнена.
Более подробную информацию о результатах анализа и о самих
анализируемых алгоритмов вы можете посмотреть в соответствующих
разделах (Основные алгоритмы поиска пути; Сравнительный анализ
алгоритмов поиска пути).
57
Список использованной литературы
1. Ian Millington и John Funge – «AI For Games» оригинальная английская
версия.
2. David Merz – цикл статей «Очаровательный Python» оригинальная
английская версия.
3. Иван Орехов – цикл статей «Программирование на Python»
оригинальная русская версия.
4. Pierre-Marie Baty – статья «navmesh tutorial» - оригинальная английская
версия.
5. Материалы сайта http://ru.wikipedia.org/
6. Материалы сайта http://algolist.ru
7. Материалы сайта http://pmg.org.ru
58
Download