- •Олимпиада школьников «Шаг в будущее»
- •Введение
- •Алгоритм Дейкстры
- •Принцип работы
- •Эффективность алгоритма
- •Практика
- •Волновой алгоритм
- •Практика
- •Алгоритм a* История создания
- •Принцип работы
- •Эффективность.
- •Практика
- •Дальнейшая оптимизация
- •Навигационная сетка
- •Эвристические алгоритмы поиска пути
- •Сравнительный анализ алгоритмов поиска пути
- •Практическое применение алгоритмов поиска пути.
- •Заключение
- •Список использованной литературы
Алгоритм a* История создания
В 1964 году Нильс Нильсон изобрел эвристический подход к увеличению скорости алгоритма Дейкстры. Этот алгоритм был назван А1. В 1967 году Бертрам Рафаэль сделал значительные улучшения по этому алгоритму, но ему не удалось достичь оптимальности. Он назвал этот алгоритм A2. Тогда в 1968 году Петр Э. Харт представил аргументы, которые доказывали, что A2 был оптимальным при использовании последовательной эвристики лишь с незначительными изменениями. В его доказательство алгоритма также включен раздел, который показывал, что новый алгоритм A2 был, возможно, лучшим алгоритмом, учитывая условия.
Принцип работы
Практически, алгоритм A* отличается от алгоритма Дейкстры направленностью обхода узлов графа за счёт использования эвристической функции, определяющей ориентировочное расстояние между данным узлом и концом пути. Иными словами, приоритет отдаётся тем узлам, которые согласно эвристической функции находятся ближе к концу пути.
A* пошагово просматривает все пути, ведущие от начальной вершины в конечную, пока не найдёт минимальный. Сначала рассматриваются те маршруты, которые «кажутся» ведущими к цели. В начале работы просматриваются узлы, смежные с начальным; выбирается тот из них, который имеет минимальное значение эвристической функции, после чего этот узел раскрывается.
В случае с графом, алгоритм продолжает свою работу до тех пор, пока значение f(x) целевой вершины не окажется меньшим, чем любое значение в очереди (либо пока всё дерево не будет просмотрено). Из множественных решений выбирается решение с наименьшей стоимостью.
В случае с двумерным массивом, A* действует подобно направленному волновому алгоритму, поэтому при достижении им конечной точки, формирование кратчайшего пути уже становится возможным и совершается незамедлительно.
Эффективность.
Алгоритм A* на данный момент является оптимальным способом поиска пути между двумя точками в тех случаях, когда существует сравнительно простой эвристический метод оценки расстояния между элементами области поиска. Если такого метода не существует,A* идентичен либо алгоритму Дейкстры в вариации для двух точек, либо волновому алгоритму в зависимости от вида области поиска.
Также алгоритм A* не оптимален, если область поиска статична и поиск пути на ней осуществляется множество раз, поскольку в таком случае все пути можно заранее рассчитать при помощи алгоритма Дейкстры для всех точек.
Практика
На данном примере алгоритм A* будет реализован для двумерного массива наPython2.7.
Теперь о тонкостях реализации: то, что отличает A* от алгоритма Дейкстры и Волнового алгоритма – эвристическая функция оценки расстояния от текущего узла до конечного в данном случае легко выводится из координат этих точек в области поиска, по сути – индексов этих точек в двумерном массиве, представляющем эту область. Поскольку функция эвристическая и точных значений от неё не требуется, можно использовать даже не формулу расстояния между точками в системе координат а просто модуль разности этих координат. Результат в подавляющем большинстве случаев будет одинаковый.
Теперь что касается области поиска. Двумерный массив как и при реализации волнового алгоритма состоит из полностью проходимых и полностью непроходимых элементов. В данном случае проходимые элементы будут представлены символом « » (пробел), а непроходимые – «#». Элемент начала будет представлен буквой «А», конца – «B», а положение исполнителя –«*» . Для удобства создания лабиринтов и создадим функцию, позволяющую вводить их поэлементно с текстовым интерфейсом ввода:
def stepbystep():
length=input('Ширина лабиринта(без учёта границ):')
higth=input('Высота лабиринта(без учёта границ):')
lab=[]
numrow=['_','@','@']
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+=' '
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[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[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[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[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, т.е. информации о связи. Она будет применяться при обработке узла в процессе основного алгоритма. Помимо движений по вертикали или горизонтали, данная функция будет поддерживать движение в диагональных направлениях, при этом не позволяет такое движение, если в его процессе исполнитель будет проходить через непроходимую область.
Наконец, создадим функцию, отвечающую за взаимодействие с пользователем:
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
olist= [startRecord]
clist = []
while len(olist) > 0:
current=olist[-1]
for nodenum in range(len(olist)):
if olist[nodenum].estimatedTotalCost<current.estimatedTotalCost:
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
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
startRecord.costSoFar = 0
startRecord.estimatedTotalCost =((sum(end)-sum(start))**2)**0.5
Как и в алгоритме Дейкстры, вначале обрабатывается отдельно узел начала пути, но в данном алгоритме мы также вычисляем для него значение ориентировочного расстояния до концапути.
olist= [startRecord]
clist = []
В алгоритме A* также используются два списка узлов:
Открытый список (olist) – множество необработанных узлов, к которым можно перейти из уже обработанных.
Закрытый список (clist) – множество уже обработанных узлов.
Различия состоят лишь в том, что элементы этих списков будут несколько другими структурами данных, о чём было подробно рассказано при описании этих структур.
while len(olist) > 0:
Алгоритм также будет работать до тех пор, пока остались доступные необработанные узлы, но теперь данный цикл также сворачивается при достижении узла конца пути, что допустимо в силу специфики области поиска.
current=olist[-1]
for nodenum in range(len(olist)):
if olist[nodenum].estimatedTotalCost<current.estimatedTotalCost:
current = olist[nodenum]
На данном этапе выбирается узел из открытого списка, который согласно эвристике будет ближе всего к концу пути.
if current.node == end:
break
Как и было ранее заявлено, фаза обработки узлов будет закончена, как только будет достигнут узел конца пути. Дальнейшая обработка области поиска не имеет смысла, поскольку A* рассчитан исключительно на поиск пути между двумя точками.
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))
Далее, А* как и алгоритм Дейкстры проходит по связям выбранного узла и актуализирует информацию об узлах. Единственное отличие – операции с эвристическими расчётами. Стоит заметить, что хотя в данном случае это и малозаметно, но в случае с более сложными эвристическими функциями выгоднее при возможности вместо перевычисления таких функций просто откатывать их значения до предыдущих.
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 - (желательно рядом со стенкой не по-диагонали)
другой символ - пустота
просьба не делать колонн
Следуюий эллемент: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 : ['#', '#', '#', ' ', '#']
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 : ['#', '#', '#', '#', '#']
Начало прохождения
['#', '#', '#', '#', '#']
['#', '*', ' ', ' ', '#']
['#', '#', '#', ' ', '#']
['#', 'B', ' ', ' ', '#']
['#', '#', '#', '#', '#']
Следующий шаг # 1
['#', '#', '#', '#', '#']
['#', ' ', '*', ' ', '#']
['#', '#', '#', ' ', '#']
['#', 'B', ' ', ' ', '#']
['#', '#', '#', '#', '#']
Следующий шаг # 2
['#', '#', '#', '#', '#']
['#', ' ', ' ', '*', '#']
['#', '#', '#', ' ', '#']
['#', 'B', ' ', ' ', '#']
['#', '#', '#', '#', '#']
Следующий шаг # 3
['#', '#', '#', '#', '#']
['#', ' ', ' ', ' ', '#']
['#', '#', '#', '*', '#']
['#', 'B', ' ', ' ', '#']
['#', '#', '#', '#', '#']
Следующий шаг # 4
['#', '#', '#', '#', '#']
['#', ' ', ' ', ' ', '#']
['#', '#', '#', ' ', '#']
['#', 'B', ' ', '*', '#']
['#', '#', '#', '#', '#']
Следующий шаг # 5
['#', '#', '#', '#', '#']
['#', ' ', ' ', ' ', '#']
['#', '#', '#', ' ', '#']
['#', 'B', '*', ' ', '#']
['#', '#', '#', '#', '#']
Следующий шаг # 6
['#', '#', '#', '#', '#']
['#', ' ', ' ', ' ', '#']
['#', '#', '#', ' ', '#']
['#', '*', ' ', ' ', '#']
['#', '#', '#', '#', '#']
Прохождение заверщено!