Графы
В настоящей главе изучаются основные методы поиска путей на графах — поиск в глубину и поиск в ширину. Рассматривается отношение достижимости на графе. Разбираются некоторые способы поиска кратчайших путей. Генерируется случайный лабиринт, визуализируется проход по нему.
Для представления графа в языке Пролог обычно используются факты. Например, пусть в отдельных фактах хранятся вершины и ребра графа:
node("a"). node("b"). node("c"). arc(1, "a", "b"). % номер ребра, его начало и конец arc(2, "b", "c").
Тогда предикат, возвращающий элементы матрицы инцидентности, можно определить следующим образом:
incidenceMatrix(Node, Edge, Elem):- node(Node), arc(Edge, Node1, Node2), getElem(Node, Node1, Node2, Elem). getElem(Node, Node, _, 1):- !. getElem(Node, _, Node, -1):- !. % 1 для неориент. графа getElem(_, _, _, 0). ?- incidenceMatrix(Node, Edge, Elem).
В качестве основного примера в данной главе используется система дорог, которая связывает некоторые города. Эту систему можно изобразить в виде нагруженного графа, вершины которого соответствуют городам, а ребра — соединяющим им дорогам (рис. 8.1).
Граф представляется в виде множества ребер, для хранения которых используются факты:
clauses arc("Москва", "Нижний Новгород", 400). arc("Нижний Новгород", "Пермь", 950). arc("Екатеринбург", "Пермь", 350). arc("Екатеринбург", "Новосибирск", 1550). arc("Нижний Новгород", "Екатеринбург", 1300). arc("Москва", "Самара", 1050). arc("Самара", "Екатеринбург", 950). arc("Самара", "Новосибирск", 2300). arc("Санкт-Петербург", "Петрозаводск", 450). arc("Санкт-Петербург", "Псков", 300).
В директории Exe проекта следует создать текстовый файл graph.txt и поместить в него данные факты.
8.1. Поиск в глубину
Поиск в глубину является естественным для языка Пролог, он используется машиной вывода Пролога для вычисления целей. Поэтому поиск в глубину путей на графах реализуется в языке Пролог наиболее просто.
Поясним идею поиска в глубину на следующем примере. Рассмотрим граф, изображенный на рис. 8.2 (a).
Пусть ребра графа хранятся в следующем порядке: . Совершим обход графа из вершины . Будем двигаться вперед по ребрам и помечать пройденные вершины до тех пор, пока будут встречаться непомеченные вершины. Когда это станет невозможным, вернемся в последнюю пройденную вершину , для которой существует непомеченная смежная с ней вершина. Далее обход будет совершаться из вершины . Обход продолжается до тех пор, пока остаются непомеченные вершины. Схематично обход графа можно представить следующим образом (рис. 8.2 (b)):
В следующих двух программах для нахождения путей используется поиск в глубину. В первой программе используется функциональный стиль, а во второй предикатный. Во второй программе вместе с поиском подсчитывается длина пути. Отношение edge является симметричным замыканием отношения arc. Для хранения пройденного пути используется список. В процессе вычислений вершины записываются в список, как в стек, поэтому к найденному пути применяется операция обращения списка.
class facts - graph arc: (string Город1, string Город2, unsigned Расстояние). class predicates depthFirst: (string, string) -> string* nondeterm. path: (string, string, string*) -> string* nondeterm. edge: (string, string, unsigned) nondeterm (i,o,o). clauses edge(X, Y, Dist):- arc(X, Y, Dist); arc(Y, X, Dist). depthFirst(Start, Goal) = list::reverse(path(Start, Goal, [Start])). path(Goal, Goal, Path) = Path:- !. path(V, Goal, CurrPath) = path(NextV, Goal, [NextV | CurrPath]):- edge(V, NextV, _), not(list::isMember(NextV, CurrPath)). run():- file::consult("graph.txt", graph), VertexList = depthFirst("Москва", "Новосибирск"), write(string::concatWithDelimiter(VertexList, " -> ")), nl, fail; _ = readLine().Пример 8.1. Поиск в глубину
Предикат concatWithDelimiter соединяет список строк в одну строку, вставляя между ними заданный разделитель.
Для текущей вершины можно не использовать отдельный аргумент (см. выше определение предиката path/3):
depthFirst(Start, Goal) = list::reverse(path([Start], Goal)). path([Goal | Path], Goal) = [Goal | Path]. path([V | Path], Goal) = path([NextV, V | Path], Goal):- edge(V, NextV, _), not(NextV in Path).
Упражнение 1. Найдите все пути из Москвы в Новосибирск, проходящие через Пермь1Пример приведен для версии 7.5. В версии 7.4 вместо выражения not(A in B) должно быть not(list::isMember(A, B)). Вместо A и B в листингах стоят разные переменные..
class facts - graph arc: (string, string, unsigned). class predicates depthFirst: (string, string, string* [out], unsigned [out]) nondeterm. path: (string, string, string*, string* [out], unsigned, unsigned [out]) nondeterm. edge: (string, string, unsigned) nondeterm (i,o,o). clauses edge(X, Y, Dist):- arc(X, Y, Dist); arc(Y, X, Dist). depthFirst(Start, Goal, list::reverse(Path), Dist):- path(Start, Goal, [Start], Path, 0, Dist). path(Goal, Goal, Path, Path, Dist, Dist):- !. path(V, Goal, CurrPath, Path, CurrDist, Dist):- edge(V, NextV, D), not(NextV in CurrPath), path(NextV, Goal, [NextV | CurrPath], Path, CurrDist + D, Dist). run():- file::consult("graph.txt", graph), depthFirst("Москва", "Новосибирск", Path, D), write(string::concatWithDelimiter(Path, " -> "), " : ", D), nl, fail; _ = readLine().Пример 8.2. Поиск в глубину с подсчетом длины пути
Упражнение 2.
- Найдите все пути, не превосходящие заданной длины.
- Найдите все пути от одного пункта до другого, которые содержат не более заданного числа пересадок.