haskell-notes

станции возвращает список всех соседних с ней станций:

metroMap :: Station -> [Station]

metroMap x = case x of

St Black Kosmodrom

-> [St Black UlBylichova]

St Black UlBylichova

->

[St Black Kosmodrom, St Black Zvezda, St Red UlBylichova]

St Black

Zvezda

->

[St Black UlBylichova, St Blue

Zvezda, St Green Zvezda]

Приведён пример заполнения только для одной линии. Остальные линии заполняются аналогично. Об-

ратите внимание на то, что некоторые станции имеют одинаковые имена, но находятся на разных линиях.

Всё готово для того чтобы написать функцию поиска маршрута. Для этого мы воспользуемся алгоритмом

A*.

19.1 Алгоритм эвристического поиска А*

Наша задача относится к задачам поиска путей на графе. Путём на графе называют такую последователь-

ность узлов, в которой для любых двух соседних узлов существует ребро, которое их соединяет. В нашем

случае графом является карта метро, узлами~– станции, рёбрами~– линии между станциями, а путями~–

маршруты.

Представим, что мы находимся в узле A и нам необходимо попасть в узел B и единственное, что нам

известно~– это все соседние узлы с тем, в котором мы находимся. У нас есть возможность перейти в один

276 | Глава 19: Ориентируемся по карте

из соседних узлов и посмотреть нет ли среди их соседей узла B. В этом случае нам ничего не остаётся кроме

того как бродить по карте от станции к станции в случайном порядке, пока мы не натолкнёмся на узел B или

все узлы не кончатся. Такой поиск называют слепым.

Вот если бы у нас был компас, который в каждой точке указывал в сторону цели нам было бы гораздо

проще. Такой компас принято называть эвристикой. Это функция, которая принимает узел и возвращает

число. Чем меньше число, тем ближе узел к цели. Обычно эвристика указывает не точное расстояние до

цели, поскольку мы не знаем где цель, а приблизительную оценку. Мы не знаем расстояние до цели, но

догадываемся, нам кажется, что она где-то там, ещё чуть-чуть и мы найдём её. Примером эвристики для

поиска по карте может быть функция, которая вычисляет расстояние по прямой до цели. Предположим, что

мы не знаем где находится цель (какая дорога к ней ведёт), но мы знаем её координаты. Также мы знаем

координаты каждой вершины, в которой мы находимся. Тогда мы можем легко вычислить расстояние по

прямой до цели и наш поиск станет гораздо более осмысленным.

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

поиск называют поиском по первому лучшему приближению. В поиске A* учитывается не только расстояние

до цели, но и то расстояние, которое мы уже прошли. Мы выбираем не ту вершину, которая ближе к цели, а

ту для которой полный путь до цели будет минимальным. Ведь пока мы идём мы можем запоминать какое

расстояние мы уже прошли. Прибавив к этому значению, то которое мы получим с помощью эвристики мы

получим полный (предполагаемый) путь до цели.

Поиск А* гораздо лучше поиска по первому лучшему приближению. Его часто применяют в компьютерных

играх для поиска пути или принятия решений.

Принято разделять поиск на графе и поиск на дереве. Если мы идём по графу, то вершины могут по-

вторятся (они образуют циклы). В случае поиска на дереве мы считаем, что все вершины уникальны. При

поиске на графе очень важно запоминать те вершины, в которых мы уже побывали. Иначе мы будем очень

часто ходить кругами.

В Haskell очень удобно работать с данными, которые имеют иерархическую структуру. Их можно пред-

ставить в виде дерева, обычно в таких типах у нас есть конструкторы-константы и конструкторы, которые

собирают составные значения. Граф выходит за рамки этого класса данных, потому что рёбра графов могут

образовывать циклы. Но мы схитрим и представим граф поиска в виде дерева. Корнем нашего дерева будет

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

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

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

подобных ситуаций мы будем запоминать те вершины, в которых мы уже побывали и не рассматривать их,

если они встретятся нам ещё раз.

Сформулируем задачу поиска в типах. У нас есть дерево поиска, которое содержит все возможные раз-

ветвления, также каждая вершина содержит значение эвристики, по нему мы знаем насколько близка данная

вершина к цели. Также у нас есть специальный предикат, который определён на вершинах, по нему мы мо-

жем узнать является ли данная вершина целью. Нам нужно получить путь, или цепочку вершин, которая

будет начинаться в корне дерева поиска и заканчиваться в целевой вершине.

search :: Ord h => (a -> Bool) -> Tree (a, h) -> Maybe [a]

Здесь a – это значение вершины и h – значение эвристики. Обратите внимание на зависимость Ord h в

контексте, ведь мы собираемся сравнивать эти значения по близости к цели. При обходе дерева мы будем

запоминать повторяющиеся вершины, для этого мы воспользуемся типом множество из стандартного мо-

дуля Data.Set. Внутри Set могут хранится только значения, для которых определены операции сравнения,

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162