Задача о кувшинах [1]

advertisement
Решение сложных задач на Прологе
Оглавление
Задача о волке, козе и капусте [1] ........................................................................................................................... 2
Задача о кувшинах [1]................................................................................................................................................. 4
Головоломка об олимпийцах [1] .............................................................................................................................. 6
Недетерминированное программирование .......................................................................................................... 8
Метод "образовать и проверить" [1] ....................................................................................................................... 9
Задача о ферзях [2]..................................................................................................................................................... 9
Задача о раскраске карты [1] .................................................................................................................................. 13
Программа "Быки и Коровы" [1] ............................................................................................................................. 15
Источники .................................................................................................................................................................... 16
1
Задача о волке, козе и капусте [1]
Задача заключается в следующем:
Фермер ( Farmer ), волк ( Wolf ), козел ( Goat ) и капуста ( Cabbidge ) находятся на одном
берегу. Надо перебраться на другой берег на лодке. Лодка перевозит только двоих. Нельзя
оставлять на одном берегу козу и капусту,козу и волка.
Главная проблема в формировании алгоритма - найти эффективное представление
структурой данных Лиспа информации о задаче.
Процес перевозки может быть представлен последовательностью состояний. Состояние
представляется отношением state c 4 аргументами, каждый из которых отражает размещение
обьектов F,W,G,S :
Farmer
Wolf
Goat
Cabbidge
state(e, w, e, w) - F,G in east side (e - east);
FWGC
W,C in west side (w - west).
Оставшаяся программа основывается на этих предикатах. В частности,они используются для
реализации четырех возможных действий фермера, выраженных в перевозе фермера через реку:

самого себя

W

G

C
Предикат opposite ( определена позже ) определяет новое размещение объектов, которые пересекли
реку.
move(state(X,X,G,C),state(Y,Y,G,C)):-opposite(X,Y).
move(state(X,W,X,C),state(Y,W,Y,C)):-opposite(X,Y).
move(state(X,W,G,X),state(Y,W,G,Y)):-opposite(X,Y).
/*
FARMER
/*
FARMER
/*FARMER
2
+
+
+
WOLF
*/
GOAT
*/
CABBIDGE*/
move(state(X,W,G,C),state(Y,W,G,C)):-opposite(X,Y). /* ONLY FARMER */
Теперь можно определить предикат opposite, который определяет другую сторону.
opposite(east,west).
opposite(west,east).
Предикат unsafe определен для проверки двух опасных состояний:

F находится на противоположном берегу от W,G

