Поиск в пространстве состояний
13.2. Поиск в ширину в пространстве состояний
В настоящем параграфе для решения задач применяется поиск в ширину в пространстве состояний. Для ограничения пространства поиска решения не продлеваются в ранее достигнутые состояния. Ведется поиск только одного оптимального решения.
Ниже приводится универсальный решатель задач методом поиска в ширину в пространстве состояний. Он реализуется в модуле breadth (листинги 13.7–13.8)2В листинге 13.8 строку из версии Visual Prolog 7.5 V = varM::new([]), в версии Visual Prolog 7.4 нужно заменить строкой V = varM{State*}::new([])..
predicates breadthSearch: (depth::move{State}, State, State, positive [out]) -> tuple{State*, string*} determ.Пример 13.7. Декларация класса breadth
open core, list, depth domains t{State} = t(State*, string*, positive). class predicates breadthPath: (move{State}, t{State}*, State*, State) -> t{State} nondeterm. clauses breadthSearch(Move, Start, Goal, N) = tuple(reverse(Path), reverse(Moves)):- t(Path, Moves, N) = breadthPath(Move, [t([Start], [], 0)], [Start], Goal), !. breadthPath(_, [t([Goal | Path], Ms, N) | _], _, Goal) = t([Goal | Path], Ms, N):- !. breadthPath(Move, [t([State|Path], Ms, N) | PL], StateList, Goal)= breadthPath(Move, append(PL, PL1), append(V:value, StateList), Goal):- V = varM::new([]), PL1 = [t([NextState, State | Path], [Str | Ms], N + 1) || NextState = Move(State, Str), not(isMember(NextState, StateList)), V:value := [NextState | V:value]].Пример 13.8. Имплементация класса breadth
Применим решатель к задаче о переливаниях. Имеется несколько сосудов, вмещающих целое количество литров воды, и источник воды (река). Требуется отмерить в первом сосуде заданное целое количество литров воды. Можно наливать воду в сосуды из источника, переливать воду из одного сосуда в другой, так чтобы либо первый сосуд становился пустым, либо второй сосуд полным, а также опорожнять сосуды. Обычно считается, что сосуды упорядочены по убыванию вместимости. В этом случае в первом сосуде можно отмерить любое целое количество литров, которое делится на наибольший общий делитель объемов всех сосудов. Потребуем, чтобы нужное количество литров отмерялось в первом, наибольшем сосуде, а остальные сосуды оказывались бы пустыми. Если сосудов два, то можно показать, что задача в этом случае имеет единственное оптимальное решение. Если сосудов более двух, то оптимальных решений более одного. Пространство состояний в этом случае велико, поэтому мы будем искать только одно оптимальное решение с помощью поиска в ширину.
open core, console, list, string, breadth class facts volums: (positive*) determ. clauses volums([17, 11, 8]). % volums([27, 17, 11, 8, 3]). class predicates jmove : depth::move{positive*}. clauses jmove(L, Str) = L1:- memberIndex_nd(A, I, L), A > 0, setNth(I, L, 0, L1), Str = format("Вылить % л из %-го сосуда", A, I + 1). jmove(L, Str) = L2:- volums(VL), memberIndex_nd(A, I, L), A > 0, memberIndex_nd(B, J, L), J <> I, V = nth(J, VL), B < V, Vol = math::min(V - B, A), setNth(I, L, A - Vol, L1), setNth(J, L1, B + Vol, L2), Str = format("Перелить % л из %-го сосуда в %-й", Vol, I + 1, J + 1). jmove(L, Str) = L1:- volums(VL), memberIndex_nd(A, I, L), V = nth(I, VL), A < V, setNth(I, L, V, L1), Str = format("Налить % л в %-й сосуд", V - A, I + 1). run():- Start = [0, 0, 0], Goal = [4, 0, 0], tuple([S0 | P], Moves) = breadthSearch(jmove, Start, Goal, _), V = varM::new(1), write("\t", S0), nl, forAll(zip(P, Moves), {(tuple(X, S)):- writef("%. %\n\t%\n", V:value, S, X), V:value := V:value + 1}), nl, fail; _ = readLine().Пример 13.9. Задача о переливаниях. Модуль ex4
Предикат memberIndex_nd/3 недетерминированно возвращает из списка элементы вместе с их индексами. Предикат setNth/4 заменяет элемент списка, стоящий в заданной позиции, заданным элементом. Предикат nth/2 возвращает элемент списка по его индексу. Предикат min/2 возвращает минимум двух чисел.
Используем поиск в ширину для решения задачи о переправе рыцарей и оруженосцев через реку с островом. Она отличается от первоначальной задачи тем, что на реке есть остров, на который можно высаживаться. Известно, что в этом случае решение существует для любого количества пар рыцарей и оруженосцев и двухместной лодки.
Следует перенести указанные ниже объявления в декларацию класса ex2 из его имплементации (листинг 13.10).
domains knightOrSquire = k(integer); s(integer). predicates moveFromTo: (positive, knightOrSquire*, knightOrSquire*, knightOrSquire* [out], knightOrSquire* [out], knightOrSquire* [out]) nondeterm. f: (knightOrSquire*) -> string.Пример 13.10. Декларация класса ex2
open core, console, list, string, breadth, ex2 domains kistate = tuple(loc, knightOrSquire*, knightOrSquire*, knightOrSquire*). loc = left; right; island. class predicates kimove : depth::move{kistate}. clauses kimove(tuple(left, L, Isl, R), Str) = tuple(right, L1, Isl, R1):- moveFromTo(2, L, R, L1, R1, B), Str = format("% from left to right", f(B)). kimove(tuple(island, L, Isl, R), Str) = tuple(right, L, Isl1, R1):- moveFromTo(2, Isl, R, Isl1, R1, B), Str = format("% from island to right", f(B)). kimove(tuple(left, L, Isl, R), Str) = tuple(island, L1, Isl1, R):- moveFromTo(2, L, Isl, L1, Isl1, B), Str = format("% from left to island", f(B)). kimove(tuple(right, L, Isl, R), Str) = tuple(left, L1, Isl, R1):- moveFromTo(1, R, L, R1, L1, B), Str = format("% from right to left", f(B)). kimove(tuple(island, L, Isl, R), Str) = tuple(left, L1, Isl1, R):- moveFromTo(1, Isl, L, Isl1, L1, B), Str = format("% from island to left", f(B)). kimove(tuple(right, L, Isl, R), Str) = tuple(island, L, Isl1, R1):- moveFromTo(1, R, Isl, R1, Isl1, B), Str = format("% from right to island", f(B)). run():- L = sort([s(1), k(1), s(2), k(2), s(3), k(3), s(4), k(4)]), Start = tuple(left, L, [], []), Goal = tuple(right, [], [], L), tuple([S0 | P], Moves) = breadthSearch(kimove, Start, Goal,_), V = varM::new(1), write("\t", S0), nl, forAll(zip(P, Moves), {(tuple(X, S)):- writef("%. %\n\t%\n", V:value, S, X), V:value := V:value + 1}), nl, fail; _ = readLine().Пример 13.11. Задача о рыцарях и оруженосцах с островом. Модуль ex5
Наконец, используем поиск в ширину для решения задачи о переправе мисиионеров и каннибалов через реку с островом (конец XIX в.). Если на реке имеется остров, на который можно высаживаться, то переправу можно организовать также для любого количества миссионеров и такого же количества каннибалов и двухместной лодки.
Указанные ниже объявления следует перенести в декларацию класса ex3 из имплементации этого класса (см. листинг 13.12).
predicates mcmoveFromTo: (positive, positive, positive*, positive*, positive* [out], positive* [out]) determ. f: (positive, positive) -> string.Пример 13.12. Декларация класса ex3
open core, console, list, string, breadth, ex3 domains mistate = tuple(loc, positive*, positive*, positive*). loc = left; right; island. class predicates mimove : depth::move{mistate}. clauses mimove(tuple(left, L, Isl, R), Str) = tuple(right, L1, Isl, R1):- Cb = std::downTo(2, 0), Mb = 2 - Cb, mcmoveFromTo(Mb, Cb, L, R, L1, R1), Str = format("% move from left to right", f(Mb, Cb)). mimove(tuple(left, L, Isl, R), Str) = tuple(island, L1, Isl1, R):- Cb = std::downTo(2, 0), Mb = 2 - Cb, mcmoveFromTo(Mb, Cb, L, Isl, L1, Isl1), Str = format("% move from left to island", f(Mb, Cb)). mimove(tuple(island, L, Isl, R), Str) = tuple(right, L, Isl1, R1):- Cb = std::downTo(2, 0), Mb = 2 - Cb, mcmoveFromTo(Mb, Cb, Isl, R, Isl1, R1), Str = format("% move from island to right", f(Mb, Cb)). mimove(tuple(right, L, Isl, R), Str) = tuple(left, L1, Isl, R1):- Cb = std::fromTo(0, 1), Mb = 1 - Cb, mcmoveFromTo(Mb, Cb, R, L, R1, L1), Str = format("% moves from right to left", f(Mb, Cb)). mimove(tuple(right, L, Isl, R), Str) = tuple(island, L, Isl1, R1):- Cb = std::fromTo(0, 1), Mb = 1 - Cb, mcmoveFromTo(Mb, Cb, R, Isl, R1, Isl1), Str = format("% moves from right to island", f(Mb, Cb)). mimove(tuple(island, L, Isl, R), Str) = tuple(left, L1, Isl1, R):- Cb = std::fromTo(0, 1), Mb = 1 - Cb, mcmoveFromTo(Mb, Cb, Isl, L, Isl1, L1), Str = format("% moves from island to left", f(Mb, Cb)). run():- N = 4, Start = tuple(left, [N, N], [0, 0], [0, 0]), Goal = tuple(right, [0, 0], [0, 0], [N, N]), tuple([S0| P], Moves) = breadthSearch(mimove, Start, Goal,_), V = varM::new(1), write("\t", S0), nl, forAll(zip(P, Moves), {(tuple(X, S)):- writef("%. %\n\t%\n", V:value, S, X), V:value := V:value + 1}), nl, fail; _ = readLine().Пример 13.3. Задача о миссионерах и каннибалах с островом. Модуль ex6
Упражнения
- Задача о шатком мосте. Бабушка, дедушка, мать, отец, дочь и сын должны перейти ночью через шаткий мост. У них имеется лишь один фонарь, находиться на мосту без фонаря нельзя. Идти по мосту одновременно могут не более двух человек. Найдите способ семье переправиться через мост за минимальное время, если бабушка может перейти мост за шесть минут, дедушка за пять, мать за четыре, отец за три, дочь за две и сын за одну минуту.
- Переправа семьи через реку (Алкуин, VIII в.). Мать и отец, каждый весом с воз, и два их сына, которые вместе весят воз, хотят переправиться через реку. В их распоряжении имеется лодка, которая выдерживает вес, равный одному возу. Как им переправиться через реку?
- Переправа группы людей через реку. Семья — родители, два сына и две дочери — и полицейский с заключенным должны переправиться через реку с помощью плота, который вмещает не более двух человек. Управлять плотом может только взрослый. Нельзя оставлять мать наедине с сыновьями, отца наедине с дочерьми и заключенного в присутствии других людей без полицейского.
- Задача о бочке. Бочка стоит у реки. С помощью двух кувшинов, вмещающих 8 и 5 литров воды, требуется получить в бочке 41 литр воды, не переливая воду из кувшина в кувшин.
- Задача о песочных часах. Имеются песочные часы на 8 и 3 минуты. Требуется отмерить 4 минуты.
- Задача о ферзях. Требуется расставить n ферзей на шахматной доске n x n так, чтобы они не били друг друга.
- Задача о раскраске. Требуется найти такую раскраску плоской карты, используя не более четырех цветов, чтобы никакие две соседние страны не были раскрашены в один и тот же цвет.
- Игра в 8. Требуется расставить по порядку восемь перепутанных фишек, на которых написаны номера от единицы до восьми, на поле 3 x 3. Одна из клеток поля пустая, в остальных находится по фишке. За один ход фишку можно передвинуть на соседнее пустое место.