=========================================================================== CSC 236 Lecture Summary for Week 7 Winter 2008 =========================================================================== - Recursive binary search: RecBinSearch(x,A,b,e): 1. if b == e: 2. if x <= A[b]: return b 3. else: return e+1 else: 4. m = (b + e) / 2 # integer division 5. if x <= A[m]: return RecBinSearch(x,A,b,m) 6. else: return RecBinSearch(x,A,m+1,e) Precondition? Elements of A comparable with each other and x, 0 <= b <= e < len(A) (assuming array indices start at 0), A[b..e] sorted in nondecreasing order (A[b] <= ... <= A[e]). Postcondition? RecBinSearch(x,A,b,e) returns index p such that: . b <= p <= e+1; . if b < p, then A[p-1] < x; . if p <= e, then x <= A[p]. Proof of correctness: By induction on size n = e+1-b, prove (precondition and execution) implies (termination and postcondition). Inductive structure of proof will follow recursive structure of algorithm. Base case: n = 1, i.e., e = b. Then, algo terminates (lines 1-3 contain no loop or call), and returns b if x <= A[b], e+1 if x > A[e], which satisfies postcondition. Ind. Hyp.: Let n > 1 and suppose that for 1 <= k < n, for all inputs of size k that satisfy precondition, algo terminates and postcondition holds after execution. Ind. Step: Consider call RecBinSearch(x,A,b,e) when e+1-b = n >= 2. Test on line 1 fails, so b < e (since b <= e by precondition and b != e by negation of test) and algo executes line 4. [Exercise: prove b <= floor((b+e)/2) < e for all b < e.] Next, test on line 5 executes. Case 1: x <= A[m]. m < e -> m+1-b < e+1-b so by IH, RecBinSearch(x,A,b,m) returns index p such that: (1) b <= p <= m+1; (2) if b < p, then A[p-1] < x; (3) if p <= m, then x <= A[p]. Hence, . b <= p <= e+1 (from (1) since m < e); . if b < p, then A[p-1] < x (from (2)); . m < e -> m+1 <= e -> p <= e, so we must show x <= A[p]: o if p <= m, then x <= A[p] (from (3)); o if m < p, then A[m] <= A[p] (A is sorted) so x <= A[m] <= A[p]; in all cases, x <= A[p]. Therefore, current call satisfies postcondition. Case 2: A[m] < x. b <= m -> b < m+1 -> e+1-(m+1) < e+1-b so by IH, RecBinSearch(x,A,m+1,e) returns index p such that: (1) m+1 <= p <= e+1; (2) if m+1 < p, then A[p-1] < x; (3) if p <= e, then x <= A[p]. Hence, . b <= p <= e+1 (from (1) since b < m+1); . b < m+1 <= p so we must show A[p-1] < x: o if m+1 < p, then A[p-1] < x (from (2)); o if p <= m+1, then p-1 <= m so A[p-1] <= A[m] < x (A is sorted); in all cases, A[p-1] < x; . if p <= e, then x <= A[p] (from (3)). Therefore, current call satisfies postcondition. In all cases, current call satisfies postcondition. Therefore, by induction, RecBinSearch is correct. - NOTES: This may seem complicated, but only because we thought through borderline cases carefully -- in a sense, we ensured code works in all cases from the start, rather than write code carelessly and waste time fixing it up afterwards. - MergeSort(A,b,e): 1. if b == e: return 2. m = (b + e) / 2 # integer division 3. MergeSort(A,b,m) 4. MergeSort(A,m+1,e) # merge sorted A[b..m] and A[m+1..e] back into A[b..e] 5. for i in [b..e]: B[i] = A[i] 6. c = b 7. d = m+1 8. for i in [b..e]: 9. if d > e or (c <= m and B[c] < B[d]): 10. A[i] = B[c] 11. c += 1 else: # d <= e and (c > m or B[c] >= B[d]) 12. A[i] = B[d] 13. d += 1 Precondition? b, e (- N, 0 <= b <= e < len(A) elements of A[b..e] comparable with each other Postcondition? A[b..e] contains same elements as before, but sorted in non-decreasing order (A[b] <= ... <= A[e]) Proof of correctness: By induction on size n = e+1-b, prove (precondition and execution) implies (termination and postcondition). Inductive structure of proof will follow recursive structure of algorithm. Base case: n = 1, i.e., e = b. Then, algo terminates and returns A unchanged, which satisfies postcondition. Ind. Hyp.: Let n > 1 and suppose that for 1 <= k < n, for all inputs of size k that satisfy precondition, algo terminates and postcondition holds after execution. Ind. Step: Consider call MergeSort(A,b,e) when e+1-b = n >= 2. Test on line 1 fails, so b < e (since b <= e by precondition and b != e by negation of test) and algo executes line 2. Since b <= floor((b+e)/2) < e, IH implies that MergeSort(A,b,m) terminates and A'[b..m] contains same elements as A[b..m] sorted in non-decreasing order. For the same reason, MergeSort(A,m+1,e) terminates and A'[m+1..e] contains same elements as A[m+1..e] sorted in non-decreasing order. (Notation: to compare elements of A at various points during execution, use A' to refer to order after execution of recursive calls.) Line 5 copies A'[b..e] into B[b..e] (obvious enough to state without proof). Lines 6-13 merge B[b..m] and B[m+1..e] into A[b..e], which satisfies postcondition -- this requires formal proof, but we lack tools to carry this out for now; will come back to it later. -------------------- Iterative Algorithms -------------------- - Example: algorithm to compute x^y def pow(x, y): z = 1 m = 0 while m < y: z *= x m += 1 return z Precondition? x (- R, y (- N Postcondition? returns x^y (with convention 0^0 = 1) - To deal with loop, insert comments with what we know (or want to prove) at various points: # Pre: x (- R, y (- N # Post: returns x^y def pow(x, y): z = 1 m = 0 # loop precondition: x (- R, y (- N, z = 1, m = 0 while m < y: z *= x m += 1 # loop postcondition: z = x^y return z - To describe values of variables during different iterations, use notation v_k = value of v at the end of k complete iterations (just before loop test evaluated for (k+1)st time). Conventions: v_{k+1} = v_k if there is no iteration number k+1; subscript omitted for variables that do not change during loop. - In our example, suppose y = 2, then i | 0 1 2 3 ... ----|-------------------- m_i | 0 1 2 2 ... z_i | 1 x x^2 x^2 ... Partial Correctness and Loop Invariants: - Want to prove "precondition /\ execution -> postcondition", equivalent to "precondition -> (execution -> postcondition)", so suppose precondition. - Standard method: prove "loop invariant": statement about variables that holds before loop test is evaluated for every iteration (including at start of first iteration). - How to pick loop invariant? In practice, loop invariant part of code *design*, i.e., loop invariant used to help write loop, not the other way around. For now, given code, think of goal: to prove loop postcondition given loop invariant and negation of loop test. - Example: many statements are loop invariants ("m_k >= 0", "y >= 0", etc.), but many are not ("z_k = x^y", "z_k > 0", etc.). What do we know? At start, m_0 = 0, z_0 = 1. At end, m >= y (opposite of loop test) and want "z = x^y". Trace algorithm on small example, keeping track of values of m, z: iter 0 1 2 3 ... m 0 1 2 3 ... z 1 x x^2 x^3 ... Suggests loop invariant "z = x^m" (note: invariant should be stated as function of program variables only, i.e., without involving "iteration number" directly; otherwise, harder to prove postcondition from it). However, this alone does not allow us to conclude "m = y" at end of loop. We need more: m <= y. So complete loop invariant (LI) is "m <= y and z = x^m".