ОЛИМПИАДА ШКОЛЬНИКОВ «ШАГ В БУДУЩЕЕ» НАУЧНО-ОБРАЗОВАТЕЛЬНОЕ СОРЕВНОВАНИЕ «ШАГ В БУДУЩЕЕ, МОСКВА» регистрационный номер _____________________________________________________________ название факультета название кафедры __________________________________________________________ __________________________________________________________ __________________________________________________________ ______ название работы Автор: _____________________________________ фамилия, имя, отчество ___________________________________ наименование учебного заведения, класс Научный руководитель: _____________________________________ фамилия, имя, отчество ___________________________________ место работы ___________________________________ звание, должность Москва - 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