[net.lang.prolog] Graph searching in Prolog

OKeefe.R.A.%EDXA%UCL-CS@sri-unix.UUCP (01/28/84)

From:  O'Keefe HPS (on ERCC DEC-10) <OKeefe.R.A.%EDXA@UCL-CS>

Here is a set of three graph-searching programs written for
teaching purposes, and the 8-puzzle set up as an example for them to
solve.  They really belong in some sort of directory, they are

        [400,421,teach,search]puzzle
        [400,421,teach,search]depth
        [400,421,teach,search]breadt
        [400,421,teach,search]guess  on the DEC-10, and in

/usr/lib/prolog/search on the VAX here.  Beware: they've only been
tested on the 8-puzzle, but they do work on that.  Depth-first was
astonishingly difficult, I had a go, Peter Ross pointed out that the
result wasn't truly depth-first and came up with a modified version,
and I pointed out that *that* wasn't dpeth-first either, and came up
with the current version.  It's so hard to be stupid!

Divide the body of this message at the @@@@@ lines

@@@@@@@@ PUZZLE @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ %
File : /usr/lib/prolog/search/eight_puzzle % Author : R.A.O'Keefe %
Updated: 12 December 1983 % Purpose: illustrate the searching methods


/* The illustration I have chosen is the well known 8-puzzle.
    The state of the game is represented by a tuple of 9 labels,
    1 to 8 representing the movable tiles and x representing an
    empty square, together with an integer between 1 and 9 which
    says where the empty square is.  The operations are moving
    the empty square u(p), d(own), l(left), or r(right).  */

solution(5/b(
        1,2,3,
        8,x,4,
        7,6,5) ).

starting_position(9/b(
        1,2,3,
        7,8,4,
        6,5,x) ).

equivalent(X, X).

operator_applies(Operator, OldX/OldB, NewX/NewB) :-
        operator_ok(Operator, OldX, NewX),
        new_board(OldX, OldB, NewX, NewB).

operator_ok(u, OldX, NewX) :- OldX > 3, NewX is OldX-3.  
operator_ok(d, OldX, NewX) :- OldX < 7, NewX is OldX+3.  
operator_ok(l, OldX, NewX) :- OldX mod 3 =\= 1, NewX is OldX-1.  
operator_ok(r, OldX, NewX) :- OldX mod 3 =\= 0, NewX is OldX+1.


% new_board(OldX, OldB, NewX, NewB) % creates a New Board which is
essentially the same as the Old Board, % except that the labels at the
Old and New X positions have been % swapped.

new_board(OldX, OldB, NewX, NewB) :-
        functor(OldB, F, N),
        functor(NewB, F, N),
        arg(OldX, OldB, x),
        arg(NewX, OldB, L), % L is a label 1..8
        arg(OldX, NewB, L),
        arg(NewX, NewB, x),
        new_board(N, OldB, NewB).

new_board(0, _, _) :- !.  new_board(N, OldB, NewB) :-
        arg(N, NewB, Lab),
        var(Lab),
        !,
        arg(N, OldB, Lab),
        M is N-1,
        new_board(M, OldB, NewB).  new_board(N, OldB, NewB) :-
        M is N-1,
        new_board(M, OldB, NewB).


distance(X1/Board1, Distance) :-
        solution(X2/Board2),
        distance(9, Board1, Board2, 0, Distance).

distance(0, _, _, Distance, Distance) :- !.  distance(N, Board1,
Board2, SoFar, Distance) :-
        arg(N, Board1, Piece),
        arg(N, Board2, Piece),
        !,
        M is N-1,
        distance(M, Board1, Board2, SoFar, Distance).  distance(N,
Board1, Board2, SoFar, Distance) :-
        M is N-1,
        Accum is SoFar+1,
        distance(M, Board1, Board2, Accum, Distance).


@@@@@@@@ DEPTH @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ %
File : /usr/lib/prolog/search/depth_first % Author : R.A.O'Keefe %
Updated: 21 December 1983 % Purpose: define a schema for depth first
search

% This schema has four parameters:  % starting_position(Start) % binds
Start to the first position to try % solution(Position) % tests
whether a Position is a solution or not % operator_applies(Operator,
OldPosition, NewPosition) % enumerates all the operators which apply
to the % OldPosition, and also gives the NewPosition which % results
from that operator application.  % equivalent(Pos1, Pos2) % tests
whether the two positions are essentially % the same.  The idea is
that we will only look at % a position once.

% depth_first_search(DepthBound, Position, OperatorList) % returns the
first solution it can find, and the list of % Operators which produced
it: [O1,...,On] means that % applying O1 to the start position, then
O2, then ... and % finally On produces Position.  The number of
operators n % will not exceed DepthBound.  You may omit DepthBound, in
% which case it is taken to be 10000 (chosen for portability).  % The
eight-puzzle problem has a solution at depth 4, so 4 % is a good depth
to try.  I have tried depths up to 8 and % got an answer in a
reasonable time.


depth_first_search(Position, History) :-
        depth_first_search(10000, Position, History).


depth_first_search(DepthBound, Position, History) :-
        starting_position(Start),
        depth_first_search([d(DepthBound,Start,[])], [], Position,
History).


depth_first_search([d(_,Position,OpList)|_], _, Position, OpList) :-
        solution(Position),
        !.  % assuming you want only one 
depth_first_search([d(_,Position,_)|Rest], Seen, Answer, History) :-
        member(OldPos, Seen),
        equivalent(OldPos, Position),
        !,
        depth_first_search(Rest, Seen, Answer, History).  
depth_first_search([d(0,_,_)|Rest], Seen, Answer, History) :- !,
        depth_first_search(Rest, Seen, Answer, History).  
