CS538: Spring'06: Project#3: Prolog

Sample Solutions


NOTE:This is not the ONLY solution or the BEST solution. It is a solution that would obtain full points. You are free to use any algorithm and Prolog feature you like in your solutions, as long as you can produce the right answer and as long as you are not abusing the language.

1.

A straightforward translation of the definition of a median into Prolog rules.

First we define partition(Pivot,List,Smaller,Greater) which is true if List is a list of integers, Pivot is an element of List, and Smaller and Greater are lists of the elements of List that are smaller and respectively greater than Pivot. The definition loops over List by inspecting the head and recursing for the tail. Apart from the base case, there are three clauses for the three cases of the head of the current list being equal to, smaller or greater than the pivot element. In the last two cases, we add the head to the appropriate list and recurse.

Given partition, the median predicate is simple: M is a median of list L if it is a member of L, and if it partitions L into two lists S and G of equal length. Membership and list length functions are borrowed from the class notes.

 
member(X,[X|_]).
member(X,[_|Y]):- member(X,Y).

len([],0).
len([_|T],N) :- len(T,M), N is M+1.


partition(Pivot,[],[],[]).
partition(Pivot,[Pivot|Rest],Smaller,Greater) :-
partition(Pivot,Rest,Smaller,Greater).
partition(Pivot,[Element|Rest],[Element|SmallerRest],Greater) :-
Element < Pivot,
partition(Pivot,Rest,SmallerRest,Greater).
partition(Pivot,[Element|Rest],Smaller,[Element|GreaterRest]) :-
Element > Pivot,
partition(Pivot,Rest,Smaller,GreaterRest).


median(List,Median) :-
member(Median,List),
partition(Median,List,Smaller,Greater),
len(Smaller,N),
len(Greater,N).

2.

(a)

The key notion in any set implementation is set membership. All other functions build on this basic concept. For part (a), we define the setMember(E,S) predicate identical to the usual list member predicate. This works for sets with atomic elements.

Next, we define setValid(S) which tests if the input list is a valid set, i.e., if it does not have repeated elements. This is easily expressed recursively by checking that the head of the list is not a member of the tail, and that the tail itself is a valid list.

Finally ,we define set equality as we did for the Scheme solution: S1 == S2 iff S1 is a subset of S2 and S2 is a subset of S1. The subset relationship has a trivial recursive definition.

 
setMember(X,[X|_]).
setMember(X,[_|Y]) :-
setMember(X,Y).

setValid([]).
setValid([X|Y]) :-
not setMember(X,Y),
setValid(Y).

setSubset([],X).
setSubset([X|Y],S) :-
setMember(X,S),
setSubset(Y,S).

setEq(X,Y) :-
setValid(X),
setValid(Y),
setSubset(X,Y),
setSubset(Y,X).

(b)

All we need to change for this part is the notion of set membership. We now need to allow sets as members also. We add another clause to setMember which uses the setEq predicate to compare set elements if they are sets themselves. Atomic elements get compared with the second clause, while list (set) elements use the first clause. The rest of the code is unchanged from part (a).

 
setMember(X,[X|_]).
setMember(X,[S|_]) :-
setEq(X,S).
setMember(X,[_|Y]) :-
setMember(X,Y).

3.

This question is an exercise in constraint satisfaction: you need to search for a solution in a state space, subject to some constraints: a problem naturally suited to Prolog.

Express each known constraint in Prolog and use the conjunction of the constraints as the top-level goal for Prolog. There are two kinds of constraints: those for individual variables, and those for the whole list of variables.

Constraints on individual variables: M is known to be 1, O is 0, S is either 8 or 9, N is E+1, and the rest (and N too) are all digits between 0 and 9. We define a digit predicate that lists the single-digit integers, and then define a predicate for the constraint on each variable. Each predicate is passed both the variable and the whole solution, in case its value depends on other variables (as with N).

Constraints on the whole list: all variables have to map to distinct integers. We implement using the alldistinct predicate, which is identical to the setValid predicate of Q.2. The final constraint is that when variables are mapped, SEND + MORE = MONEY.