F находится на противоположном берегу от G ,C.
unsafe( state(F,X,X,_) ):- opposite(F,X).
/* The wolf eats the goat */
unsafe(
state(F,_,X,X)
opposite(F,X).
/* The goat eats the cabbage */
):-
path теперь определяется:
path(S,G,L,L1):move(S,S1),
not( unsafe(S1) ),
not( member(S1,L) ),
path( S1,G,[S1|L],L1),!.
path(G,G,T,T):-!.
/* The final state is reached
*/
Для решения можно использовать:
go:go(state(east,east,east,east),state(west,west,west,west)).
go(S,G):path(S,G,[S],L),
nl,write('A solution is:'),nl,
3
write_path(L),
fail.
go(_,_).
Для организации удобной формы вывода использованы следующие процедуры:
member(X,[X|_]).
member(X,[_|L]):-member(X,L).
write_move( state(X,W,G,C), state(Y,W,G,C) ) :-!,
write('The farmer crosses the river from '),
write(X),
write(' to '),
write(Y),nl.
write_move( state(X,X,G,C), state(Y,Y,G,C) ) :-!,
write('The farmer takes the Wolf from '),
write(X),
write(' of the river to '),
write(Y),nl.
write_move( state(X,W,X,C), state(Y,W,Y,C) ) :-!,
write('The farmer takes the Goat from' ),
write(X),
write(' of the river to '),
write(Y),nl.
write_move( state(X,W,G,X), state(Y,W,G,Y) ) :-!,
write('The farmer takes the cabbage from '),
write(X),
write(' of the river to '),
write(Y),nl.
write_path( [H1,H2|T] ) :- !,
write_move(H1,H2),write_path([H2|T]).
write_path( _ ).
Сама программа намного короче программы на лиспе.
Задача о кувшинах [1]
Задача о кувшинах воды состоит в следующем:
В кувшин А вмещается 5 литров, а в кувшин B входит 2 литра.
При старте 5-литровый кувшин полный, вы можете лить воду
из одного кувшина в другой или на землю, полностью наполнять
4
кувшины водой. Это делается до тех пор пока
вы не уверены, что кувшин B содержит ровно 1 литр.
две емкости
Решение:
Каждое состояние представляется парой
Aamout:Bamount
которая описывает сколько воды содержится в каждом кувшине.
Рассмотрим программу
?-op(100,yfx,':').
определить : как лево-ассоциативный оператор
Главный предикат:
solve(Current_state, Goal_state, Traversed_path, Solution_path).
где Traversed_path - список сделанных пока разливаний
solve(Goal, Goal, Path, Path).
Если текущее состояние - искомое состояние, то выводят путь к 4-ому параметру
solve(Current, Goal, Path, Solution) :- edge(Step, Current, New),
/* Найти 4 способа литья - 'Step' */
not marked(solve(New, Goal, _, _)),
/* Поиск графа требует проверки не искали ли мы этот узлел прежде */
solve(New, Goal, Path:Step, Solution).
/* Используйте рекурсию для поиска в глубину. При откате вернуться к
'pour'. */
edge(pour_a_down_drain, A:B , 0:B).
edge(pour_b_down_drain, A:B , A:0).
edge(pour_a_into_b, A:B, C:D) :A>0, B<2, T is A+B, (T>=2, C is T-2, D=2 ; T<2, C=0, D=T) .
edge(pour_b_into_a, A:B, C:D) :B0, A<5, T is A+B, (T=5, D is T-5, C=5 ; T<5, C=T, D=0) .
/*Проверяет, искался ли узел ранее. Правило
marked(X) :- asserta((marked(X):- !)), fail.
помечает узел, если это не было сделано ранее, и делает откат. Следующая проверка этого
узла пройдет успешно, так как узел был уже помечен. Reset_marked разрешает рекурсии
быть выполненной несколько раз.
reset_marked :- retractall(marked(_)),
asserta( (marked(X) :- asserta((marked(X):- !)), fail) ).
5
jug_problem(X,Y,Z) :- reset_marked, solve(X,Y,start,Z).
?-jug_problem(5:0, A:1, Answer).
Answer = start :
pour_a_down_drain:(O:0)
fill_up_a :(5:0)
pour_a_into_b :(3:2)
pour_a_down_drain :(0:2)
pour_b_into_a :(2:0)
fill_up_b :(2:2)
pour_b_into_a:(4:0)
fill_up_b: (4:2)
pour_b_into_a (5:1)
Головоломка об олимпийцах [1]
В качестве последнего примера рассмотрим решение логической головоломки. Поведение
этой программы будет подобно поведению задачи о раскрашивании карты. Логическая
головоломка состоит из нескольких фактов относительно небольшого числа объектов,
которые имеют различные атрибуты. Минимальное число фактов относительно объектов и
атрибутов связано с желанием выдать единственный вариант назначения атрибутов
объектам.
Метод решения логических головоломок опишем на следующем примере.
Три друга заняли первое, второе и третье места в соревнованиях универсиады. Друзья разной национальности, зовут их по-разному, и любят они разные виды спорта.
Майкл предпочитает баскетбол и играет лучше чем американец. Израильтянин Саймон
играет лучше теннисиста. Игрок в крикет занял первое место.
Кто является австралийцем? Каким спортом занимается Ричард?
Решение:
Подобные логические головоломки изящно решаются посредством конкретизации значений
подходящей структуры данных и выделения значения, приводящего к решению. Каждый ключ
к загадке преобразуется в факт относительно структуры данных. Это может быть сделано с
использованием абстракции данных до определения точной формы структуры данных.
Проанализируем первый ключ к разгадке: "Майкл предпочитает баскетбол и играет лучше
чем американец". Очевидно, речь идет о двух разных людях. Одного зовут Майкл и он
занимается баскетболом, в то время как второй - американец. Кроме того, Майкл лучше
играет в баскетбол чем американец. Предположим, что Friends - структура данных,
подлежащая конкретизации, тогда наш ключ может быть выражен следующей конъюнкцией
целей:
(did_better(Man1Clue1, Man2Clue1, Friends)
name_(Man1Clue1, michael), sport(Man1Clue1,basketball),
nationality(Man2Clue1,american)
Аналогично второй ключ можно представить конъюнкцией целей:
6
did_better(Man1Clue2, Man2Clue2, Friends)
name_(Man1Clue2, simon), nationality(Man1Clue2,israeli),
sport(Man2Clue2,tennis)).
Наконец третий ключ к разгадке выразится следующим образом:
first(Friends,ManClue3),sport(ManClue3,cricket)
Базовая программа для решения головоломок представлена программой 14.6. Вычислению
подлежит отношение solve_puzzle( Puzzle,Solution), где Solution является решением
головоломки Puzzle.
Головоломка представляется структурой puzzle(Clues,Queries,Solution), где структура
данных, подлежащая конкретизации, представляется ключами и вопросами а получаемые
значения определяются аргументом Solution.
Программа solve_puzzle тривиальна. Все, что она делает, состоит в последовательном
решении каждого ключа и вопроса, которые представляются как цели Пролога и
выполняются с использованием метапеременных.
solve_puzzle(puzzle(Clues,Queries,Solution),Solution) :solve(Clues),
solve(Queries).
solve([Clue|Clues]) :Clue, solve(Clues).
solve([]).
test_puzzle(Name,Solution) :structure(Name,Structure),
clues(Name,Structure,Clues),
queries(Name,Structure,Queries,Solution),
solve_puzzle(puzzle(Clues,Queries,Solution),Solution).
structure(test,[friend(N1,C1,S1), friend(N2,C2,S2), friend(N3,C3,S3)]).
clues(test,Friends,
[(did_better(Man1Clue1, Man2Clue1, Friends), % Clue 1
name_(Man1Clue1, michael), sport(Man1Clue1,basketball),
nationality(Man2Clue1,american)),
(did_better(Man1Clue2, Man2Clue2, Friends), % Clue 2
name_(Man1Clue2, simon), nationality(Man1Clue2,israeli),
sport(Man2Clue2,tennis)),
(first(Friends,ManClue3),sport(ManClue3,cricket))]).
queries(test, Friends,
[ member(Q1,Friends),
name_(Q1,Name),
nationality(Q1,australian), % Query 1
member(Q2,Friends),
name_(Q2,richard),
sport(Q2,Sport) % Query 2],
[['The Australian is', Name], ['Richard plays ', Sport]]).
did_better(A,B,[A,B,C]).
7
did_better(A,C,[A,B,C]).
did_better(B,C,[A,B,C]).
name_(friend(A,B,C),A).
nationality(friend(A,B,C),B).
sport(friend(A,B,C),C).
first([X|Xs],X).
Ключи и вопросы для нашего примера даны в программе 14.7. Рассмотрим структуру,
представляемую ключами, для решения этой головоломки. Каждый человек имеет три
атрибута и может быть представлен структурой friend(Name, Country , Sport). Есть три
друга, распределение мест которых в итоге соревнования имеет существенное значение. Это
наводит на мысль выбрать в качестве структуры данных для решения задачи упорядоченную
последовательность из трех элементов, т.е. список:
[frienfd(N1, C1, S1), friend (N2, C2, S2), friend (N3, C3, S3)].
В программе 14.7 даны определения условий did_better,name, nationality, sport и first,
которые, очевидно, легко программируются.
Объединение программ 14.6 и 14.7 дает нечто гиганское на тему "образовать и проверить".
Каждая из целей did_better и принадлежит (member) имеет дело с людьми, а остальные
цели обращаются к атрибутам людей. Какие функции они выполняют - генерацию или
проверку, зависит от того, конкретизированы их элементы или нет. Для любопытных
сообщаем ответ нашей головоломки: Майкл - австралиец, а Ричард играет в теннис.
Недетерминированное программирование
Одним из отличий вычислительной модели логического программирования от моделей
обычного программирования является недетерминизм. Недетерминизм - это техническое
понятие, используемое для сжатого определения абстрактных моделей вычислений. Однако
недетерминизм не только мощная теоретическая идея, но и полезное средство описания и
реализации алгоритмов.
Интуитивно ясно, что недетерминированная машина, перед которой возникло несколько
альтернативных путей решения, осуществляет корректный выбор очередного действия.
Подлинно недетерминированную машину реализовать нельзя, однако ее можно
моделировать или аппроксимировать. В частности, Пролог-интерпритатор аппроксимирует
недетерминированное поведение интерпретатора абстрактных логических программ с
применением механизма последовательного поиска и возвратов. Однако тот факт, что
детерминизм
только
"моделируется",
но
"реально
не
присутствует",
для
недетерминированного мышления во многих случаях не существенен, точно также как
несущественны для символьного мышления детали обработки указателей в процессе
унификации.
8
Метод "образовать и проверить" [1]
Метод "образовать и проверить" - общий прием, используемый при проектировании
алгоритмов и программ. Суть его состоит в том, что один процесс или программа генерирует
множество предполагаемых решений задачи, а другой процесс или программа проверяет эти
предполагаемые решения, пытаясь найти те из них, которые действительно являются
решениями задачи.
Обычно программы, реализующие метод "образовать и проверить", конструировать проще,
чем программы, в которых решение находится непосредственно, однако они менее
эффективны. Стандартный прием оптимизации программ типа "образовать и проверить"
заключается в стремлении погрузить программу проверки в программу генерации
предполагаемых решений настолько глубоко, насколько это возможно. В пределе программа
проверки полностью переплетается с программой генерации предполагаемых решений,
которая начинает порождать только корректные решения.
Используя вычислительную модель Пролога, легко создавать логические программы,
реализующие метод "образовать и проверить". Обычно такие программы содержат
конъюнкцию двух целей, одна из которых действует как генератор предполагаемых решений,
а вторая проверяет, являются ли эти решения приемлемыми:
find(X)  generate(X), test (X).
Эта Пролог-программа действует подобно обычной процедурной программе, выполняющей
генерацию вариантов и их проверку. Если при решении вопроса find(X)? Успешно
выполняется цель generate(X) с выдачей некоторого X, то затем выполняется проверка
test(X). Если проверка завершается отказом, то производится возвращение к цели generate,
с помощью которой генерируется следующий элемент. Процесс продолжается итерационно
до тех пор, пока при успешной проверке не будет найдено решение с характерными
свойствами или генератор не исчерпает все альтернативные решения.
Однако программисту нет необходимости интересоваться циклом "образовать и проверить".
Он Может рассматривать этот метод более абстрактно, как пример недетерминированного
программирования. В этой недетерминированной программе генератор вносит о некотором
элементе из области возможных решений, а затем просто проверяется, корректно ли данное
предположение генератора.
В качестве генератора обычно используется программа для предиката member (программа
3.12), порождающая множество решений. На вопрос member(X,[a,b,c])? Будут даны в
требуемой последовательности решения X = a, X = b, X = c. Таким образом, предикат
member можно использовать в программах, реализующих метод "образовать и проверить"
для недетерминированного выбора корректного элемента из некоторого списка.
Задача о ферзях [2]
На шахматной доске размером NxN требуется расставить N ферзей, так, чтобы они не били
друг друга.
Если представить позицию в виде набора из координат x и y всех ферзей, то задачу можно
сформулировать как задачу на удовлетворение ограничений.
9
Найти набор значений для x1,y1,x2,y2,...,xN,yN, из множества [1..N] такой, что для всех
различных i и j выполняются условия:
1. xi =/= xj (ферзи не стоят на одной вертикали);
2. yi =/= yj (ферзи не стоят на одной горизонтали);
3. |xi - xj| =/= |yi - yj| (ферзи не стоят на одной диагонали).
Можно попытаться решить эту задачку методом перебора
queens(N,Ys-Xs) :range(1,N, Ns),
length(Xs, N), members(Xs,Ns),
length(Ys, N), members(Ys,Ns),
safe(Ys-Xs).
Однако размер пространства перебора N2N должен насторожить. Для классического случая
N=8 придется рассмотреть порядка 1014 вариантов. Заметим, что среди координат x ферзей
x1,x2,...,xN ровно один раз встречается каждое из чисел 1,2,...,N. Таким образом, номера
ферзей можно рассматривать как координаты x и достаточно перебирать только координаты
y. Новая формулировка задачи:
Найти набор (y1,y2,...,yN), 1<yi<N такой, что для всех различных i и j выполняются условия:
1. yi =/= yj
2. |yi - yj| =/= | i-j |.
Новая схема перебора будет выглядеть так
queens(N,Ys) :range(1,N, Ns),
length(Ys, N),
members(Ys, Ns),
safe(Ys).
Теперь пространство перебора содержит NN (для N=8 -107) вариантов, что уже лучше, но все
еще многовато. Если учесть, что среди координат y ферзей y1,y2,...,yN каждое из чисел
1,2,...,N также встречается ровно один раз, можно еще раз переформулировать задачу.
Найти набор различных (y1,y2,...,yn), 1<yi<N, такой что |yi - yj| =/= | i-j | при i =/= j.
Конечно это, по сути, то же самое, просто мы переносим условие yi =/= yj из множества
ограничений в определение пространства перебора. Процедура перебора изменяется
незначительно.
queens(N,Ys) :range(1,N, Ns),
length(Ys, N),
selects(Ys, Ns),
safe(Ys).
Пространство перебора сокращается с NN до N!, что для N=8 означает 40 320 и уже
приемлемо. Теперь можно записать процедуру проверки.
10
safe([Y|Ys]) :- noattack(Y,Ys), safe(Ys).
safe([]).
noattack(Y,Ys) :noattack(Y,Ys,1).
noattack(_,[],_).
noattack(Q,[Y|Ys],D) :abs(Q-Y) =\= D,
D1 is D+1,
noattack(Q,Ys,D1).
Однако факториал есть факториал и если для N=8 время поиска всех ответов измеряется
секундами, то для N=10 это уже минуты, а для N=12 - часы. Ничего поделать с природой
задачи мы не можем - невозможно сделать из свиньи скаковую лошадь. Но можно получить
более быструю свинью. Мы можем, как и в предыдущем примере, переместить проверку
ограничений в процедуру генерации. Выбирая положение очередного ферзя, будем
проверять его совместимость с расставленными ранее. Тем самым мы объединим
процедуры selects и safe в одну процедуру place_queens. Эта процедура имеет
дополнительный аргумент Board, содержащий список уже расставленных ферзей.
queens(N,Ys) :range(1,N,Ns),
length(Ys,N),
place_queens(Ys,Ns,[]).
place_queens([Y|Ys], Ns, Board) :select(Y, Ns, Ns1),
noattack(Y, Board),
place_queens(Ys, Ns1, [Y|Board]).
place_queens([],_,_).
В сравнении с предыдущей эта программа выполняется раз в 20 быстрее при N=8, а кроме
того время вычислений пропорционально уже N!0.7.
Заметим, что процедура place_queens сама содержит пару из генератора и теста. Мы
сначала выбираем положение ферзя посредством select, а затем проверяем его на
совместимость, вызывая noattack. Заманчиво объединить и эти две процедуры. Однако,
чтобы такое объединение имело смысл, необходимо научиться выбирать только допустимые
положения. Для этого потребуется нечто большее, чем просто список уже расставленных
ферзей. В своей знаменитой статье [1] Никлаус Вирт использовал два булевых массива
представляющих /-диагонали и \- диагонали. Мы несколько модифицируем эту идею, и будем
использовать два списка Us и Ds, содержащие переменные, "охраняющие" диагонали.
Каждая переменная представляет одну диагональ. При размещении очередного ферзя
переменная из списка Ys связываетя с номером ферзя, но одновременно мы будем
связывать соответствующие переменные из списков Us и Ds. Поскольку переменная может
только иметь одно значение, как только она связана, никакой другой ферзь уже не может
оказаться на той же диагонали (этот же трюк мы применяли при раскраске карты). Процедура
place_queens после выбора положения для ферзя "сдвигает" Us и Ds на одну позицию в
разные стороны.
queens(N,Ys) :range(1,N, Ns),
length(Ys, N),
place_queens(Ns,Ys,_,_).
place_queens([N|Ns], Ys, Us, [D|Ds]):place1(N, Ys, Us, [D|Ds]),
place_queens(Ns, Ys, [U|Us], Ds).
11
place_queens([], _, _, _).
place1(N,[N|_], [N|_],[N|_]).
place1(N,[_|Cs],[_|Us], [_|Ds]):- place1(N,Cs,Us,Ds).
Такое усовершенствование дает выигрыш в скорости в 2-3 раза, но в целом оказывается не
очень существенным. Пространство перебора остается, по сути, тем же самым. Ускоряется
лишь проверка возможности очередного выбора. Тем не менее, эта программа иллюстрирует
важный метод решения задач, называемый распространением ограничений (constraints
propagation). Каждый выбор значения для очередной переменной ограничивает возможность
выбора для других переменных.
Распространение ограничений - довольно общий принцип, допускающий разнообразные
реализации. Важный способ реализации этого метода - вести явный учет возможных
значений для каждой переменной, в нашем случае - возможных положений для каждого
ферзя. Этот метод называется "просмотром вперед" (forward checking). Заведем список пар
(номер ферзя : список допустимых положений). Вначале каждый ферзь получает полную
свободу, его список имеет вид [1,2,...N]. Размещая очередного ферзя, мы удаляем
атакованные им позиции из допустимых положений оставшихся ферзей, ограничивая тем
самым возможность выбора.
queens(N, Queens) :range(1,N, Ns),
init_vars(Ns, Ns, V),
place_queens(V,Queens).
init_vars([X|Xs],Ys,[X:Ys|V]) :init_vars(Xs,Ys,V).
init_vars([], _, []).
place_queens([X:Ys|V],[X-Y|Qs]) :member(Y, Ys),
prune(V, X, Y, V1),
place_queens(V1,Qs).
place_queens([],[]).
prune([Q:Ys|Qs], X,Y, [Q:Ys1|Ps]) :sublist(noattacks(X,Y,Q), Ys, Ys1),
Ys1 \== [],
prune(Qs, X,Y, Ps).
prune([], _,_, []).
noattacks(X1,Y1,X2,Y2) :Y1 \== Y2,
abs(Y2 - Y1) =\= abs(X2 - X1).
Встроенный предикат sublist - аналог функции filter. Он возвращает список тех элементов
списка, для которых выполняется указанный предикат. В сущности, это более быстрый
способ выполнить
findall(T, (member(T,Ys),noattacks(X,Y,Q,T)) , Ys1)
Обратите внимание на проверку Ys1 \== [] . Если для какого либо из еще не размещенных
ферзей не остается свободных мест, prune завершается неудачей. Таким образом,
предполагаемое положение ферзя отвергается сразу.
Теперь предположим, что мы не стали включать указанную проверку в prune. Тогда, вместо
того чтобы тупо расставлять ферзей одного за другим, разумно сначала убедиться, что среди
не размещенных ферзей нет таких, которые нельзя разместить. Далее надо посмотреть, нет
12
ли таких ферзей, для которых существует всего один вариант размещения и, если есть,
выбрать именно такого. Аналогично, в общем случае лучше выбирать того ферзя, для
которого осталось меньше всего свободных мест. Итак, мы можем усовершенствовать
программу, изменив порядок расстановки ферзей. Поскольку теперь ферзи необязательно
будут размещаться по порядку, результат будем представлять в виде списка пар X-Y.
place_queens(V, [X-Y|Qs]) :queen_to_place(V,X:Ys,V1),
member(Y, Ys),
prune(V1, X, Y, V2),
place_queens(V2,Qs).
place_queens([], []).
Процедура queen_to_place выбирает ферзя подлежащего размещению из списка не
размещенных. Если запишем просто
queen_to_place([V|Vs],V,Vs).
то получим предыдущий вариант. Мы же будем выбирать "наиболее ограниченного" ферзя,
то есть того, у которого список возможных положений самый короткий.
queen_to_place([V|Vs], Q, Vx) :- select_boundest(Vs, V, Q, Vx).
select_boundest([], Q, Q, []).
select_boundest([X|Xs], Q, Qm, [Q|R]) :shorter(X, Q), !,
select_boundest(Xs, X, Qm, R).
select_boundest([X|Xs], Q, Qm, [X|R]) :select_boundest(Xs, Q, Qm, R).
shorter(_:L1, _:L2) :- length(L1,N1), length(L2,N2), N1<N2.
При сокращении пространства перебора надо соотносить размер этого сокращения с
затратами на его реализацию. На небольших досках эта программа оказывается даже
медленнее - затраты на реализацию более сложной схемы съедают весь выигрыш от
сокращения. Тем не менее, с ростом N сокращение играет все большую роль. Время,
выполнения программы растет как квадратный корень из N!, а это означает, что мы сможем
решить задачу для досок большего размера.
Упражнение. Процедура queen_to_place допускает очевидное усовершенствование. Найдите
и реализуйте его.
Стоит упомянуть и другие возможности. Кроме выбора "самого ограниченного" ферзя,
полезным может оказаться и выбор "самого ограничивающего", то есть такого, который
отнимает у других наибольшее число полей. Можно выбирать и "самого ограничивающего" из
"самых ограниченных". Возможен и более глубокий просмотр, когда производится проверка
на совместимость еще не размещенных ферзей. Например, если два ферзя претендуют на
одну и ту же строку продолжать поиск бессмысленно.
Задача о раскраске карты [1]
Следующая задача состоит в раскрашивании плоской карты так,
чтобы никакие две смежные области на ней не были раскрашены в
одинаковый цвет. Эта знаменитая задача, известная уже сотни лет,
была решена в 1976 году, когда было доказано, что для
13
раскрашивания любой плоской достаточно использовать четыре краски. На рисунке показана
простая карта, для корректного раскрашивания которой требуется четыре цвета. Это можно
доказать путем перечисления всех возможных вариантов раскраски. Следовательно, для
решения для решения задачи использование четырех красок является необходимым и
достаточным.
color_map([Region|Regions],Colors) :color_region(Region,Colors),
color_map(Regions,Colors).
color_map([],Colors).
color_region(region(Name,Color,Neighbors),Colors) :select(Color,Colors,Colors1),
members(Neighbors,Colors1).
select(X,[X|Xs],Xs).
select(X,[Y|Ys],[Y|Zs]) :- select(X,Ys,Zs).
members([X|Xs],Ys) :- member(X,Ys), members(Xs,Ys).
members([],Ys).
test_color(Name,Map) :map(Name,Map),
colors(Name,Colors),
color_map(Map,Colors).
map(test,[region(a,A,[B,C,D]), region(b,B,[A,C,E]),
region(c,C,[A,B,D,E,F]), region(d,D,[A,C,F]),
region(e,E,[B,C,F]), region(f,F,[C,D,E])]).
map(west_europe,
[ region(portugal,P,[E]), region(spain,E,[F,P]),
region(france,F,[E,I,S,B,WG,L]), region(belgium,B,[F,H,L,WG]),
region(holland,H,[B,WG]), region(west_germany,WG,[F,A,S,H,B,L]),
region(luxembourg,L,[F,B,WG]), region(italy,I,[F,A,S]),
region(switzerland,S,[F,I,A,WG]), region(austria,A,[I,S,WG])]).
colors(X,[red,yellow,blue,white]).
В программе , предназначенной для решения задачи о раскрашивании карты, также
использован принцип "образовать и проверить". Программа реализует следующий
недетерминированный алгоритм:
для каждой области карты
- выбрать цвет,
- выбрать из оставшихся цветов (или проверить) цвета соседних областей
Для реализации алгоритма необходимо выбрать подходящие структуры данных. Карта
представляется списком областей, каждая из которых имеет имя, цвет и список цветов, в
14
которые окрашены смежные
представляется списком
области.
Например
Карта
изображенная
на
рисунке
region(a, A, [B, C, D]), region(b, B, [A, C, E]),
region(c, C, [A, B, D, E, F]), region(d, D, [A, C, F]),
region(e, E, [B, C, F]), region(f, F, [C, D, E]).
Для того чтобы избежать раскраски одной и той же области в разные цвета на разных
интерпретациях алгоритма, используются общие переменные.
Отношение верхнего уровня рассматриваемой программы является color_map(Map,Color),
где Map - карта, представляемая описанным выше способом, Colors - список цветов,
используемых для раскрашивания карты. Выберем цвета: красный, желтый, голубой и белый.
Ядро алгоритма - определение отношения color_region(Region, Colors):
color_region(region(Name, Color, Neighbors), Colors) select(Color, Coors, Colors1),
member(Neighbors, Colors1).
Цели select и members в зависимости от того, конкретизированы или нет их аргументы, могут
либо производить генерацию вариантов, либо выполнять проверку.
Итогом выполнения программы о раскрашивании карты является
конкретизация структуры данных - карты. Вызовы предикатов select
и members могут рассматриваться как описания локальных
ограничений. Предикаты либо генерируют предполагаемое решение
посредством конкретизации элементов структуры, либо проверяют,
удовлетворяют ли конкретизированные значения локальным
ограничениям.
Программа "Быки и Коровы" [1]
В игровых программах описывается стратегия, приводящая к выигрышу игры. Для игры с
двумя игроками возможны варианты: человек-машина, машина-машина. Это программа
разгадывает секретный код. Другое название этой игры "Быки и коровы". Она является
хорошим примером того, как без особых размышлений легко запрограммировать на Прологе.
Правила
игры:
Игрок А выбирает секретный код, представляющий из себя последовательность из N
различных десятичных цифр (обычно N устанавливается равным 4). Игрок В пытается
угадать задуманный код и спрашивает игрока А о числе "быков" (число "быков" - количество
совпадающих цифр в одинаковых позициях предполагаемого и задуманного кодов; число
"коров" - количество совпадающих цифр, входящих в предполагаемый и задуманный код, но
находящихся на разных позициях). Код угадан если число быков равно N.
Алгоритм в общем описывается следующим образом:

Вводится некоторый порядок на множестве допустимых правилами предложений;

выдвижение очередных предположений учитывает, накопленную к тому времени
информмацию, и так до тех пор пока секретный код не будет раскрыт.
Рассмотренный алгоритм позволяет решить задачу - раскрыть код за 4-6 попыток ( как
опытный игрок ).
15
Источники
1. Морозов М.Н. - Логическое программирование. Курс лекций.
2. Дехтяренко И.А. - Декларативное программирование
(*) источники указаны в квадратных скобках в названиях (заголовках) задач
(*) источники представлены ссылками, по которым можно скачать указанные уч. пособия
16
Download