UNSW Programming Competition 1997
Preliminary Round

Solution to Task A: Quiz Generator


Judges' Report

One of the simplest input transformations is the selective substitution of single characters by other characters. If the replacement is independent of what occurred previously in the input, then the solution requires no variables other than the current input character (or line and current position for line-based programming systems).

Most teams recognised the simplicity of the basic task, and produced a correct answer. A common practice that detracted from the readability of many solutions was the use of ASCII codes for the digit characters '0' to '9'. For many students

If ASC(ch$) <= 57 AND ASC(ch$) >= 48 Then ...
seems to make more sense than
If (ch$ >= "0") And (ch$ <= "9") Then...
In fact, built-in operations in some languages can make the test even more human-friendly:
If IsNumeric(ch$) Then ...    ' BASIC

if ch in ['0'..'9'] then ...  { Pascal }

if (isdigit(ch)) ...          /* C */

The extensions required some input history to be retained, and this was generally well recognised by teams that made a serious attempt at the extra points. The ability for strings to be easily appended in BASIC made solutions to the second extension in this language a little easier than for Pascal or C.


Problem Analysis

The basic task conforms to the "stateless filtering" model:
for each input character c
    if c is special
        display its replacement
    else
        display c
For the first extension, it's necessary to keep track of the number of digit sequences encountered (to generate the correct letter), and to determine when the current character is the start of a digit sequence, to interpolate the letter in brackets.

The second extension incorporates an array of strings to retain the original text. Depending on the string processing supported by the language used, the end of the digit sequence may also need to be identified to terminate the string appropriately.

Algorithms

Algorithm 1 (basic task):

for each input character c
    if c is a digit
        display an underscore character ("_")
    else
        display c
An equivalent algorithm replaces c by "_" and then displays c.

Ends of lines are assumed to be echoed, that is, when the end of an input line is encountered the output line is terminated as well.

The first extension requires the program to keep count of the number of sequences, and to be able to tell when c is the start of a sequence. The new lines are in blue.

Algorithm 2 (first extension):

set counter n to 0
for each input character c
    if c is a digit
        if previous character is not a digit
            increment n
            display corresponding letter in brackets
        display "_"
    else
        display c
The best way to determine what the previous character was is to save the value of c before advancing it the the next character. It is set to a non-digit at the start of each line. Alternatively, if complete lines are stored the previous character can be picked out of the line.

Displaying the lower-case letter corresponding to a small integer n is possible for all the common programming notations. Say n = 1 corresponds to the letter a, 2 to b and so on, then the mapping is

Language Mapping
BASIC CHR$(n-1 + ASC("a"))
Pascal chr(n-1 + ord('a'))
C n-1 + 'a'
Java (char) (n-1 + 'a')
Note: it should never be necessary to know that the letter a has an ASCII code of 97. The above examples exploit the ability for the computer to determine this out without people having to remember magic numbers.

The second extension requires some thought. As well as keeping track of the number of digit sequences, the original sequences themselves need to be remembered too. The counter n is an obvious index to use into an array of strings, one for each "answer":

Algorithm 3 (second extension):

set counter n to 0
for each input character c
    if c is a digit
        if previous character is not a digit
            increment n
            display corresponding letter in brackets
        display "_"
        append c to the n-th string
    else
        display c

display "Answers:"
for each value i from 1 to n
    display letter corresponding to i in brackets, then answer i
The trickiest part can be the step append c to the n-th string, since not all languages support string concatenation (joining operations) easily. You should investigate how this is done in your favourite notation.

Solutions (enciphered)

Download details here (all tasks).

Solution to Task B: Word Count


Judges' Report

This task is easy to describe but tricky to solve correctly. Many teams produced nearly-correct solutions, but with one or more defects of the kind discussed below. The most common approach was to count spaces rather than words, leading to inaccurate counts for the number of words. The line counts were generally correct, since it is much easier to identify the occurrence of a line.


Problem Analysis

The key to successfully completing the task is to devise a scheme to identify each word accurately, regardless of whether it's the first, last or any other word on a line.

The problem normally falls into the character-stream type, where the input history only needs to comprise

However, solutions can also take the alternative approach of reading and processing input a line at a time. This makes counting lines very easy, but introduces an extra loop (an outer loop to read a line into a string and an inner one to extract characters from the string). The same algorithms (both correct and incorrect) were evident in both line-based and character-based solutions.

