Скачиваний:
26
Добавлен:
22.05.2015
Размер:
104.96 Кб
Скачать

Лабораторная работа 7

Задачи на графах

Графы в Прологе могут представляться в виде внутренней БД и виде списков. Рассмотрим способы представления графа, изображённого на рисунке 7.1.

Этот граф можно описать в Прологе в виде фактов: ребро(Вершина1,Вершина2,Расстояние).

Тогда внутренняя БД будет выглядеть так:

clauses

ребро('a','b',60).   ребро('a','d',70).

ребро('b','c',50).   ребро('c','e',10).

ребро('c','f',60).   ребро('d','e',70).

ребро('e','g',20).   ребро('f','g',30).

ребро('f','h',20).   ребро('f','k',10).

ребро('g','h',30).

В этом описании ребро между двумя вершинами описывается одним фактом. Например, факт ребро('a','b',60) задаёт ребро между вершинами a и b длиной 60.

Либо этот же граф можно описать в виде списка рёбер:

[ребро('a','b',60), ребро('a','d',70),

 ребро('b','c',50), ребро('c','e',10),

 ребро('c','f',60), ребро('d','e',70),

 ребро('e','g',20), ребро('f','g',30),

 ребро('f','h',20), ребро('f','k',10),

 ребро('g','h',30)].

Оба эти описания эквивалентны, но обладают различными свойствами при решении задач на графах. В случае, когда граф задан фактами БД, легко осуществлять поисковые задачи на графах, не предполагающие модификацию состава его вершин или длину рёбер. Тогда поиск смежной вершины и/или инцидентного ребра осуществляет механизм поиска с откатом.

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

Существует ещё масса других комбинированных способов описания графа в Прологе. Например, в виде списка вершин и списка рёбер, которые отражают либо матрицу смежности, либо матрицу инцидентности. Однако в данной лабораторной работе они не будут рассматриваться в виду их чрезвычайно редкого использования в Прологе.

Пути на графе

Пример 1. Пусть дан граф, изображённый на рисунке 7.1. Найдём все пути между двумя вершинами, которые вводятся с клавиатуры.

В начале всего надо написать предикат, который для заданной вершины вернём смежную ей и расстояние до неё. Можно было бы подумать, что для этого можно использовать предикат ребро(Вершина1,Вершина2,Расстояние). Однако это не так. Если, к примеру, надо найти вершину, смежную вершине a, то достаточно использовать такой вызов: ребро('a',Вершина,Расстояние). И тогда мы получим сначала вершину b, а при откате – вершину d. Однако при поиске вершин, смежных вершине b, надо использовать уже разные вызовы, так как в базе фактов содержаться рёбра, в которых вершина b выступает как в роли первого аргумента, так и в роли второго аргумента:

ребро('a','b',60). ребро('b','c',50).

Эту трудность можно преодолеть двумя путями. Первый путь заключается в удвоении числа фактов в БД, то есть нам надо описать каждое ребро двумя фактами, в которых вершины поменяны местами. Например, ребро между вершинами a и b длиной 60. надо задать так:

ребро('a','b',60). ребро('b','a',60).

Однако это не самый лучший выход, так как размер БД увеличивается в два раза. Есть лучший путь – описать предикат, который сам ищет смежные вершины. Говоря языком математики, он осуществляет транзитивное замыкание. Назовём этот предикат связь(Вершина1,Вершина2,Расстояние). Он, для заданного первого аргумента Вершина1 находит второй аргумент Вершина2, т.е. смежную вершину, а также расстояние между ними Расстояние:

связь(Вершина1,Вершина2,Расстояние):-      ребро(Вершина1,Вершина2,Расстояние);

      ребро(Вершина2,Вершина1,Расстояние).

Теперь мы уже можем описать предикат поиска пути на графе между двумя вершинами путь/6. Пусть начальная и конечная вершина носят имена Старт и Стоп соответственно. Аргументами предиката при его вызове являются:

путь(Старт, % Начальная вершина [in]

Стоп, % Конечная вершина [in]

[Старт], % Список для хранения пройденных вершин [in]

Путь, % Список вершин от Старт до Стоп [out]

0, % Аккумулятор пройденного пути [in]

Длина) % Суммарный путь от Старт до Стоп [out]

Программа для решения примера 1, использующая встроенную в Пролог машину поиска, работает на основании алгоритма поиска «сначала вглубь» и выглядит так:

implement main

open core, console, list

constants className = "com/visual-prolog/main".

classVersion = "$JustDate: $$Revision: $".

domains

решение = пара(char*,unsigned).

class facts – граф

ребро: (char,char,unsigned).  % (Вершина1,Вершина2,Длина)

class predicates

связь :(char,char,unsigned) nondeterm (i,o,o).