Finally, soln is a simple conjunction of these constraints, with the individual constraints ordered before the whole-list constraints. In general state space search, the formulation and ordering of constraints can affect search time. E.g., if we defined constraints in this order: all variables are single digits, they are distinct, individual variables have such and such constraints, whole solution has these constraints, then the search would take more time since a number of variables satisfying earlier more relaxed constraints would not satisfy the later stricter ones. In general, it helps to consider more strict constraints first to limit the degree of freedom for later constraints.

The unique solution is [D,E,M,N,O,R,S,Y] = [7, 5, 1, 6, 0, 8, 9, 2], 9567 + 1085 = 10652.

 
member(X,[X|_]).
member(X,[_|Y]) :- member(X,Y).

digit(0).
digit(1).
digit(2).
digit(3).
digit(4).
digit(5).
digit(6).
digit(7).
digit(8).
digit(9).

constraint_D(D, _) :-
digit(D).

constraint_E(E, _) :-
digit(E).

constraint_M(M, _) :-
M is 1.

constraint_N(N, Solution) :-
Solution = [_,E,_,N,_,_,_,_],
N is E + 1,
digit(N).

constraint_O(O, _) :-
O is 0.

constraint_R(R, _) :-
digit(R).

constraint_S(S, _) :-
S is 8.
constraint_S(S, _) :-
S is 9.

constraint_Y(Y, _) :-
digit(Y).


alldistinct([]).
alldistinct([Variable|Rest]) :-
not member(Variable,Rest),
alldistinct(Rest).



soln(Solution) :-
Solution = [D,E,M,N,O,R,S,Y],
constraint_M(M, Solution),
constraint_O(O, Solution),
constraint_S(S, Solution),
constraint_E(E, Solution),
constraint_N(N, Solution),
constraint_D(D, Solution),
constraint_R(R, Solution),
constraint_Y(Y, Solution),
alldistinct(Solution),
SEND is (1000 * S + 100 * E + 10 * N + D),
MORE is (1000 * M + 100 * O + 10 * R + E),
MONEY is (10000 * M + 1000 * O + 100 * N + 10 * E + Y),
MONEY is SEND + MORE.

4.

As explained in the submission guidelines, this question is an exercise in showing the fuzzy and invertible nature of inputs and outputs in Prolog programs. You are lead from a straightforward algorithmic computation of the power set, to a declarative specification that can compute, check and search for <set, power set> combinations.

(a)

This part requires you to compute the power set of a given set. We reuse the power set algorithm used for Q.1 of Proj#1/Scheme, and translate the original subsets, extend and distrib functions into their equivalent Prolog predicates. The recursive list processing translates naturally.

The only additional nit is that we constrain the subset function to work with a known set and an unknown (to be computed) power set. This is done by asserting that Set is not an unbound variable, while Powerset is an unbound variable. The first cut (!) forces Prolog not to search for other subsets clauses once we know that we have to compute the power set from a given set, while the second cut tells Prolog that this subsets is a 1-1 function, producing only one answer, thus not requiring backtracking to try other solutions.

 
member(X,[X|_]).
member(X,[_|Y]) :-
member(X,Y).

append([],L,L).
append([H|T1],L2,[H|T3]) :-
append(T1,L2,T3).

distrib(_,[],[]).
distrib(E,[Subset|RestSubsets],[[E|Subset]|RestSubsetsWithElement]) :-
distrib(E, RestSubsets, RestSubsetsWithElement).

extend(E,SubsetsWithoutElement,Powerset) :-
distrib(E,SubsetsWithoutElement,SubsetsWithElement),
append(SubsetsWithoutElement,SubsetsWithElement,Powerset).

% -- base case
subsets([],[[]]).

% -- Set is ground, Powerset is free
subsets(Set,Powerset) :-
nonvar(Set),
var(Powerset),
!,
Set = [Element|Rest],
subsets(Rest,RestPowerset),
extend(Element,RestPowerset,Powerset),
!.

(b)