Algorithms

We'll start with a common, naïve and incorrect solution. Words are separated by spaces, which might be a little easier to identify than the words themselves. Hence,

Algorithm 1:

set counters to zero
for each input character c
    if end of line has been reached
        add 1 to line counter
    else if c is a space
        add 1 to word counter
Algorithm 1 has two serious errors:
  1. If words are separated by several spaces, all of them are counted so the word count is too high; and
  2. If words are separated by exactly one space and there are no spaces at the beginning or ends of lines, the word count is too low. For example, if there are five words on a line there could be only four spaces between them.

A common attempt at overcoming the second difficulty is to add one to the word count for each line read. The first problem is fixed by counting spaces only if the previous character wasn't also a space:

Algorithm 2:

set counters to zero
set prev to space
for each input character c
    if end of line has been reached
        add 1 to line counter
        add 1 to word counter
        set c to space   // then set prev below
    else if c is a space and prev isn't a space
        add 1 to word counter
    set prev to c
Note the introduction of a new variable prev providing some more context. It's usually the previous character to c, but of course must be set specially at the beginning of a line, where there is no previous character.

Algorithm 2 almost works. With "normal" text, whether close-spaced or spread out, it does seem to count words properly. For example, in processing the following input line the word counter is incremented only at the points indicated, where there's a space preceded by a non-space. After adding one for the end of the line, the correct count (3) is obtained.

    pack     my  box
        ^      ^

Have you spotted the flaw in Algorithm 2? Think about it for a little while before checking.

What's wrong with Algorithm 2?

There are two situations where the wrong answer will be obtained:
  1. when there are spaces at the end of a line; and
  2. when a line contains no words at all (whether or not it contains spaces).
In the first instance the space following the last word is counted, so we would get 3 rather than 2 for the above example, and then 4 when the end of line is reached:
    pack     my  box
        ^      ^    ^

In the second case the end of line increment is again inappropriate.

Both cases expose the logical error: adding 1 to the word counter at end of line is wrong. It was a bad way to try to account for the last (or first) word on a line.

In general, having to make adjustments of this kind to an algorithm often implies that there is something fundamentally wrong with the original strategy.

Let's reconsider how to identify a word reliably.

Rather than concentrate on spaces following a word, consider the start of a word. Each word is preceded either by a space, or by the beginning of a line. In Algorithm 2 prev is already to set to a space at the beginning of a line, which is exactly what we need for the algorithm to be able to handle the first word in exactly the same way as the other words.

The only changes to Algorithm 2 needed to correct it are to remove the offending increment and to reverse the space/non-space comparison:

Algorithm 3:

set counters to zero
set prev to space
for each input character c
    if end of line has been reached
        add 1 to line counter
        // don't touch word counter
        set c to space
    else if c is not a space and prev is a space
        add 1 to word counter
    set prev to c
How does Algorithm 3 differ from Algorithm 2? As soon as the first character of a word is detected, the word counter is incremented. It's not necessary to wait for the appearance of a (potentially ambiguous) character following a word. If there are no words (even with lots of spaces), nothing happens. For the above example, the increment points are now at the appropriate places:
    pack     my  box
    ^        ^   ^

Solutions (enciphered)

Download details here (all tasks).

Solution to Task C: Date Conversion


Judges' Report

This task, conversion from the Julian date format (year/day-of-year) to the usual day month year format, can be solved using either

  1. many conditional statements with a large number of literal constants, or
  2. an iterative scheme than minimises both the amount of typing and the number of constants.
Most teams only considered the first kind. They relied on one of the following direct approaches, which invariably result in long, difficult to read and error-prone solutions:

  1. Use twelve or thirteen conditionals (if-then or if-then-else constructs), having previously worked out the number of days to the end of each month (31, 31+28 = 59, 31+28+31 = 90, etc).

  2. Do the same, but get the computer to do the sums. Unfortunately, this generates a large number of terms representing the number of days in each month. If the sums appear as above, there will be no fewer than 12*13/2 = 78 occurrences of the constants from 28 to 31, which makes for a lot of duplicate typing.

  3. Use a range of constants in a Pascal case or BASIC Select statement. This requires 12*2 = 24 precalculated constants, but is relatively compact and readable.