путь: (char,char,char*,char*,unsigned,unsigned) nondeterm (i,i,i,o,i,o).

вывод: (решение*).

clauses classInfo(className, classVersion).

ребро('a','b',60).   ребро('a','d',70).

ребро('b','c',50).   ребро('c','e',10).

ребро('c','f',60).   ребро('d','e',70).

ребро('e','g',20).   ребро('f','g',30).

ребро('f','h',20).   ребро('f','k',10).

ребро('g','h',30).

связь(Вершина1,Вершина2,Расстояние):-      ребро(Вершина1,Вершина2,Расстояние);

      ребро(Вершина2,Вершина1,Расстояние).

путь(Стоп,Стоп,Путь,Путь,Длина,Длина).

путь(Вершина,Стоп,Стек,Путь,Аккум,Длина):-

    связь(Вершина,Вершина1,Расстояние),

    not(isMember(Вершина1,Стек)),

    Аккум1=Аккум+Расстояние,

    путь(Вершина1,Стоп,[Вершина1|Стек],Путь,Аккум1,Длина).

вывод([пара(Путь,Длина)|Решения]):-

    writef("Путь = % \nДлина = % \n\n",Путь,Длина),

    вывод(Решения).

вывод([]).

run():-init(),

    write("Начальная вершина - "),Старт=readchar(),

    clearInput(),

    write("Конечная вершина - "),Стоп=readchar(),nl,

    Решения=[пара(Путь,Длина) || путь(Старт,Стоп,[Старт],Путь,0,Длина)],

    вывод(Решения),

    _ = readLine().

end implement main

goal

   mainExe::run(main::run).

Будьте внимательны! В зависимости от настроек операционной системы ввод символов автоматически «исправляется» в ту раскладку клавиатуры, которая задана по умолчанию.

Задание 1. Решить задачу из примера 1 для ориентированного графа. Подсказка: транзитивное замыкание для пути в ориентированном графе не нужно.

Задание 2. Решить задачу из примера 1 для вершин неориентированного графа, которые задаются в памяти координатами X,Y декартовой системы. При этом расстояние между вершинами вычисляется по известной формуле через корень квадратный их суммы квадратов координат отрезка. Подсказка: ребро между двумя вершинами с координатами X,Y и X1,Y1 соответственно, можно описать так:

ребро(X,Y, X1,Y1)

или так:

ребро(вершина(X,Y), вершина(X1,Y1))

предварительно описав домен вершина(integer X, integer Y).

Пример 2. Найдём минимальный путь на графе между двумя вершинами. В простейшем случае – для графов малой размерности, достаточно найти в списке решений то, которое имеет наименьший второй аргумент, т.е. суммарный путь:

implement main open core, console, list

constants className = "com/visual-prolog/main". classVersion = "$JustDate: $$Revision: $".

domains решение = пара(char*,unsigned).

class facts - граф ребро: (char,char,unsigned). % (Вершина1,Вершина2,Длина)

class predicates связь :(char,char,unsigned) nondeterm (i,o,o). путь: (char,char,char*,char*,unsigned,unsigned) nondeterm (i,i,i,o,i,o). comp: (решение,решение) -> compareResult.

clauses classInfo(className, classVersion).

ребро('a','b',60).   ребро('a','d',70). ребро('b','c',50).   ребро('c','e',10). ребро('c','f',60).   ребро('d','e',70). ребро('e','g',20).   ребро('f','g',30). ребро('f','h',20).   ребро('f','k',10). ребро('g','h',30).

связь(Вершина1,Вершина2,Расстояние):-    ребро(Вершина1,Вершина2,Расстояние);

    ребро(Вершина2,Вершина1,Расстояние).

путь(Стоп,Стоп,Путь,Путь,Длина,Длина).

путь(Вершина,Стоп,Стек,Путь,Аккум,Длина):-

    связь(Вершина,Вершина1,Расстояние),

    not(isMember(Вершина1,Стек)),

    Аккум1=Аккум+Расстояние,

    путь(Вершина1,Стоп,[Вершина1|Стек],Путь,Аккум1,Длина).

comp(пара(_,Y),пара(_,Y))=equal:-!.

comp(пара(_,X),пара(_,Y))=greater :- X>Y,!.

comp(_,_)=less.

run():-init(),

    write("Начальная вершина - "),Старт=readchar(),

    clearInput(),

    write("Конечная вершина - "),Стоп=readchar(),

    clearInput(),

    Решения=[пара(Путь,Длина) || путь(Старт,Стоп,[Старт],Путь,0,Длина)],

   пара(МинПуть,МинДлина) = list::minimumBy(comp,Решения),

   write(МинПуть,"   ",МинДлина),

   _ = readLine().

end implement main

goal

   mainExe::run(main::run).

