These are notes on the usage of assertions in small programs. I used pseudo-code that is almost a real PL. Assertions are either enclosed in braces (following CS literature tradition, not to be confused with blocks in C++ or Java), or given using the notation /*@ assertion */.
This is a Correct by Design example from the book by Dijkstra, Edsger Wybe, A Discipline of Programming, Prentice-Hall, 1976. A classic. Has many examples of Correct by Design. Dijkstra is a Turning Award Winner 1972.
Problem: An array a[1..n] is filled with elements colored red, white or blue. Design an algorithm that rearranges the elements so that all the red-elements are at the left, all the blues are at the right, and the whites are in the middle.
Non-functional Requirements: Do not assume that red < white < blue. We are expecting an O(n) algorithm. Focus on assertions.
Answer: We use a[x .. y] to stand for all elements a[i] such that x <= i <= y. When x > y, a[x..y] is empty and we can ascribe any property P wish to it. The location of LI is immediately after the "while" token.
r = w = 0; b = n+1; while /*@ LI is a[1..r] is red and a[r+1 .. w] is white and a[b..n] is blue */ (w+1 < b) { switch (a[w+1]) { case white: w ++; break; case red: w ++; exchange a[w] with a[r+1]; break; case blue: b --; exchange a[w+1] with a[b]; break; } } /*@ w+1 == b and a[1..r] is red and a[r+1 .. w] is white and a[b..n] is blue */
The while loop terminates because the gap (b - w) guaranteedly decreases in every iteration.
You should also explain why/how the LI remains valid after each of the switch-cases.
Exercise: Write down the two loop invariants. Their location is immediately after the first semicolon of the for loop. Before the termination test.
There are many variations in the implementation.
template < typename T> void bubbleSort(std::vector < T > & v) { const int size = v.size(); for(int i = 0; i < size; i++) { for(int j = 0; j < size - i; j++) { if (v[j] > v[j+1]) std::swap(v[j], v[j+1]); } } }
Variation #2: Better suited to the name of the algorithm. [Spot any errors?]
template < typename T> void bubbleSort(std::vector < T > & v) { const int size = v.size(); for(int i = 0; i < size; i++) { for(int j = size - 1; j >= i ; j--) { if (v[j] > v[j+1]) std::swap(v[j], v[j+1]); } } }
Q. The following is known as the game of Stanley Gill (see Abacus, Vol 4, No. 4, 1987, page 31). State (1) the proof rule for repeat-until, (2) a strong-enough loop invariant at the bottom of the loop body, and (3) prove the correctness of the algorithm.
LCM stands for the 'least common multiple'; GCD stands for greatest common divisor.
{a > 0 and b > 0} u := a; x := a; v := b; y := b; repeat if x < y then y := y - x; v := v + u fi; if y < x then x := x - y; u := u + v fi; {loop invariant} until x = y {x = y and ((x+y) div 2) = gcd(a, b) and ((u+v) div 2) = lcm(a, b)}
A: (1) Proof rule for repeat-until:
{P} S {Q}, Q and ~B -> P ------------------------------ {P} repeat S until B {Q and B}
(2) A strong-enough loop invariant is given below.
a >= x > 0 and b >= y > 0 and u > 0 and v > 0 gcd(a, b) = gcd(x, y) and xv + yu = 2ab (i)
(3) We show that (i) is a loop invariant. Referring to the rule of repeat- until above, both P, and Q are being taken as (i), and B as x != y. It is trivial to see that Q and ~B -> Q.
{(i)} if x < y then y := y - x; v := v + u fi; {(i)} and you can check that
{(i)} if y < x then x := x - y; u := u + v fi; {(i)}
follows similarly. If x >= y, the first of these assertions immediately follows, so we need only worry about the case when x < y. So, we need to show
{(i) and x < y} y := y - x; v := v + u fi; {(i)}.
From the axiom of assignment, and the rule of consequence, this is equivalent to showing that
(i) and x < y implies a >= x > 0 and b >= y-x > 0 and u > 0 and v+u > 0 gcd(a, b) = gcd(x, y-x) and x(v+u) + (y-x)u = 2ab
This simplifies to
(i) and x < y implies a >= x > 0 and b >= y-x > 0 and u > 0 and v+u > 0 gcd(a, b) = gcd(x, y-x) and xv + yu = 2ab
We know that gcd(x, y) = gcd(x, y-x) if x < y, so the above implication follows easily.
From the repeat-until rule, we can therefore conclude that Q and B, which is
a >= x > 0 and b >= y > 0 and u > 0 and v > 0 gcd(a, b) = gcd(x, y) and xv + yu = 2ab and x = y
holds at the end of the loop. gcd(a, b) = gcd(x, x) = x = (x + y) div 2. The harder part is to show that (u+v) div 2 = lcm(a, b). This part of the proof is independent of the above algorithm. It only depends on the number-theoretic properties of lcm and gcd, and here it goes:
Let g = gcd(a, b). Then a = gi, for some i, and b = gj, for some j, such that i and j do not have any common factors. (Otherwise, g would not be the gcd.) Therefore, lcm(a, b) = lcm(gi, gj) = g*lcm(i, j) = gij.
xv + yu = 2ab = 2gigj = 2ggij = g(v + u), because x = y = gcd(a,b) = g
Therefore, u + v = 2gij and lcm(a, b) = gij = (u+v) div 2.
Exercise: Show that u+v is always even.
{a > 0 & b > 0 & (odd(a) or odd(b)} x := a; y := b; loop while even(y) do y := y/2 od; while even(x) do x := x/2 od; if x = y then exit loop; if x < y then y := (y - x)/2 else if x > y then x := (x - y)/2 end {x = y = gcd(a, b)}
A: LI == loop invariant. The algorithm uses a loop construct with a middle exit.
LI0 between lines 5 and 6: gcd(x, y) = gcd(a, b).(N.B. That all three have the same loop invariant is highly unusual.)
Exit assertion at 10 follows from LI0 because gcd(x, x) = x.
The proof rules for the middle-exit loop are given below. If the following three conditions hold
(1) {A} S1 {LI}then we can conclude that the following holds.
{A} loop S1; {LI} if BX then exit loop; S2; end {B}
Exercise: Prove the correctness: See the Game of Stanley Gill for the rest.
Exercise: Does {odd(a) or odd(b)} imply {a > 0 & b > 0} ?
The function nlass(s) returns the length of longest ascending subsequences of the string s. Design an algorithm for it. Convince us, preferably through a correctness proof, that your algorithm 'works'.
The following solution is a mild adaptation of the solution and discussion of the above problem from the book "The Science of Computer Programming," Section 20.2 by David Gries.
Let s[1..n] be the given string, lass(x) stand for the set of all longest ascending subsequences of x, and let len(x) stand for the length of a longest ascending subsequence of x. Suppose we have so far examined s[1..i], and found k to be the length of longest asc subseq, i.e.,
P(d) is k = len(s[1..i]) & 1 <= i <= n
We would like to extend the above to hold for P(d+delta), where delta is going to be the element s[i+1]. It is clear that, for i = 1, k will be 1.
Consider lass(s[1..i]).
Q: Systematically develop an algorithm for the `previous permutation' problem. That is, given a sequence s of numbers, your algorithm should generate a t, also a sequence of numbers, such that s is the next permutation of t (as defined in class). Describe precisely all intermediate steps in the development.
A: The problem can be solved only if s is not sorted. Let n == length of s.
(a) Find i:1..n such that s[i] > s[i+1], and s[i+1..n] is sorted.
(b) Find j:i+1..n such that s[j] < s[i] and s[j] is the highest such element.
(c) Exchange s[i] with s[j], and invert the tail s[i+1..n]
Exercise: We leave the implementation details to you.
[TBD make start index 0. URL to cpp] Q: In a printed line of length L, certain characters are marked. The marks are represented by a specific character (say, '*') printed below each such character. The positions (indices) of the marked characters are given in an array p[1..n], n >= 0. These elements of p are such that for all i: 1 <= p[i] <= L, and p[1] <= p[2] <= p[3] <= ... <= p[n]. This example is from Niklaus Wirth, Systematic Programming, 1973 (book).
Example: p = <3, 6, 9, 19, 22>, n = 5 line of text :Thes broplem is peenuds. line of marks: * * * * *
Investigate the following proposals (written in C++, but with array index starting at 1) to print the line of marks. The line of text is assumed to have been printed already.
To save some vertical space, we violate a few well accepted pretty printing conventions.
#define n whatever #define L whatever int p[n+1]; int i, j, k; // (a) k = 1; for (i = 1; i <= L; i++) { if (i == p[k]) { putchar('*'); k ++; } else putchar(' '); } // (b) k = 1; p[n+1] = 0; for (i = 1; i <= L; i++) { if (i == p[k]) { putchar('*'); do { k++; } while (i == p[k]); } else putchar(' '); } // (c) i = 1; for (k = 1; k <= n; k++) { do { putchar(' '); i++; } while (i != p[k]); putchar('*'); i++; } // (d) i = 1; k = 1; do { while (i < p[k]) { putchar(' '); i++; } if (i == p[k]) { putchar('*'); i++; } k++; } while (k <= n); // (e) i = 1; k = 1; while (k <= n) { while (i < p[k]) { putchar(' '); i++; } while (i == p[k]) k++; putchar('*'); }Answer:
The loop invariants of the various proposals are similar; so to save space we use the following abbreviations.
Let I stand for 1 <= i <= L+1.
Let K stand for 1 <= k <= n+1
Let P stand for (for all i:1..n-1(1 <= p[i] <= p[i+1] <= L))
Let IKP stand for I & K & P.
Let s stand for the seq of chars printed so far; #s denotes the length of s.
Let a[1..y] denote the correct seq that needs to be printed; i.e., for all j:1..y, a[j] == '*' iff p[k] == j for some k in 1..x; a[j] == ' ', otherwise. This seq a is clearly a function of p, x and y; so we write a = es(p, x, y), es for expected seq.
Note that n may or may not be <= L. It is in establishing the
proper relationship between s and a that most of the proposals failed.
The relation should have been
s == a[1..i] == es(p, k, i) at all times, and
s == a[1..L] == es(p, n, L) eventually.
(a.i) IKP & s == es(p, k-1, p[k-1]) | blank ^ (i - p[k-1]), where blank ^ x stands for x-blanks when x > 0, for empty seq when x <= 0.
(a.ii) The p[k]'s are not guaranteed to be distinct. If they are not, once an i equals p[k], k will get incremented and then onwards it will remain unchanged. Also, k can become equal to n+1.
(b.i) IKP & s == es(p, k-1, i-1) for the for-loop.
IKP & s == es(p, k-1, i) for the do-loop at the boolean exp. (b.ii) correct except for reference to p[n+1], which is non existent.
(c.i) IKP & s == es(p, k-1, i-1) & i-1 <= p[k-1] for the for-loop, and
K&P & s == es(p, k-1, i) for the do-loop at the boolean exp. Note that I is dropped from the second loop invariant. (c.ii) Suppose p[1] == p[2] >= 2. We then get an infinite sequence of blanks after the '*'. Also, if p[1] == 1, we get an inf seq of blanks.
(d.i) IKP & s == es(p, k, i) & i-1 == p[k-1] at bool exp for the do-loop.
IKP & s == es(p, k-1, i-1) & (k <= n -> i <= p[k]) for the while-loop. (d.ii) n can be 0, so that p[1] is undefined, and yet (d) accesses p[1]. The informal stmt of the problem does not quite say it, but don't we want L chars (blanks/stars) printed? (d) does not do that.
(e.i) IKP & s == esb(p, k-1) & #s <= i <= p[k] for the outer while-loop.
IKP & s == esb(p, k-1) | blank^x & #s <= i <= p[k] for the inner
while-loops.
(e.ii) forgot to increment i after printing '*'; so fails, e.g., on p[1] = 1,
p[2] = 2.
typedef ordered but opaque is item; typedef 0 .. n-1 is index; global array a[index] of item; exchange(index i, index j) { item temp := a[i]; a[i] := a[j]; a[j] := temp }Specify exchange(). Rewrite the code of quick sort using this primitive and with the additional constraint of using no variables of type item other than the a[]. Verify the correctness of this revised version of quick sort.
/*@ x is the given input integer, x > 0 */ while (x > 1) { if (x % 2 == 0) x /= 2; else x = 3*x + 1; }
Does this terminate for arbitrary integers x > 0? Ignore arithmetic overflow issues. Read http://en.wikipedia.org/wiki/ Collatz_conjecture.