- •Глава 1. Введение в пролог
- •1. Декларативные и процедурные языки программирования
- •2. Пролог и логика предикатов. Внешние цели
- •3. Управление программой. Подцели. Механизм сопоставления
- •4. Внутренние подпрограммы унификации
- •Глава 2. Внутренние цели. Механизм возврата
- •1. Структура пролог-программы
- •2. Использование внутренних целей
- •3. Встроенный предикат fail
- •4. Сокращенные варианты внутренних запросов
- •5. Использование в запросах анонимных переменных
- •6. Механизм возврата
- •Глава 3. Типы данных и арифметика Turbo Prolog
- •1. Стандартные типы данных
- •2. Структуры, простые и составные
- •3. Структурные диаграммы
- •4. Использование в запросах анонимных переменных
- •5. Использование альтернативных доменов
- •6. Арифметика в Turbo Prolog
- •Глава 4. Предикат отсечения (!). Программирование альтернатив. Правила повтора
- •1. Повторения и возвраты
- •2. Отсечение (!)
- •3. Программирование альтернатив
- •4. Правило повтора
- •Глава 5. Методы организации рекурсии
- •1. Простая рекурсия
- •2. Метод обобщенного правила рекурсии
- •3. Граничное условие рекурсии. Нисходящая и восходящая рекурсии
- •4. Программа о подсчете числа точек
- •Глава 6. Списки
- •1. Основные понятия
- •2. Списки и турбо-пролог
- •3. Атрибуты списка
- •4. Внутреннее представление списков
- •5. Применение списков в программе
- •6. Метод разделения списка на голову и хвост
- •7. Поиск элемента в списке
- •8. Присоединение списка
- •9. Добавление и удаление элемента
- •10. Подсписок
- •11. Перестановки списка
- •Глава 7. Сортировка списков
- •1. Разделение списка на два
- •2. Сортировка списков методом вставки
- •3. Быстрая сортировка
- •4. Быстрая сортировка_1
- •5. Компоновка данных в список
- •Глава 8. Программирование алгоритмов с возвратом. Представление графов в turbo prolog
- •1. Задача о весах
- •2. Представление графов в turbo prolog
- •3. Поиск пути на неориентированном графе
- •4. Поиск гамильтоновых циклов
- •5. Поиск пути минимальной стоимости
- •Глава 9. Динамическая база данных
- •1. Турбо-пролог и реляционные базы данных
- •2. Описание предикатов динамических бд
- •3. Встроенные предикаты asserta, assertz, retract, retractall, save, consult
- •4. Создание динамической базы данных
- •5. Обсуждение проекта базы данных
- •6. Создание базы данных
- •7. Написание программных модулей
- •Глава 10. Глобальные переменные в turbo prolog
- •1. Модификация базы данных
- •2. Накопление результатов с помощью вынуждаемого возврата
- •3. Подсчет членов парторганизации
- •4. Поиск пути минимальной стоимости от a до z
- •Библиографический список
- •Оглавление
4. Поиск гамильтоновых циклов
Задача о поиске гамильтонова цикла на неориентированном графе — классическая задача теории графов и классическая NP-полная задача с точки зрения классификации задач по степени их вычислительной сложности.
Преобразуем предыдущую программу в программу поиска всех гамильтоновых циклов графа (рис 8.2). Воспользуемся тем, что мы умеем находить на графе все пути от A до Z. Добавим к нашей программе процедуру верхнего уровня gami, в которой будет проверяться, замкнется ли найденный путь в гамильтонов цикл.
Единственный недостаток такой программы то, что каждый цикл будет находиться и печататься дважды. Например, цикл 1-2-5-4-3-1 будет найден как замыкание пути 1-2-5-4-3 и при замыкании пути 1-3-4-5-2. Чтобы существенно не усложнять программу, смиримся с двойной печатью каждого цикла.
По определению, гамильтонов цикл проходит через все вершины графа ровно один раз, кроме вершины, являющейся одновременно и началом, и концом. Так как гамильтонов цикл содержит все вершины, то поиск его можно начать с любой вершины. Выберем некоторую вершину A и будем генерировать все пути, начинающиеся в A и заканчивающиеся в произвольной вершине. Процедура way будет генерировать путь, состоящий из различных вершин, поэтому останется проверить, замыкается ли он в полный цикл, то есть, содержит ли он все вершины графа, и существует ли ребро, соединяющее конец пути с вершиной A.
gami(A, Solution):-
% путь выходит из A и заканчивается в любой вершине
way(_, [A], Solution),
% замкнется ли он в гамильтонов цикл ?
full_cycle(A, Solution).
/* Программа 9.4 «Гамильтоновы циклы» */
domains
uzl=integer
list=uzl*
predicates
show_cycle
num_uzl(integer)
graph(list)
gami(uzl,list)
way(uzl,list,list)
full_cycle(integer,list)
full(integer,list)
sosed(uzl,uzl)
member(uzl,list)
goal
show_cycle.
clauses
num_uzl(5).
graph([1,2,3,4]).
graph([2,1,3,5]).
graph([3,1,2,4]) .
graph([4,1,3,5]).
graph([5,2,4]).
show_cycle:-
write("Начало цикла "),readint(A),nl,
gami(A, Solution), write(Solution),
nl, fail.
gami(A, Solution):-
way(_, [A], Solution),
full_cycle(A, Solution).
way(Z, [Z|Was1],[Z|Was1]).
way(Z, [X|Was1], Sol):-
sosed(X,Y),
not(member(Y,Was1)),
way(Z, [Y,X|Was1], Sol).
full_cycle(A, [H|S]):-
sosed(H, A),!, % путь замыкается в цикл,
num_uzl(N), % N — число вершин графа,
full(N, [H|S]).% в цикле все вершины графа
full(0,[]):-!. % в пустом пути 0 вершин
full(N, [_|T]):- % в цикле ровно N вершин
N1=N-1,
full(N1, T).
sosed(X,Y):-
graph([X|T]),
member(Y,T).
member(H,[H|_]).
member(X,[_|T]):-
member(X,T).
/* Конец программы */
5. Поиск пути минимальной стоимости
Будем искать пути на графе, ребрам которого приписаны веса, указанные в скобках (рис. 8.4).
рис. 8.4
Естественно возникает задача о поиске минимального пути от A до Z, если под длиной (стоимостью) пути понимать сумму весов входящих туда ребер. Такой граф будем хранить в виде утверждений базы данных — списков инцидентности graph1.
Процедура поска пути way, кроме нахождения пути, должна будет вычислять еще его стоимость. Поэтому к ней нужно будет добавить еще два аргумента — стоимость текушего пути (SW) и стоимость окончательного решения (S). Ей будет соответствовать описание
way1(uzl,list,integer,list,integer)
и первый вызов из процедуры верхнего уровня show_min
way1(Z, [A], 0, Solution, S),
где 0 — стоимость начального пути и S — стоимость решения.
Аналогично, процедуры sosed1 и member1 должны иметь в качестве аргументов стоимость соответствующего ребра и список стоимостей ребер к вершинам — соседкам.
sosed1(uzl,uzl,integer)
member1(uzl,list,list1,integer)
Процедура member1, выбирая очередную соседку из списка соседок, будет выбирать стоимость соответствующего ребра из списка стоимостей:
member1(Y,[Y|_],[Stoim|_],Stoim)
Затем они (соседка Y и стоимость Stoim) будут передаваться в процедуру sosed1. Процедура way1, получив их из процедуры sosed1, после соответствующей проверки, добавит Y к текущему пути, а вес ребра — к текущей стоимости.
way1(Z, [X|Was],SW, Sol,S):-
sosed1(X,Y,St), % St — вес ребра (X-Y).
not(member(Y,Was)),
SW1=SW+St,
way1(Z, [Y,X|Was],SW1, Sol,S).
SW1 — стоимость текущего пути, S — стоимость решения.
Нетрудно, немного изменив программу поиска пути, находить всевозможные пути от A до Z с их стоимостями. Но нам нужен один-единственный путь — путь минимальной стоимости.
Обычный алгоритмический способ — запоминать текущий минимум, сравнить с ним каждый свеженайденный путь и, в случае необходимости, менять минимум, — здесь неприемлем. Пути мы получаем за счет возвратов, а глобальной переменной, чтобы запоминать текущий минимум, у нас пока нет (пока мы не умеем работать с динамической базой данных).
Реализуем декларативную идею поиска минимального пути. Словами ее можно выразить так:
«мы сгенерировали путь, который будет минимальным, потому что невозможно сгенерировать путь меньшей стоимости». На Прологе это можно записать примерно так:
show_min:-
way1(Z, [A], 0, Solution, MinS),
not(way1(Z, [A], 0, _, S),
S < MinS).
Анонимная переменная стоит на месте НЕСУЩЕСТВУЮЩЕГО пути, вид которого нас не интересует.
Это правило будет работать следующим образом:
вначале way1 найдет некий путь Solution. Затем правило not будет генерировать пути и проверять, что их стоимость больше стоимости MinS. Если найдется путь меньшей стоимости, not даст отказ, и произойдет возврат к way1, которая сгенерирует новый путь Solution и т.д.
Наконец, будет найден такой путь Solution, для которого not сгенерирует ВСЕ пути, но ни один не окажется меньше. not даст успех, и все правило show_min окончится успехом.
В случае большого количество путей (например, для полного графа) выполнение этого правила приведет к КОМБИНАТОРНОМУ ВЗРЫВУ. В следующей главе будет показано, как существенно уменьшить число переборов.
Поскольку встроенный предикат not не может иметь в качестве аргумента более одного предиката, конъюнкцию way1(Z, [A], 0, _, S), S < MinS
нужно записать в виде отдельного правила:
min_way(Z, A, MinS):-
way1(Z, [A], 0, _, S),
S < MinS.
/* Программа 8.5 «Поиск пути от A до Z */
/* минимальной стоимости» */
domains
uzl=integer
list=uzl*
list1=integer*
predicates
num_uzl(integer)
graph1(list,list1)
show_way1
min_way(uzl,uzl,integer)
show_min
way1(uzl,list,integer,list,integer)
sosed1(uzl,uzl,integer)
member1(uzl,list,list1,integer)
member(uzl,list)
goal
show_min.% показать путь минимальной стоимости
clauses
num_uzl(5). % число вершин графа
graph1([1,2,3,4],[3,0,2]).
graph1([2,1,3,5],[3,4,5]).
graph1([3,1,2,4],[0,4,0]).
graph1([4,1,3,5],[2,0,1]).
graph1([5,2,4],[5,1]).
show_min:-
write("Начало пути "), readint(A),
write("Конец пути "), readint(Z),
way1(Z, [A], 0, Solution, MinS),
not(min_way(Z, A, MinS)),
% не существует пути стоимости меньшей, чем MinS
write("MIN ", Solution," ст.= ", MinS).
min_way(Z, A, MinS):-
way1(Z, [A], 0, _, S),
S < MinS.
% находим путь стоимости S меньшей, чем MinS.
way1(Z, [Z|Was],S, [Z|Was],S).
way1(Z, [X|Was],SW, Sol,S):-
sosed1(X,Y,St), % St — вес ребра (X-Y).
not(member(Y,Was)),
SW1=SW+St,
way1(Z, [Y,X|Was],SW1, Sol,S).
sosed1(X, Y, St):-
graph1([X|T], LS),
member1(Y, T, LS, St).
member1(H,[H|_],[S|_],S).
member1(X,[_|T],[_|ST],S):-
member1(X,T,ST,S).
% детерминированная процедура
% отыскивает первое вхождение H
member(H,[H|_]):-!.
member(X,[_|T]):-
member(X,T).
/* Конец программы */
В заключение рассмотрим более подробно свойства предиката not. Этот предикат определяется следующим образом:
not(P):-
P,!,fail.
not(P):- true.
Пусть P — детерминированный предикат. Если P истинен, то комбинация ! и fail вызывают отказ цели not(P), без возможности ее повторного передоказательства. Если P ложен, то второе правило согласует not(P) как истину.
В нашей программе P(min_way) недетерминированный предикат.
Если P истинен, то аналогично детерминированному P not(P) дает отказ.
Если P ложен, то прежде, чем произойдет переход на второе правило и согласование not(P), система переберет всевозможные способы передоказательства P (в нашей программе min_way сгенерирует всевозможные пути и сравнит их стоимости с минимальной). Если ВСЕ эти решения окажутся ложными, то только тогда not(P) будет согласован.