Однако это не самое удачное решение, так как выбор минимального пути начинается только тогда, когда будут просмотрены и запомнены все возможные пути между двумя вершинами. А на это требуется много времени и памяти для графов большого размера. Поэтому, не используя специальные алгоритмы для поиска кратчайшего пути можно применить поисковую машину Пролога, то есть алгоритм поиска «сначала вглубь», дополнив его одной эвристикой – памятью последнего минимального пути. Это даст возможность при прохождении по очередному пути сравнивать его текущую длину с минимальной, и если текущая длина равна или превышает минимальную, то дальне идти смысла нет, надо вызывать искусственный откат назад.

Пример 3. Нижеприведённая программа демонстрирует поиск минимального пути на графе, используя алгоритм поиска «сначала вглубь», встроенный в машину вывода Пролога. Наилучшее текущее решение сохраняется в двух фактах базы данных минПуть и минДлина соответственно.

implement main open core, console, list

constants className = "com/visual-prolog/main". classVersion = "$JustDate: $$Revision: $".

domains решение = пара(char*,unsigned).

class facts – граф

ребро: (char,char,unsigned). % (Вершина1,Вершина2,Длина) минПуть: (char*) determ. минДлина: (unsigned) determ.

class predicates связь :(char,char,unsigned) nondeterm (i,o,o). путь: (char,char,char*,unsigned) nondeterm.

clauses classInfo(className, classVersion).

ребро('a','b',60).   ребро('a','d',70). ребро('b','c',50).   ребро('c','e',10). ребро('c','f',60).   ребро('d','e',70). ребро('e','g',20).   ребро('f','g',30). ребро('f','h',20).   ребро('f','k',10). ребро('g','h',30).

связь(Вершина1,Вершина2,Расстояние):-

    ребро(Вершина1,Вершина2,Расстояние);

    ребро(Вершина2,Вершина1,Расстояние).

путь(Стоп,Стоп,Путь,Аккум):-     минДлина(Длина),Длина>Аккум,

    assert(минДлина(Аккум)), assert(минПуть(Путь));

    not(минДлина(_)),

    assert(минДлина(Аккум)), assert(минПуть(Путь)).

путь(Вершина,Стоп,Стек,Аккум):-     связь(Вершина,Вершина1,Расстояние),     not(isMember(Вершина1,Стек)),     Аккум1=Аккум+Расстояние,     (минДлина(Длина),Длина>Аккум1;not(минДлина(_))),     путь(Вершина1,Стоп,[Вершина1|Стек],Аккум1).

run():-init(),     write("Начальная вершина - "),Старт=readchar(),

    clearInput(),     write("Конечная вершина - "),Стоп=readchar(),

    clearInput(),     путь(Старт,Стоп,[Старт],0),     fail;     минПуть(Путь),минДлина(Длина),     write(Путь,"   ",Длина),     _ = readLine(),!;     _ = readLine().

end implement main

goal

   mainExe::run(main::run).

В данной программе предикат поиска пути путь/4 стал четырёхарным, потеряв две выходные переменные, роль которых играют два дополнительных факта минПуть и минДлина.

Условие останова рекурсии содержит по сути два условия. Первое условие:

    минДлина(Длина),Длина>Аккум,

    assert(минДлина(Аккум)), assert(минПуть(Путь));

говорит о том, что если очередной найденный путь имеет меньшую длину, чем текущий минимальный путь (который был найден ранее) Длина>Аккум, то текущее минимальное решение надо обновить новыми значениями Путь и Аккум.

Второе условие срабатывает только один раз:

    not(минДлина(_)),

    assert(минДлина(Аккум)), assert(минПуть(Путь)).

Оно предназначено для записи в память первого найденного решения, то есть тогда, когда в памяти ещё нет фактов минПуть и минДлина. Проверка отсутствия факта в памяти осуществляется предикатом not(минДлина(_)).

Задание 3. Написать программу для поиска максимального пути на неориентированном графе, координаты вершин которого задаются в декартовой системе.

Задание повышенной сложности 1. Написать программу для поиска всех цепей заданной длины N от заданной вершины на неориентированном графе, координаты вершин которого задаются в декартовой системе. Цепь длины N – путь на графе, содержащий N смежных вершин. Вес цепи – расстояние от начальной до конечной вершин цепи. Подсказка: для поиска цепей надо добавить в предикат путь/6 счётчик числа пройденных вершин.

Задание повышенной сложности 2. Написать программу для поиска минимального цикла на полном графе. Полный граф – это граф у которого любые две вершины являются смежными, то есть между ними етсь ребро. Подсказка: для поиска минимального цикла достаточно найти цепь длиной N, минимального веса, где N – число всех вершин графа.

Соседние файлы в папке Лабораторные работы