depth_first_search([d(Bound,Position,OpList)|Rest], Seen, Answer,
History) :-
        findall(Op, NP^operator_applies(Op, Position, NP), Ops),
        % we can't use setof, because that fails when there is no such
Op
        NewBound is Bound-1,
        fill_out(Ops, NewBound, Position, OpList, Descendants),
        append(Descendants, Rest, NewRest),
        NewSeen = [Position|Seen],
        !,
        depth_first_search(NewRest, NewSeen, Answer, History).


fill_out([], _, _, _, []) :- !.  fill_out([Op|Ops], Bound, Position,
OpList, [d(Bound,NewPos,[Op|OpList])|Rest]) :-
        operator_applies(Op, Position, NewPos), !,
        fill_out(Ops, Bound, Position, OpList, Rest).


@@@@@@@@ BREADT @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ %
File : /usr/lib/prolog/search/breadth_first % Author : R.A.O'Keefe %
Updated: 21 December 1983 % Purpose: define a schema for breadth first
search

% This schema has four parameters:  % starting_position(Start) % binds
Start to the first position to try % solution(Position) % tests
whether a Position is a solution or not % operator_applies(Operator,
OldPosition, NewPosition) % enumerates all the operators which apply
to the % OldPosition, and also gives the NewPosition which % results
from that operator application.  % equivalent(Pos1, Pos2) % tests
whether the two positions are essentially % the same.  The idea is
that we will only look at % a position once.

% breadth_first_search(Position, OperatorList) % returns the first
solution it can find, and the list of % Operators which produced it:
[O1,...,On] means that % applying O1 to the start position, then O2,
then ... and % finally On produces Position.  There can be no shorter 
% solution than this, though there may be other solutions % of the
same length.


breadth_first_search(Position, History) :-
        starting_position(Start),
        breadth_first_search([Start-[]], [Start], Position, History).


breadth_first_search([Position-OpList|Rest], Seen, Position, OpList)
:-
        solution(Position),
        !.  % assuming you want only one 
breadth_first_search([Position-OpList|Rest], Seen, Answer, History) :-
        findall(Operator, new_position(Operator, Position, Seen),
Ops),
        % we can't use setof, because that fails when there is no such
Op
        fill_out(Ops, Position, OpList, Seen, NewSeen, Descendants),
        append(Rest, Descendants, NewRest), !,
        breadth_first_search(NewRest, NewSeen, Answer, History).


new_position(Operator, Position, Seen) :-
        operator_applies(Operator, Position, NewPos),
        \+ (
            member(OldPos, Seen),
            equivalent(OldPos, NewPos)
        ).


fill_out([], _, _, Seen, Seen, []) :- !.  fill_out([Op|Ops], Position,
OpList, Seen, NewSeen, [NewPos-[Op|OpList]|New]) :-
        operator_applies(Op, Position, NewPos), !,
        fill_out(Ops, Position, OpList, [NewPos|Seen], NewSeen, New).


@@@@@@@@ GUESS @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ %
File : /usr/lib/prolog/search/guess_first % Author : R.A.O'Keefe %
Updated: 21 December 1983 % Purpose: define a schema for guess first
search

% This schema has five parameters:  % starting_position(Start) % binds
Start to the first position to try % solution(Position) % tests
whether a Position is a solution or not % operator_applies(Operator,
OldPosition, NewPosition) % enumerates all the operators which apply
to the % OldPosition, and also gives the NewPosition which % results
from that operator application.  % equivalent(Pos1, Pos2) % tests
whether the two positions are essentially % the same.  The idea is
that we will only look at % a position once.  % distance(Position,
Distance) % returns an estimate of how far the Position is % from a
solution.  This is only used to rank the % descendants of a node, so
the actual values of % the estimate don't matter too much.  See BEST %
for a method where the values *do* matter.

% guess_first_search(Position, OperatorList) % returns the first
solution it can find, and the list of % Operators which produced it:
[O1,...,On] means that % applying O1 to the start position, then O2,
then ... and % finally On produces Position.


guess_first_search(Position, History) :-
        starting_position(Start),
        guess_first_search([Start-[]], [Start], Position, History).


guess_first_search([Position-OpList|_], _, Position, OpList) :-
        solution(Position),
        !.  % assuming you want only one 
guess_first_search([Position-OpList|Rest], Seen, Answer, History) :-
        findall(Operator, new_position(Operator, Position, Seen),
Ops),
        % we can't use setof, because that fails when there is no such
Op
        fill_out(Ops, Position, OpList, Seen, NewSeen, Descendants),
        rank(Descendants, ByOrderOfGuess),
        append(ByOrderOfGuess, Rest, NewRest),
        !,
        guess_first_search(NewRest, NewSeen, Answer, History).


new_position(Operator, Position, Seen) :-
        operator_applies(Operator, Position, NewPos),
        \+ (
            member(OldPos, Seen),
            equivalent(OldPos, NewPos)
        ).


fill_out([], _, _, Seen, Seen, []) :- !.  fill_out([Op|Ops], Position,
OpList, Seen, NewSeen,
                [Distance-(NewPos-[Op|OpList])|New]) :-
        operator_applies(Op, Position, NewPos),
        distance(NewPos, Distance), !,
        fill_out(Ops, Position, OpList, [NewPos|Seen], NewSeen, New).


rank(Keyed, Ranked) :-
        keysort(Keyed, Sorted),
        strip(Sorted, Ranked).

strip([], []) :- !.  strip([_-H|T], [H|R]) :-
        strip(T, R).


@@@@@@@@ -END- @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@