151 lines
5.5 KiB
Markdown
151 lines
5.5 KiB
Markdown
---
|
|
title: "assignment 5 - prolog"
|
|
date: 2025-11-05
|
|
author: fredrik robertsen
|
|
margin:
|
|
x: 3.5cm
|
|
y: 4cm
|
|
fontsize: 14pt
|
|
mainfont: libertinus serif
|
|
lineheight: 1.5
|
|
pagesize: a4
|
|
---
|
|
|
|
## task 1
|
|
|
|
the following code is my solution:
|
|
|
|
```prolog
|
|
payment(Sum, Coins) :-
|
|
payment_acc(0, Sum, Coins).
|
|
|
|
payment_acc(Acc, Sum, []) :-
|
|
Acc #= Sum.
|
|
|
|
payment_acc(Acc, Sum, [coin(Count, Value, Available)|Tail]) :-
|
|
Count in 0..Available,
|
|
NewAcc #= Acc + Count * Value,
|
|
payment_acc(NewAcc, Sum, Tail).
|
|
```
|
|
|
|
the predicate `payment/2` has two arguments (hence `/2`). the first, `Sum`, is
|
|
a target cost we are attempting to make from a collection of `Coins`, a list of
|
|
`coin/3` items, specifying the value of the coin type and how many of such coins
|
|
we have available.
|
|
|
|
this is a classic constraint satisfaction problem which is solved elegantly in
|
|
prolog, even with my messy first-timer code above.
|
|
|
|
we are essentially making a search in the state graph of our problem space. this
|
|
is what prolog does when it performs a combination of inference/deduction, DFS
|
|
and backtracking to find values that fit our variables, given the stated
|
|
predicates.
|
|
|
|
in this program we create a helper function which `acc`umulates a sum accross
|
|
the recursion. this is similar to how we would solve it in oz, except we would
|
|
need to implement the search explicitly. prolog does a lot of heavy lifting in
|
|
that regard, since we are essentially only recursively creating a long string of
|
|
CNF logic statements that constrain our problem until a solution can be found.
|
|
|
|
note that i am also using patternmatching to destructure the arguments of the
|
|
`payment_acc` predicate.
|
|
|
|
## task 2
|
|
|
|
### subtask 1
|
|
|
|
this is my solution code:
|
|
|
|
```prolog
|
|
% arity 4 predicate
|
|
plan(Cabin1, Cabin2, Path, TotalDistance) :-
|
|
plan(Cabin1, Cabin2, [Cabin1], Path, TotalDistance).
|
|
|
|
% base case
|
|
plan(Cabin1, Cabin2, Visited, Path, Distance) :-
|
|
not(Cabin1 = Cabin2),
|
|
distance(Cabin1, Cabin2, Distance, 1),
|
|
append(Visited, [Cabin2], Path).
|
|
|
|
% recursive case
|
|
plan(Cabin1, Cabin2, Visited, Path, TotalDistance) :-
|
|
not(Cabin1 = Cabin2),
|
|
distance(Cabin1, CabinX, Distance, 1),
|
|
\+ member(CabinX, Visited),
|
|
append(Visited, [CabinX], NewVisited),
|
|
plan(CabinX, Cabin2, NewVisited, Path, SubDistance),
|
|
TotalDistance is Distance + SubDistance.
|
|
```
|
|
|
|
as you can see, i learned that you can overload predicates with differing
|
|
arities, such that the previous `payment_acc` could've only been named
|
|
`payment`. we can also do multiple definitions to clearly state the different
|
|
branches of a recursive algorithm, such as the base case and the recursive step.
|
|
|
|
we can then use a `plan/5` auxiliary function to carry a log of what cabins we
|
|
have already `Visited`. thus, our base case becomes the case where we have
|
|
a direct connection between the first and last cabin, such that we can easily
|
|
read the distance from the predicate. then just make sure to mark `Cabin2` as
|
|
visited.
|
|
|
|
in the recursive step we assume there is a `CabinX` that lies between our start
|
|
and end cabins. this cabin cannot have been visited previously, lest we enter an
|
|
infinite cycle -- `\+ member(CabinX, Visited)`. we can then visit `CabinX` and
|
|
continue our search recursively from there -- `plan(CabinX, Cabin2, ...)`.
|
|
lastly, we can calculate the `TotalDistance` as the sum of the total distance from `CabinX`
|
|
to `Cabin2` and the total distance from `Cabin1` to `CabinX`.
|
|
|
|
```
|
|
Cabin1 --> CabinX --> Cabin2
|
|
L Distance J L SubDistance J
|
|
L TotalDistance J
|
|
```
|
|
|
|
### subtask 2
|
|
|
|
my initial solution was:
|
|
|
|
```prolog
|
|
bestplan(Cabin1, Cabin2, ShortestPath, ShortestDistance) :-
|
|
findall([Path, Distance], (plan(Cabin1, Cabin2, Path, Distance)), Solutions),
|
|
shortestpath(Solutions, [ShortestPath, ShortestDistance]).
|
|
|
|
% takes a list of [Path, Distance] pairs
|
|
shortestpath([[Path, Distance]|[]], Solution) :- Solution = [Path, Distance].
|
|
shortestpath([[Path, Distance]|Tail], [ShortestPath, ShortestDistance]) :-
|
|
shortestpath(Tail, [TailPath, TailDistance]),
|
|
(Distance < TailDistance ->
|
|
(ShortestPath = Path, ShortestDistance = Distance)
|
|
;
|
|
(ShortestPath = TailPath, ShortestDistance = TailDistance)
|
|
).
|
|
```
|
|
|
|
but i cleaned it up to this:
|
|
|
|
```prolog
|
|
bestplan(Cabin1, Cabin2, ShortestPath, ShortestDistance) :-
|
|
findall([Path, Distance], (plan(Cabin1, Cabin2, Path, Distance)), Solutions),
|
|
sort(2, @=<, Solutions, [ShortestSolution|_]),
|
|
[ShortestPath, ShortestDistance] = ShortestSolution.
|
|
```
|
|
|
|
the main idea is a bit naive: we are just picking out the shortest path from
|
|
_all_ the paths we find using the `plan` predicate from the last subtask. it
|
|
sounds like it wouldn't be particularly performant, but i think prolog does
|
|
a lot of work such that it isn't too terrible.
|
|
|
|
anyway, i initially just expanded all the paths found by `plan`, then used
|
|
a homemade `shortestpath/2` predicate that works on a list of `[Path, Distance]`
|
|
pairs to tail recurse and find the shortest such pair, keeping a running
|
|
shortest distance result. this is similar to oz, taking the head of the list,
|
|
checking if it is smaller than the current accumulated value, then carry on the
|
|
smaller value of the two. the base case is when we only have a single path and
|
|
distance: we return that as a solution.
|
|
|
|
but this entire process can be shortened to simply sorting the solutions based
|
|
on the distance and then picking the first one of that list. the code above
|
|
performs such a sort and only takes the first solution in the sorted list,
|
|
sorting from low to high based on the key `2`, such that we are sorting the
|
|
distance.
|