Opgaven Logisch Programmeren

Hieronder een aantal opgaven over programmeren in Prolog, met uitwerkingen. De meeste zijn ontleend aan het boek The Art of Prolog (2nd edition) van Leon Sterling en Ehud Shapiro (MIT Press, 1994, ISBN 0-262-69163-9), daarnaast zijn er enkele opgaven van Gertjan van Noort en Gerard Renardel. Alle programma's zijn geschreven in SWI Prolog.

Elementaria

Geef van de volgende termen aan of ze `ground' (gesloten) zijn of niet.

x.
father(john).
father(John).
father(x, y).

Ga na of mother(abraham,isaac) een instantie is van

mother(X,isaac)
mother(X,X)
mother(Y,X)
X(abraham, isaac)

Ga ook na of mother(abraham,isaac) een gemeenschappelijke instantie is van

mother(X,isaac) en mother(Y,isaac)
mother(X,isaac) en mother(abraham,X)
mother(X,isaac) en mother(abraham,Y)
mother(X,Y) en mother(abraham,X)
mother(abraham,Z) en mother(Y,X)

Uitwerkingen

Een eerste logisch programma

Probeer het volgende logische programma uit.

kleur(rood).
kleur(blauw).
kleur(groen).
kleur(geel).

% kaart(Gr,Fr,Dr,Ov,Ge,Fl,Nb,Ut,Nh,Zh,Lb,Zl) :-
%     de provincies Groningen,...,Zeeland, aangeduid door de variabelen
%     Gr,...,Zl, krijgen een kleur toegewezen zodanig dat aangrenzende
%     provincies verschillende kleuren krijgen.
kaart(Gr,Fr,Dr,Ov,Ge,Fl,Nb,Ut,Nh,Zh,Lb,Zl) :-
    Gr \== Fr, Gr \== Dr,
    Fr \== Dr, Fr \== Ov, Fr \== Fl,
    Dr \== Ov,
    Ov \== Fl, Ov \== Ge,
    Ge \== Ut, Ge \== Fl, Ge \== Nb, Ge \== Lb, Ge \== Zh,
    Fl \== Nh, Fl \== Ut,
    Nb \== Zh, Nb \== Zl, Nb \== Lb,
    Ut \== Nh, Ut \== Zh,
    Nh \== Zh,
    Zh \== Zl,
    kleur(Gr), kleur(Fr), kleur(Dr), kleur(Ov),
    kleur(Ge), kleur(Fl), kleur(Nb), kleur(Ut),
    kleur(Nh), kleur(Zh), kleur(Lb), kleur(Zl).

Hoeveel kleuren zijn er eigenlijk minimaal nodig om deze kaart te kleuren? Hoeveel verschillende oplossingen zijn er voor het minimale aantal kleuren?
Uitwerkingen

Natuurlijke getallen

Ga uit van het volgende programma:

% natural_number(N) :- N is een natuurlijk getal.
natural_number(0).
natural_number(s(X)) :- natural_number(X).

% plus(X,Y,Z) :-
%     Z is de som van de natuurlijke getallen X en Y.
plus(0,X,X) :- natural_number(X).
plus(s(X),Y,s(Z)) :- plus(X,Y,Z).

% times(X,Y,Z) :-
%     Z is het produkt van de natuurlijke getallen X en Y.
times(0,X,0) :- natural_number(X).
times(s(X),Y,Z) :- times(X,Y,XY), plus(XY,Y,Z).

f(0,s(0)).
f(s(N),F) :- f(N,F1), times(s(N),F1,F).

% leq(X,Y) :-
%     X en Y zijn natuurlijke getallen
%     met X kleiner dan of gelijk aan Y.
leq(0,X)       :- natural_number(X).
leq(s(X),s(Y)) :- leq(X,Y).

Wat is de betekenis van f(N,X)?
Definieer 'kleiner-dan' en 'groter-dan-of-gelijk-aan' op twee manieren: (i) analoog aan leq; (ii) met gebruikmaking van leq.
Definieer even/1 en odd/1.
Definieer het predikaat fib/2 voor de Fibonacci-functie.
Definieer het predikaat div/3 voor de div-functie (gehele deling van natuurlijke getallen). Aanwijzing: X div Y is gelijk aan het aantal keren dat je Y van X kunt aftrekken met niet-negatief resultaat.
Herschrijf plus, times,leq voor positieve natuurlijke getallen, gedefinieerd door

% pos_nat(N) :- N is een positief natuurlijk getal.
pos_nat(1).
pos_nat(X+1) :- pos_nat(X).


Mbv. het predikaat conv/2 , gedefinieerd door

% conv(T,N) :- conversie tussen termrepresentatie T en
%              standaardrepresentatie N van een getal.
conv(0,0).
conv(s(X),Y) :- conv(X,Z), Y is Z+1.    % is/2 wordt later uitgelegd

is het mogelijk om van die vermoeiende s(s(s(...-notatie af te komen. Definieer nu, met gebruikmaking van plus,times,fib , predikaten cplus/3, ctimes/3, cfib/2 die zich ongeveer zo gedragen als plus, times, fib maar dan met de gebruikelijke getalnotatie.
Definieer predikaten notprime/1, prime/1 met de voor de hand liggende betekenis.
NB: prime is lastig. Gebruik de Stelling van Wilson: p is priem dan en slechts dan als p een deler is van (p-1)! + 1. Dit is natuurlijk geen efficiente test voor groter waarden van p, maar voor kleine waarden (bv. p = 7) valt het rekenwerk wel mee. Hij wordt overigens efficienter als je systematisch modulo p rekent.
Uitwerkingen

Lijsten

Ga uit van het volgende programma:

% list(X) :- X is een lijst.
list([]).
list([X|Xs]) :- list(Xs).

% member(X,Ys) :- X is een element in lijst Ys.
member(X,[X|Xs]).
member(X,[Y|Ys]) :- member(X,Ys).

% prefix(Xs,Ys) :- Xs is een prefix (beginstuk) van lijst Ys.
prefix([],Ys).
prefix([X|Xs],[X|Ys]) :- prefix(Xs,Ys).

% suffix(Xs,Ys) :- Xs is een suffix (eindstuk) van lijst Ys.
suffix(Xs,Xs).
suffix(Xs,[Y|Ys]) :- suffix(Xs,Ys).

% sublist(Xs,Ys) :- Xs is een aaneengesloten deellijst van Ys.
sublist(Xs,Ys) :- prefix(Xs,Ys).
sublist(Xs,[Y|Ys]) :- sublist(Xs,Ys).

sublist2([],Ys).
sublist2(Xs,[Y|Ys])     :- sublist2(Xs,Ys).
sublist2([X|Xs],[X|Ys]) :- sublist2(Xs,Ys).

% append(X,Ys,Zs) :- Zs ontstaat uit lijst Ys
%     door toevoeging van X aan het begin van Ys.
append([],Ys,Ys).
append([X|Xs],Ys,[X|Zs]) :-
	append(Xs,Ys,Zs).

Hebben sublist en sublist2 dezelfde betekenis?
Definieer adjacent/3, last/2, double/2 met de volgende betekenis:

% adjacent(X,Y,Zs) :- X en Y komen naast elkaar in lijst Zs voor.

% last(X,Zs) :- X is het laatste element van lijst Zs.

% double(Xs,Ys) :- Ys ontstaat uit lijst Xs door elk element te herhalen,
%     dus bv. double([1,2,3,3],[1,1,2,2,3,3,3,3]).

Maak hierbij geen gebruik van append.
Definieer sum/2 met de volgende betekenis:

% sum(N,Xs) :- N is de som van de elementen van de
%     lijst van natuurlijke getallen Xs.

Doe dit op twee manieren: (i) mbv. plus/3; (ii) zonder hulppredikaat, met slechts drie axioma's.
Definieer predikaten met de volgende betekenis:

% delete(L,X,LminX) :- LminX ontstaat uit lijst L door
%     alle voorkomens van X te verwijderen.

% substitute(X,Y,LmetYipvX) :- LmetYipvX ontstaat uit lijst L
%     door alle voorkomens van X door Y te vervangen.

% no_doubles(Xs,Ys) :- Ys ontstaat uit Xs
%	door van elk element met n+1 voorkomens
%	n voorkomens weg te laten.

% swap(XaYbZ,XbYaZ) :- lijst XaYbZ ontstaat
%	uit lijst XbYaZ door verwisseling
%	van twee elementen.

% even_permutation(L,M) :- M is een permutatie
%	van L die het resultaat is van een even
%	aantal verwisselingen.
% odd_permutation(L,M) :- M is een permutatie
%	van L die het resultaat is van een oneven
%	aantal verwisselingen.

%  merge(Xs,Ys,Ms) :- Ms is de gesorteerde
%	samenvoeging van de gesorteerde
%	lijsten Xs en Ys.

Definieer mergesort/2 met gebruikmaking van merge, op twee manieren: (i) nondeterministisch, met gebruikmaking van append; (ii) deterministisch, met gebruikmaking van split/3, gedefineerd door

% split(Xs,As,Bs) :- As bevat de elementen
%	van Xs die op oneven posities staan,
%	Bs de elementen van Xs op even posities.


Uitwerkingen

Binaire bomen

We gaan verder met binaire bomen, gedefinieerd door

% binary_tree(B) :- B is een gelabelde binaire boom,
%     opgebouwd mbv. de functor tree/3:
%     tree(X,L,R) denoteert en boom met label X,
%     linker subboom L en rechter subboom R.
binary_tree(void).
binary_tree(tree(X,L,R)) :-
	binary_tree(L),
	binary_tree(R).

Definieer nu de volgende predikaten:

% subtree(S,T) :- S en T zijn binaire bomen
%	en S is een deelboom van T, dwz. de
%	wortel van S is een knoop van T.

% sum_tree(T,S) :- T is een binaire boom met natuurlijke getallen als labels,
%     en S is de som van de getallen in de knopen van T.

% path(X,T,L) :- lijst L is het pad dat loopt van de wortel
%     van binaire boom T naar de knoop met label X.


Uitwerkingen

Bags

Een bag (zak) is een verzameling waarin elk element een natuurlijk getal als multipliciteit heeft. In deze opgave representeren we bags als lijsten met multipliciteiten, mbv. de functoren void/0 voor de lege bag, en bag/3 voor het maken van de niet-lege bag bag(Element,Multipliciteit,Restbag). Zo representeert bv. bag(a,3,bag(b,2,void)) de bag met drie kopieen van a en twee kopieen van b.
Defineer nu de vereniging van twee bags:

%% union(B,C,D) :- bag D ontstaat uit bag B
%%	en bag C door voor alle elementen als
%%	multipliciteit de som van de
%%	multipliciteiten in B en C te nemen.

Aanwijziging: gebruik het hulppredikaat

%% elem_union(X,N,B,C) :- bag C ontstaat
%%	uit bag B door de multipliciteit
%%	van X met N te verhogen.

Definieer op analoge wijze de doorsnede van twee bags:

%% intersect(B,C,D) :- bag D is de doorsnede
%%	van bag B en bag C.

Definieer predikaten die een lijst resp. een binaire boom in een bag omzetten.
Definieer:

%% goodbag(B) :- B is een nette bag-structuur,
%%	waarin elk element hoogstens een keer
%%	voorkomt, met multipliciteit >= 1.

Aanwijzing: gebruik het predikaat

%% not_in(X,B) :- X is not in bag B.

Definieer vervolgens:

%% norm(B,Bn) :- Bn is een nette variant van bag B,
%%	ontstaan door verwijderen van elementen met
%%	multipliciteit 0 en wegwerken van meervoudige
%%	voorkomens van elementen.


Uitwerkingen

Redundantie vermijden

Verzamelingen worden (in Prolog en elders) vaak door lijsten gerepresenteerd. Een inherent nadeel van deze representatie is zijn meerduidig karakter: een en dezeflde verzameling kan door een grote verscheidenheid aan lijsten gerepresenteerd woorden, bv. {1,2,3} door [1,2,3], [3,2,1], [1,1,1,2,3], enzovoort. Dit kan bij het programmeren in Prolog redundante oplossingen opleveren: oplossingen die als verzameling gelijk zijn maar als verzameling verschillend. Dit wordt geillustreerd in de volgende opgave.
Gegeven een lijst met n kinderen [k1,...,kn] en een lijst met n speelgoedjes [s1,...,sn], definieer de relatie verdeel/3 zodat eerste argument een lijst kinderen is, tweede argument een lijst speelgoedjes, en derde argument een lijst termen speelt_met(ki,sj) zodat kind ki met speelgoed sj speelt. Elk kind speelt natuurlijk met een ander speelgoedje, anders krijgen we ruzie. Voorbeeld:

?- verdeel([jan,anna,kees],[tol,pop,auto],X).

X = [speelt_met(jan,tol), speelt_met(anna,auto), speelt_met(kees,pop)]);

X = [speelt_met(jan,auto), speelt_met(anna,tol), speelt_met(kees,pop)]);

etc...

Een naieve aanpak zal redundante oplossingen opleveren: als lijst verschillend, maar als verzameling gelijk. Een methode om dit te voorkomen is: ga uit van de structuur (in dit geval de volgorde) van de representatie van de input, en hanteer die bij het samenstellen van de oplossingen. In dit geval: ga uit van de volgorde van de lijst van kinderen, en zorg ervoor dat de lijst termen van de vorm speelt_met(ki,sj) in die volgorde komt te staan. Het kan daarbij handig zijn gebruik te maken van het volgende hulppredikaat

% select(X,L,LzonderX) :- LzonderX ontstaat uit lijst L door een voorkomen van
 X weg te nemen.


Uitwerkingen

Aritmetiek in Prolog

In Prolog is - ter wille van de efficientie - een aantal aritmetische functies en predikaten ingebouwd. Om daarmee gemakkelijk nieuwe predikaten te programmeren is er het systeempredikaat is/2 . Het effect van

	Val is expr

is dat de aritmetische expressie expr uitgerekend wordt (dat betekent oa. dat alle variabelen een aritmetische waarde moeten hebben) en dat het resultaat geunificeerd wordt met Val. Met behulp hiervan kan de faculteitsfunctie als volgt gedefinieed worden:

% factorial(N,F) :- F equals N!.
factorial(0,1).
factorial(N,F) :- N > 0,
		  N1 is N-1,
		  factorial(N1,F1),
		  F is N*F1.

Definieer nu soortgelijke programma's voor

% triangle(N,T) :- T is het N-de driehoeksgetal, dwz.
%     de som van de getallen 1 t/m N.

% power(X,N,P) :- P is X tot de macht N
%     (N een natuurlijk getal).

Ter wille van de efficientie kan het handig zijn om recursie te vervangen door iteratie mbv. een accumulator. Voor de faculteitsfunctie ziet dat er als volgt uit:

% iterfac(N,F) :- F equals N!, berekend mbv. iteratie.
iterfac(N,F) :- fac(N,1,F).

% fac(N,M,F) :- F equals N! * M.
fac(0,M,M).
fac(N,M,F) :- N > 0,
	      M1 is M*N,
	      N1 is N-1,
	      fac(N1,M1,F).

Definieer nu soortgelijke programma's voor triangle, power.
Uitwerkingen

Cuts

De cut (notatie ! ) is een systeempredikaat dat gebruikt kan worden om het zoeken te versnellen door overbodige takken uit de zoekboom weg te snoeien. Het effect van uitvoering van de cut in een programma is: fixeren (dwz. ontoegankelijk maken voor backtracking) van alle keuzen die gemaakt zijn sedert de unificatie van de 'parent goal' met de head van de clause waar de betreffende cut in voorkomt. Zie verder 5.1 in het boek.

Voeg cuts toe in de volgende programma's:


% partition(Xs,Y,Littles,Bigs) :- (Littles,Bigs) is een partitie
%     van Xs, alle elementen van Littles zijn kleiner dan of gelijk
%     aan Y, alle elementen van Bigs zijn groter dan Y.
partition([X|Xs],Y,[X|L],B) :-
	X =< Y,
	partition(Xs,Y,L,B).
partition([X|Xs],Y,L,[X|B]) :-
	X > Y,
	partition(Xs,Y,L,B).
partition([],Y,[],[]).

% inssort(Xs,Ys) :- Ys is een geordende permutatie van Xs,
%     verkregen dmv. insertion sort.
inssort([X|Xs],Ys) :-
	inssort(Xs,Zs),
	insert(X,Zs,Ys).
	inssort([],[]).

% insert(X,Ys,Zs) :- de gesorteerde lijst Zs ontstaat uit
%     de gesorteerde lijst Ys door X in te voegen.
insert(X,[],[X]).
insert(X,[Y|Ys],[Y|Zs]) :-
	X > Y,
	insert(X,Ys,Zs).
insert(X,[Y|Ys],[X,Y|Ys]) :-
	X =< Y.

Wat is er mis met het volgede programma?

% minimum(X,Y,Z) :- Z is het minimum van X en Y.
minimum(X,Y,X) :- X =< Y,
		  !.
minimum(X,Y,Y).

Aanwijzing: wat gebeurt er met bv. minimum(1,2,2)?.
Ga na dat het volgende programma wel correct is:

% minimum(X,Y,Z) :- Z is het minimum van X en Y.
minimum(X,Y,Z) :- X =< Y,
		  !,
		  Z = X.


Uitwerkingen

Programma's combineren

Soms is het handig een aantal bewerkingen in een programma te stoppen, bv.:

% union_intersect(Xs,Ys,Us,Is) :- Us is de vereniging en
%     Is is de doorsnede van Xs en Ys.
union_intersect([],Ys,Ys,[]).
union_intersect([X|Xs],Ys,Us,[X|Is]) :-
		member(X,Ys),
		union_intersect(Xs,Ys,Us,Is).
union_intersect([X|Xs],Ys,[X|Us],Is) :-
		not member(X,Ys),
		union_intersect(Xs,Ys,Us,Is).

Breid dit programma uit met het verschil van Xs en Ys.
Nu een (op het eerste gezicht) onmogelijke opgave over binaire bomen. Ter herinnering de definitie:

% binary_tree(B) :- B is een gelabelde binaire boom,
%     opgebouwd mbv. de functor tree/3:
%     tree(X,L,R) denoteert en boom met label X,
%     linker subboom L en rechter subboom R.
binary_tree(void).
binary_tree(tree(X,L,R)) :-
	binary_tree(L),
	binary_tree(R).

De opdracht luidt nu: schrijf een programma dat in 1 pass het volgende oplevert:

% maxval(T,TM) :- TM ontstaat uit binaire boom T (met natuurlijke getallen als
 labels)
%     door alle labels te vervangen door de maximale in T voorkomende
 labelwaarde.

In twee passes is dit niet moeilijk, maar in een (1) pass? Het te ontwikkelen programma moet, gegeven een binaire boom, twee dingen doen:(i) bepalen van Max, het maximum van de labels van de boom; (ii) vervangen van alle labels door Max. Dit kan iets algemener worden geformuleerd: (i) bepalen van Max; (ii) alle labels vervangen door een vaste waarde Val. Dit leidt tot twee programma's tmax/2, tval/3 met dezelfde structuur:

% tmax(T,M) :- M is de maximale waarde van labels in binaire boom T.

% tval(T,X,TX) :- TX ontstaat uit binaire boom T door alle labels
%     door X te vervangen.

Vervolgens kunnen tmax en tval in elkaar geschoven worden tot

% tmaxval(T,M,X,TX) :- M is de maximale waarde van labels in binaire boom T,
%     en TX ontstaat uit T door alle labels door X te vervangen.

Ten slotte kunnen we maxval verkrijgen door, in de aanroep van tmaxval, op de posities 2 en 3 dezelfde variabele in te vullen.
Uitwerkingen

Generate and Test

"Generate-and-test" is een tweestapsaanpak om oplossingen voor een probleem te vinden. Het idee is: genereer eerst kandidaat-oplossingen, en test vervolgens de kandidaatoplossingen om te zien of ze inderdaad een oplossing zijn. Als logisch programma ziet dat er als volgt uit:

oplossing(X) :- genereer(X), test(X).

Het volgende programma kan gebruikt worden om de gehele getallen in een interval te genereren:

% between(I,J,K) :- I,J,K zijn gehele getallen met I =< J =< K.
between(I,I,K) :- I =< K.
between(I,J,K) :- I < J,
		  I1 is I + 1,
		  between(I1,J,K).

Gebruik dit programma om, met de generate-and-test methode, de geheeltallige wortel van een getal te bepalen:

% wortel(N,W) :- W is een geheel getal met W^2 =< N < (W + 1)^2
%		 dus een benadering van de wortel uit N

Uitwerkingen

Gerard Renardel