This part requires you to extend the solution of part (a) to allow checking that a given power set is indeed the power set of a given set. We cannot use our earlier solution directly since it is an algorithm implementing a 1-1 function whcih produces a unique power set from a given set, with subsets and elements in a specific order. The input power set need not have the subsets and elements of subsets in the same order.

However, the input power set will be permutation of the calculated power set of the input set (with the caveat that corresponding power set elements, being sets themselves, can be permutations of each other). Hence, we add another subsets clause which applies when both the input set and power set are ground (nonvar). The input set Set is used to calculate a power set SomePowerset (using the first subsets clause), which is then checked against the input power set Powerset using the permutation predicate. We extend the permutation predicate from the class notes, by allowing the list elements to be lists themselves (since the power set contains sets). We add a check that the element S used to split the list L is a permutation of the head H of the second list, and another clause that allows atomic values to be trivially permutations of each other.

Note that we could have also used the set equality predicate from Q.2(b) to compare the input power set against the calculated power set.

 
permutation(X,X) :-
atomic(X),
!.
permutation([],[]).
permutation(L,[H|T]) :-
append(V,[S|U],L),
append(V,U,W),
permutation(S,H),
permutation(W,T).

% -- base case
subsets([],[[]]).

% -- Set is ground, Powerset is free
subsets(Set,Powerset) :-
nonvar(Set),
var(Powerset),
!,
Set = [Element|Rest],
subsets(Rest,RestPowerset),
extend(Element,RestPowerset,Powerset),
!.

% -- Set is ground, Powerset is ground
subsets(Set,Powerset) :-
nonvar(Set),
nonvar(Powerset),
!,
subsets(Set,SomePowerset),
permutation(SomePowerset,Powerset),
!.

(c)

This part is the most free form: neither the set nor its power set is specified, we are expected to generate all possible bindings of < set, power set > combinations, using variables as placeholders, thus not distinguishing sets of the same size.

As it turns out, our earlier code can work just fine for this case if we remove the check for Set being ground and if we also remove the final cut from the first subsets clause, allowing Prolog to backtrack after generating a solution. The generation is seeded by the base clause of a null set, and then our first main subsets clause automatically starts generating larger and larger sets.

 
% -- base case
subsets([],[[]]).

% -- Set is ground or free, Powerset is free
subsets(Set,Powerset) :-
var(Powerset),
!,
Set = [Element|Rest],
subsets(Rest,RestPowerset),
extend(Element,RestPowerset,Powerset).

% -- Set is ground, Powerset is ground
subsets(Set,Powerset) :-
nonvar(Set),
nonvar(Powerset),
!,
subsets(Set,SomePowerset),
permutation(SomePowerset,Powerset),
!.

(d)

This part is also a search, we now want to invert the algorithm and find the set that can generate a given power set. We do this by extending our code for part (b). Our earlier code checks that a given set and power set are indeed in the right relation. We insert code that generates candidate sets and then continues with the earlier check.

Evidently, we need to limit the size of the candidate sets we choose. We make an observation that eliminates the need for a search at all. If a set S has power set PS, then S is itself an element of PS and it is the longest set in PS. Hence, we add a longest(PowerSet,LongestSet,MaxLength) predicate that is true if LongestSet is the longest set in PowerSet and has length MaxLength. Once we compute the longest element of the input power set, we then use the code from part (b) that checks whether a set's power set matches the given power set.

There is an additional wrinkle though. We would keep this additional code in a separate clause, in which case it would be a copy of the clause in part (b) with the additional code above. However, we would like to merge the 2 clauses together, so that we have one clause when Powerset is free (handling parts (a) and (c)) and another clause when Powerset is ground (handling parts (b) and (d)). Our additions above would break the logic for part (b). Since the input set is specified, it will be equal to the longest set in the input power set but may not have the elements in the same order, i.e., the input set might be a permutation of the longest set in the power set. So we add a call to permutations to allow the input set to be a permutation of the longest set in the power set.

Here's the code that handles all combinations of input and output parameter bindings. Note that we have retained the two cuts in the second clause, since backtracking is only needed in the first clause, to handle part (c). All other parts have unique solutions.

 
longest([S],S,N) :- 
len(S,N).
longest([H|T],H,N) :-
longest(T,S,M),
len(H,N),
N >= M.
longest([H|T],S,N) :-
longest(T,S,N),
len(H,M),
M < N.