Once the correct condition is selected, it's necessary to subtract another constant to determine what day of the month it is. Again it's either precalculated or requires a messy expression to evaluate.

Besides the minority of submissions that employed the method described in the next section, one solution was particularly notable. It used the built-in date manipulation functions of Visual Basic to do almost the entire conversion. Knowing when not to re-invent the wheel is the mark of a good designer.

Leap-year checking was avoided by many teams, not so much because the test itself is difficult to code, but rather that it doesn't fit at all well with the collection of constants generally required by the main algorithm. A poorly-written program may even need two sets of numbers to handle leap years and non-leap years.


Problem Analysis

Let's reject the 13-way selection method as being too boring and repetitive.

Assume the program has available to it the number of days in each month of the specified year (so the number of days in February can be set to 28 or 29 according to whether or not it's a leap year). Why not get the computer to determine the number of days up to the end of each month, not with a long literal addition but with a loop processing one month at a time? Once this progressive total passes the "target" day, you know which month must be represented.

The process very much like the following measurement problem:

I'm given a staircase and a short ruler. The heights of the steps are slightly different, but I can measure each of them individually with the ruler. If a frog can jump to a certain height h, which step would it land on?

Algorithms

Besides an array to hold the number of days in each month, we need two integer variables:

Variable Purpose
m the month currently being checked (like the step number)
total the number of days to the end of month preceding m

Let d be the day number accepted from input (assumed for the present to be a valid day). Using the notation A[i] to mean the i-th element of the array A,

Algorithm 1 (basic task):

for each month m
    record the number of days in m in daysin[m]

set total to 0
set m to 1
while d > total + daysin[m]
    add daysin[m] to total
    add 1 to m
When the loop terminates, m is the correct month number, since we know that the expression
d > total + daysin[m]
is false (because the loop stopped), hence
d <= total + daysin[m]
but it's also true that
d > total
(why?)

From the definition of total, the day of the current month is just d-total.

* * *

Algorithm 1 doesn't check if d is really a valid day, and it doesn't handle leap years. Illegal day values are either 0 or negative (we can check d directly), or beyond 31 December of the specified year. Algorithm 1 will probably terminate with a run-time error if d is too large, when an attempt is made to inspect the non-existent daysin[13]. The algorithm should be modified so that this situation is detected and the loop stops:

Algorithm 2 (basic task with error detection):

read y and d
for each month m
    record the number of days in m in daysin[m]

set total to 0
set m to 1
while m < 12 and d > total + daysin[m]
    add daysin[m] to total
    add 1 to m

if d <= 0 or d > total + daysin[m]
    display "Date out of range"
Some languages (such as C and Java) provide conditional Boolean expressions. These permit expressions of the form A && B (meaning A and B) where B is not evaluated at all if A is false, since the entire expression must be false regardless of the value of B. A is often specified to "guard" array indexing in B, which may otherwise be out of range. Conditional expressions allow the loop and termination test to be slightly simplified (although this is largely a matter of opinion):

Algorithm 2a (alternative loop formulation):

read yr and d
for each month m
    record the number of days in m in daysin[m]

set total to 0
set m to 1
while m <= 12 && d > total + daysin[m]
    add daysin[m] to total
    add 1 to m

if d <= 0 or m >12
    display "Date out of range"

The simple leap year test is whether the year number is divisible by 4. The full test could have two parts, according to whether the year is also a century year (ends in 00):

   (year mod 100 = 0 and year mod 400 = 0)
or (year mod 100 <> 0 and year mod 4 = 0)
where a mod b is the remainder when a is divided by b. A more compact form is
year mod 400 = 0 or (year mod 4 = 0 and year mod 100 <> 0)
which means "either a quad-century or a quad-year unless also a century", or even
year mod 4 = 0 and (year mod 400 = 0 or year mod 100 <> 0)
which means "a quad-year and either a quad-century or not a century". Use whichever seems most natural.

The final algorithm uses the leap year calculation at only one point, to adjust the number of days in February:

Algorithm 3 (extension):

read yr and d
for each month m
    record the number of days in m in daysin[m]
if yr is a leap-year
    add 1 to daysin[2]

set total to 0
set m to 1
while m < 12 and d > total + daysin[m]
    add daysin[m] to total
    add 1 to m

if d <= 0 or d > total + daysin[m]
    display "Date out of range"
else
    display d-total
    display name of m-th month
    display yr
The solution as finally coded should call a procedure or function containing the above algorithm within a reading loop that looks for the 0000/0 terminator.

Solutions (enciphered)

Download details here (all tasks).

Solution to Task D: Phrase Matching


Judges' Report

Although not attempted by very many teams, those who did have a go generally managed to produce a workable solution. Most answers followed either of the two schemes described in the next section, and BASIC is probably the easiest notation to use because of its string manipulation capabilities.

The extension is really a separate problem, and was completed by checking the current input string against all previous strings saved in an array.


Problem Analysis

There are two logical approaches to solving this problem, one based on eliminating matched letters in the key phrase, and the other on frequency counts.

The elimination method is a natural analogue of a pencil-and-paper approach where each letter of the test phrase is crossed off the key phrase. If a match can't be found for any letter, the process can stop and "NO" displayed. If all letters match, the answer is "YES". Implementation consideration and efficiency issues are discussed in the next section.

The frequency method is more suited to computer solution because of the ease with which the number of occurrences of each possible symbol can be recorded (compared to the inconvenience of drawing up a table on paper). Since the order of occurrence of the letters is irrelevant, if, for example, the letter e occurs 2 times in the key phrase but 3 times in the test phrase, the test phrase will fail to match. The problem reduces to

  1. counting the number of times each letter occurs in a string (either the key or test phrase), storing the counts in a 26-element integer array; and

  2. comparing the corresponding elements of the tally arrays.

Algorithms

Algorithm 1 (search and elimination):

read key phrase
for each test phrase
    copy key phrase to ref
    for each letter c in test phrase
        for each character r in ref
            if c = r
                remove r from ref  // or replace by space
                exit loop
        if search failed
            exit loop

    if search failed
        display "NO"
    else
        display "YES"
Although this is a direct method it is far from easy to code neatly. Each time a character is found the inner search has to stop, disrupting the predictable flow of the algorithm. At the end of each loop it is then necessary to determine whether the loop stopped because of the successful match or because no match could be found, introducing further complexity.

Efficiency considerations

The appearance of triply-nested loops is an indication that a lot of work is going to be done by this algorithm, at least under some circumstances. Say the key and test phrase contain the same letters, all different, and one is the reverse of the other. The matching characters will eventually be found at the last possible position, and the number of iterations of the inner loop will be proportional to square of the length of the phrases. An algorithm that performs work in proportion to the square of the size of its data is called a quadratic-time (or just quadratic) algorithm. Many quadratic algorithms (for example, Bubblesort) can be replaced by more complex algorithms that take less time in most cases.

For 26-character phrases, the maximum number of comparisons is 26*27/2 = 351. (For longer phrases the fact that there are only 26 different symbols limits the total comparisons to about 13n for strings of length n. The maximum value is 738 for 60-character phrases.)

If the test phrase contains an e we really don't care which e in the key it matches. All that matters is whether there are more es in the test phrase than in the key phrase.

Algorithm 2 counts the number of letters in the key phrase and saves the 26 counts in an array. For each test phrase it applies the same tally operation (in a procedure or function) and then checks whether any of the test counts exceeds the corresponding key letter count.

Algorithm 2 (frequency counts):

read key phrase
tally letters in key phrase, store in key_freqs array
for each test phrase
    tally letters in test phrase, store in freqs array
    for each possible letter c
        if freqs[c] > key_freqs[c]
            exit loop

    if match failed
        display "NO"
    else
        display "YES"

Efficiency considerations

Algorithm 2 inspects each character of the test phrase only once, but scans the entire array of frequencies twice, once to set every element to zero and once to look for matching frequencies. The total number of comparisons or array inspections/changes for a test phrase of length n is n + 2*26, which is better than Algorithm 1's 13n for n > 3. It's also an example where the use of additional storage (in this case, the frequency array) can result in a less complex algorithm (because the searches are simpler).

Extension

The extension to this task is very straightforward, requiring all previous test phrases to be retained and the next test phrase to be looked up in this set. Keeping the set in a linear structure (an array of strings, say), makes the programming easier but results in a fairly expensive program if the number of test phrases is large.

If the maximum number of different phrases (100) occurs, as many as 100*101/2 or 5500 string comparisons will be needed to determine that none of them is a duplicate of its predecessors. This is another example of a quadratic algorithm, this time in the number of inputs. Unfortunately, more sophisticated methods to store the strings (search trees, hash tables) are beyond the scope of this discussion. They are an important part of university computing courses, though.

Solutions (enciphered)

Download details here (all tasks).

Solution to Task E: Noughts & Crosses Checker


Judges' Report

Only a small number of teams attempted this task. It may have seemed a little daunting to some, but most of the input is window-dressing and the problem boils down to an array row/column counting exercise. Still, it does require a lot of coding.

Most attempts correctly extracted the useful content from the input and assigned them to elements of a 3x3 array. (Some Visual Basic solutions accepted data from 9 entry boxes rather than reading from the test files; this approach is acceptable since the time saved in not having to parse an input file is offset by time to design the form and perform the interactive tests.) The methods used to identify winners included

Solutions that used loops constructively rather than many if statements were preferred (and more likely to be correct).

Teams that attempted the extension (identifying impossible board positions) did so with varying success. Multiple winners can be handled fairly easily, and counting schemes can easily check for an inappropriate number of symbols.


Problem Analysis

There are eight possible winning sequences, three rows, three columns and the two diagonals. If any of these contains three X or 0 symbols, a winner can be declared. If no winner can be found and the board position is legal, then the result is either incomplete if there are spaces remaining or a draw otherwise.

Identifying winners can be done with any of the methods mentioned in the Judges Report. If a counting approach is used, however, the other checks are more easily completed. For example, if we add up the number of 0s and Xs in every row and every column (but not the diagonals), we'll end up counting every symbol twice. Thus if no winner has been found, the total number of symbols will be 2*9 = 18 if every square is filled (a draw), or the game is incomplete otherwise.

Algorithms

Algorithm 1 (valid board assumed):

set totalX and total0 to zero
read in board position to board array
for each row
    count Xs and 0s
    add to totalX and total0 counters
    if X or 0 count = 3
        declare winner

for each column
    count Xs and 0s
    add to totalX and total0 counters
    if X or 0 count = 3
        declare winner

for each diagonal
    count Xs and 0s
    if X or 0 count = 3
        declare winner

if a winner has been found
    display winner " wins"
else if totalX+total0 = 2*3*3
    display "Draw"
else
    display "Incomplete"
How the board array elements are stored and inspected will vary with the language, of course. For example, inspecting a row in Pascal could look like:
for col := 1 to 3 do begin
    if board[row, col] = 'X' then
        countX := countX + 1
    else if board[row, col] = '0' then
        count0 := count0 + 1
end;
For the basic task, declaring the winner simply means saving the winning symbol in a character variable. For example, in C we could have
/* result holds the result of the last row/column/diagonal check */
if (result != ' ')  /* New winner */
    winner = result;
For the extension, the previous value of this variable is relevant too. Five different cases can arise when a new result is obtained:
Current winner Next result New winner
Nobody any = result
any Nobody unchanged
X or 0 same as current unchanged
X or 0 0 or X (different) error
error any unchanged (still error)
The error case arises whenever a winner is discovered that is different from the current winner. The error condition propagates through to the final report. Why don't we need to check if the same symbol wins two ways? Think about it before checking.

Why not check for the same winner?

If the same symbol wins in two ways, the game may or may not be legal. It's possible for an "X" to be placed in a corner, say, completing a row and column simultaneously, or a diagonal with a row or column or the other diagonal. On the other hand, two rows or two columns can't be completed in one move.

How can we distinnguish the legal and illegal cases?

The second scheme is already required to test for other illegal board positions, so there's no extra effort in using it to account for the double-winner problem.

Algorithm 2 (first extension):

set totalX and total0 to zero
read in board position to board array
for each row
    count Xs and 0s
    add to totalX and total0 counters
    if X or 0 count = 3
        update winner

for each column
    count Xs and 0s
    add to totalX and total0 counters
    if X or 0 count = 3
        update winner

for each diagonal
    count Xs and 0s
    if X or 0 count = 3
        update winner

if X and 0 counts differ by more than 2
    display "Impossible board position"
else if multiple winners found
    display "multiple winners"
else if a winner has been found
    display winner " wins"
else if totalX+total0 = 2*3*3
    display "Draw"
else
    display "Incomplete"
The current winner is updated by replacing it by the new winner (if appropriate) or by a multiple-winner indicator, or leaving it unchanged. Writing the update algorithm out in full:

Update current winner:

if current is multiple
    no change to current
else if no new winner
    no change to current
else if no current winner
    replace current by new winner
else if current and new winner are the same
    no change to current
else
    set current to multiple
There are shorter ways to implement this, but be careful that you don't fold incompatible cases together.

Solutions (enciphered)

Download details here (all tasks).

Solution to Task F: Investigating Chaos


Judges' Report

This task, apparently complex on the surface, is really quite straightforward once you separate the programming task from the context. Fascinating though the study of chaotic systems is, much of it comes down to mundane things like iterating a function.

Teams that attempted the problem usually managed to score most of the available points. The output format was chosen to make it easy to produce with a single loop based on the required number of iterations; some people cheated by relying on the output device to move to a new line after each five iterations instead of coding it as such. Others, looking toward the extension (even if it wasn't completed), loaded up an entire array with values before starting to print. The limitation of attractors being found in the last 16 iterations means that only this many need be retained for checking. The checking algorithm and associated data structure are discussed below.


Problem Analysis

The basic task can be described thus:
given a function f (p) and an initial value p0, print the first n terms in the sequence obtained using p' = f (p), ending the line after the first, sixth, ..., 5m+1-th iteration, for m = 0+.

First code the function, then write the main program to read the parameters and set up the iteration. Simple.

For the extension, it's necessary to keep some history of previous iterations. In increasing order of sophistication, the following schemes will work:

  1. Keep every value in a (large) array, then inspect the last 16.

  2. Keep the first 16 in a 16-element array, then for each new term shuffle the previous 15 elements down one place to make room for the next one. Finally inspect the whole array.

  3. Keep the first 16 in an array that acts like a circular buffer, each new item overwriting the oldest one.
Method A is wasteful of space, as most elements are given values which are then ignored. In the other two the array represents a data structure called a bounded stack. Method B is a poor way to implement a stack, however, since after the 16th item every subsequent item requires 16 array assignments. Method C, where the stack is like a snake eating its tail, can be tricky to code correctly, although it wastes neither time nor space.

Algorithms

Algorithm 1 (basic task):

read parameters: k, p and nit
for i ranging from 0 to nit-1
    display p
    if i mod 5 = 0 then
        terminate output line
    calculate next p
If you prefer to count from 1 to nit, the remainder test is made against 1 rather than 0. Most languages make it easy to format the floating point number to have 4 decimal places:

Language version Formatting statement or expression
BASIC (most) PRINT USING "#.####"; p;
Visual Basic Format(p, "0.0000")
C printf("%6.4f", p);
Pascal write(p:6:4);
Java Double.toString(p).substring(0,8);

Algorithm 2 (find attractors):

read parameters: k, p and nit
initialise stack
for i ranging from 0 to nit-1
    display p
    add p to stack
    if i mod 5 = 0 then
        terminate output line
    calculate next p


remove last element x of stack
if x occurs n places earlier in stack
    display n " attractors" and the last n values on the stack
else
    display "Chaos"
Most of the extra work is performed in manipulating the stack. In a well designed program the stack operations will be separate from the main algorithm. They include: As an example, consider the state of a 10-element stack after a large number of push operations have occurred. The stack, stored in an array, looks like this:
2.3  4.5  1.1  7.0  4.2  6.9  1.0  4.9  9.8  6.5
                ^
The marker indicates the position of the oldest item on the stack, 7.0. Call the corresponding array index next_free. The last one pushed lies to its left (1.1), and it was preceded by 4.5, 2.3, 6.5 (wrapping around the end), and so on. The push algorithm becomes
push(item x) :-
    store x in stack[next_free]
    set next_free to (next_free + 1) mod stack_size
So the operation push(3.3) results in the
2.3  4.5  1.1  3.3  4.2  6.9  1.0  4.9  9.8  6.5
                     ^
Getting access to earlier elements (for searching and printing) requires the inverse of the step
   set next_free to (next_free + 1) mod stack_size
to be computed. It's tempting to make this
   set next_free to (next_free - 1) mod stack_size
but mod operators do funny things when their left-hand operation is negative, which occurs when next_free = 0. Luckily, adding one less than the size of the stack is equivalent to subtracting 1 modulo stack_size, giving the function
previous(position pos) :-
   return (pos + stack_size - 1) mod stack_size

Solutions (enciphered)

Download details here (all tasks).