=========================================================================== CSC 236 Lecture Summary for Week 8 Fall 2007 =========================================================================== Working on example: def pow(x, y): # function precondition: x in R, y in N z = 1 m = 0 # loop precondition: x in R, y in N, z = 1, m = 0 while m < y: z *= x m += 1 # loop postcondition: m = y, z = x^y return z # function postcondition: returns x^y 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. - How to prove loop invariant? By induction on number of iterations. - 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 "m = y and 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". Proof of LI by induction on number of iterations: Base Case: Before loop starts, m_0 = 0 <= y in N and z_0 = 1 = x^0 = x^{m_0}, so LI_0 is true.. Ind. Hyp.: Let k >= 0 and suppose LI_k holds, i.e., m_k <= y and z_k = x^{m_k}. Ind. Step: To prove LI_{k+1}, consider two cases. Case 1: There is no iteration number k+1. Then, LI_{k+1} equivalent to LI_k (since m_{k+1} = m_k and z_{k+1} = z_k), so LI_{k+1} holds by IH. Case 2: There is an iteration number k+1. Then, m_k < y (loop condition was true) and from algorithm, z_{k+1} = z_k * x and m_{k+1} = m_k + 1. Hence, m_{k+1} = m_k + 1 <= y, and z_{k+1} = z_k * x = x^{m_k} * x = x^{m_k+1} = x^{m_{k+1}} (since z_k = x^{m_k} by IH). Hence, LI_{k+1} holds in all cases. Therefore, by induction, LI holds after each iteration. When loop terminates, m >= y (from the loop test) and m <= y and z = x^m (from LI) so m = y and z = x^m = x^y. This proves loop postcondition, and function is correct. Example 2: selection sort selection_sort(A): # Pre: elements of A are comparable # Loop Inv: A[1..i-1] sorted and A[1..i-1] <= A[i..len(A)] for i in [1..len(A)]: s = i # Loop Inv: i <= s < j and A[s] <= A[i..j-1] for j in [i+1..len(A)]: if A[j] < A[s]: s = j A[i], A[s] = A[s], A[i] # Post: A is sorted in non-decreasing order General technique for multiple loops: - Consecutive loops (not nested): simply prove separate loop invariants one after the other, where proof of second one can rely on first one being true at the end of the first loop (like having partial pre- and post-conditions in between the loops). - Nested loops: work inside-out. Prove termination and loop invariant for inside loop first, for arbitrary value of the outside loop variables, and use that to prove termination and invariant for outside loop. - Remember that loop for x in [m..n]: ... is equivalent to x = m while x <= n: ... x += 1 This affects how we write invariants and prove termination. Inside loop: - Loop invariant: LI = "i <= s < j <= len(A)+1 and A[s] <= A[i..j-1]". - Proof by induction on number of iterations: Base Case: s_0 = i and j_0 = i+1 so i <= s_0 < j_0 <= len(A)+1 (because i <= len(A) inside body of outside loop), and A[s_0] <= A[i] = A[i..j_0-1]. Ind. Hyp.: Let k >= 0 and suppose LI_k. Ind. Step: To prove LI_{k+1} consider two cases. Case 1: If there is no iteration number k+1, then LI_{k+1} is equivalent to LI_k so it is true by the ind. hyp. Case 2: If there is an iteration number k+1, then from loop condition, j_k <= len(A). Also, from ind. hyp., i <= s_k < j_k <= len(A)+1 and A[s_k] <= A[i..j_k-1]. Consider two subcases, based on if-statement. Subcase A: If A[j_k] < A[s_k], then A[j_k] <= A[i..j_k] and algo sets s_{k+1}=j_k. Subcase B: If A[j_k] >= A[s_k], then A[s_k] <= A[i..j_k] and s_{k+1} = s_k. In all cases, A[s_{k+1}] <= A[i..j_k] = A[i..j_{k+1}-1] since j_{k+1} = j_k + 1. In all cases, LI_{k+1} is true. Hence, by induction, LI holds for any number of iterations. When inside loop stops, j = len(A)+1 so A[s] <= A[i..len(A)]. - Termination: use E = len(A)+1-j. . E_0 = len(A)+1-(i+1) = len(A)-i >= 0 because i <= len(A) in body of outside loop. . For some k >= 0, if there is an iteration number k+1, then from code we know j_{k+1} = j_k+1, so E_{k+1} = len(A)+1-j_{k+1} = len(A)+1-(j_k+1) = len(A)+1-j_k-1 = E_k-1 < E_k. Also, E_{k+1} >= 0 because j_k <= len(A) (from loop condition). . Therefore, E_0 > E_1 > ... is a decreasing sequence of natural numbers and must be finite by the well-ordering principle, i.e., the inside loop terminates. Outside loop: - Loop invariant: LI' = "A[1..i-1] sorted and A[1..i-1] <= A[i..len(A)]". - Proof by induction on number of iterations: Base Case: i_0 = 1, so A[1..i_0-1] = A[1..0] (trivially sorted), and A[1..i_0-1] = A[1..0] <= A[1..len(A)] = A[i_0..len(A)] is vacuously true because A[1..0] is empty. Ind. Hyp.: Let k >= 0 and suppose LI'_k. Ind. Step: To prove LI'_{k+1} consider two cases. Case 1: If there is no iteration number k+1, then LI'_{k+1} is equivalent to LI'_k so it is true by the ind. hyp. Case 2: If there is an iteration number k+1, then from the loop condition we know that i_k <= len(A). Also, ind. hyp. tells us that A[1..i_k-1] is sorted and A[1..i_k-1] <= A[i_k..len(A)]. After inside loop runs, we know from LI (for inside loop) that A[s] <= A[i_k..len(A)]. Then, algo swaps A[s] and A[i_k], so A[i_k] <= A[i_k+1..len(A)], and since A[i_k-1] <= A[i_k..len(A)] (from ind. hyp.) we know that A[i_k-1] <= A[i_k], i.e., A[1..i_k] is sorted. Since i_{k+1} = i_k+1, this proves LI'_{k+1}. In all cases, LI'_{k+1} is true. Hence, by induction, LI' holds for any number of iterations. - When outside loop terminates, we know i = len(A)+1 so LI' tells us that A[1..len(A)] is sorted (and A[1..len(A)] <= A[len(A)+1..len(A)], which is vacuoucly true because A[len(A)+1..len(A)] is empty). This is exaclty the postcondition for selection sort. - Termination: use E = len(A)+1-i, just like for inside loop.