% -- base case
subsets([],[[]]).

% -- Set is ground or free, Powerset is free
subsets(Set,Powerset) :-
var(Powerset),
!,
Set = [Element|Rest],
subsets(Rest,RestPowerset),
extend(Element,RestPowerset,Powerset).

% -- Set is ground or free, Powerset is ground
subsets(Set,Powerset) :-
nonvar(Powerset),
Powerset = [_|[_|_]], % -- at least 2 elements, 1 element case = [[]]
!,
longest(Powerset,LongestSubset,MaxLength),
permutation(LongestSubset,Set),
subsets(Set,SomePowerset),
permutation(SomePowerset,Powerset),
!.

Grading Key

Total = 100 points (Q1:20 + Q2:25 + Q3:15 + Q4:40)

Points awarded for each successful test input as listed below.

Points deducted for: abuse of language, excessive use of extra-logical predicates and operators. Deductions on case-by-case basis.

Partial credit awarded on case-by-case basis.

Q1.
median(L,M): 20 points
Input Expected Output Points
median([1],M). M = 1 ;
No
5
median([7,2,4,5,3,6,1],M). M = 4 ;
No
5
median([8,3,2,11,13,1,7],7). Yes 5
median([1,2,3,4,5,6,7,8,9],4). No 5

Q2.
(a)
setEq(S1,S2): 10 points
Input Expected Output Points
setEq([],[]). Yes 2
setEq([george,richard,dubya],[richard,george,dubya]). Yes 4
setEq([3,colin,3,paul],[paul,colin,3]). No 4

(b)
setEq(S1,S2): 15 points
Input Expected Output Points
setEq([a,[b,c],d],[d,[c,b],a]). Yes 3
setEq([a,[b,c],[d]],[[a,b],[c,d]]). No 3
setEq([a,[b,c],[c,b]],[a,[b,c],a]). No 3
setEq([a,[b,[c,[d,[e,[f]]]]]],[[[[[[f],e],d],c],b],a]). Yes 3
setEq([a,[b,[c,[6,[e,[f]]]]]],[[[[[[f],e],d],c],b],a]). No 3

Q3.
soln([D,E,M,N,O,R,S,Y]: 15 points
Input Expected Output Points
soln([D,E,M,N,O,R,S,Y]). D = 7
E = 5
M = 1
N = 6
O = 0
R = 8
S = 9
Y = 2 ;
No
15

Q4.
(a)
subsets(S,PS): 10 points
Input Expected Output Points
subsets([],PS). PS = [[]] ;
No
2
subsets([1,a,z],PS). PS = a set equal to [[], [1], [a], [z], [1,a], [1,z], [a,z], [1,a,z]] ;
No
4
subsets([scheme,sml,prolog,python,csharp],PS). PS = [.... all 32 subsets of [scheme,sml,prolog,python,csharp] ...] ;
No
4

(b)
subsets(S,PS): 10 points
Input Expected Output Points
subsets([],[[]]). Yes 2
subsets([1,a,z],[[z], [a], [1], [], [1,a], [1,z], [a,z], [1,a,z]]). Yes 4
subsets([1,a,z],[[z], [a], [1], [1,a], [1,z], [a,z], [1,a,z]]). No 4

(c)
subsets(S,PS): 10 points
Input Expected Output Points
subsets(S,PS). (variable names not important below)
S = []
PS = [[]] ;

S = [_G1]
PS = [[], [_G1]] ;

S = [_G1, _G2]
PS = [[], [_G1], [_G2], [_G1,_G2]] <Enter>
Yes
2

4

4

(d)
subsets(S,PS): 10 points
Input Expected Output Points
subsets(S,[[]]). S = [] ;
No
2
subsets(S,[[z], [a], [1], [1,a], [1,z], [a,z], [], [1,a,z]]). S = a set equal to [1,a,z] ;
No
4
subsets(S,[[z], [a], [1], [1,a], [1,z], [a,z], [1,a,z]]). No 4