SE 504
Development of a program to solve the Maximum Segment Sum Problem

Let A be an integer array. Define, for p and q satisfying 0≤p≤q≤#A,

S.p.q  =  (+i | p≤i<q : A.i)

That is, S.p.q is the sum of the elements in the array segment A[p..q).

Among the elementary observations we can make regarding S are these:

(1) S.k.k = 0 for all k (0≤k≤#A) because A[k..k) is an empty segment,
(2) S.k.(k+1) = A.k for all k (0≤k<#A) because A[k,k+1) is a one-element segment containing only A.k
(3) S.k.n = S.k.m + S.m.n for all k,m,n satisfying 0≤k≤m≤n≤#A, because

the sum of the elements in A[k..n)   =   the sum of the elements in A[k..m) + the sum of the elements in A[m..n).

As a special case of (3), take m = n−1:

(3') S.k.n = S.k.(n−1) + S.(n−1).n

which, by (2), gives us

(3'') S.k.n = S.k.(n−1) + A.(n−1)

In the Maximum Segment Sum problem, A is given as input and the output to be produced is the maximum among all the S.p.q's. That is, the program's specification is

|[con A : array of int;
  var z : int;
  { true }

  z := ?

  {Q : z = (MAX p,q | 0≤p≤q≤#A : S.p.q) }
]| 

As an example, consider the integer array

    0   1   2   3   4   5   6   7   8   9  10  11  12
  +---+---+---+---+---+---+---+---+---+---+---+---+---+
A | 2 |-1 |-2 | 3 | 2 |-2 | 3 |-1 | 1 |-6 | 4 |-1 | 3 | 
  +---+---+---+---+---+---+---+---+---+---+---+---+---+ 

The maximum among all segment sums is 6, which is achieved by each of the following segments: A[3..7), A[3..9), A[3..13), and A[10..13).

Let us attempt to derive a solution by employing the techniques from the course. The first observation we can make is that we are unlikely to find a solution that doesn't involve a loop (or recursion, or some device for iteration). Among our heuristics for developing loops, the one that seems to be applicable is replace a constant (in the postcondition) by a variable. The two constants appearing in the postcondition are 0 and #A. Suppose that we replace #A by a "fresh" variable r in order to obtain as a candidate for a loop invariant the following:

I : z = (MAX p,q | 0≤p≤q≤r : S.p.q)

As a loop guard, we would choose B : r ≠ #A, because then we obviously have [I ∧ ¬B ==> Q] (as required by proof obligation (iii) with respect to loops).

Indeed, one could rewrite the postcondition as

z = (MAX p,q | 0≤p≤q≤r : S.p.q)   ∧   r = #A

which strengthens the original postcondition insofar as it requires the fresh variable r to satisfy r = #A. Employing the delete a conjunct heuristic, we get I as the suggested loop invariant and ¬(r = #A) (i.e., r ≠ #A) as the suggested loop guard.

Our program skeleton is now

|[con A : array of int;
  var z : int;
  var r : int;
  { true }
  r, z := ?, ?;
  { loop invariant I : z = (MAX p,q | 0≤p≤q≤r : S.p.q) }
  do r ≠ #A  ---> 
     r, z := ?, ?
  od
  {Q : z = (MAX p,q | 0≤p≤q≤#A : S.p.q) }
]| 

In order to satisfy proof obligation (i), r and z must be initialized so as to truthify I. The easiest way to do this is to set r to 0, thereby making the range in the quantification correspond to p=q=0. Applying the one-point rule (and using our above observation that S.k.k = 0 for all k), we obtain that z should be set to zero, too.

Now consider the loop body. Given that we have decided to initialize r to zero and that the loop guard indicates termination when r becomes #A, the most obvious way to modify r inside the loop is by incrementing it. Let's assume, for the moment at least, that this is what we shall do.

As the expression needed on the right-hand side of the assignment command for incrementing r does not involve any other variables, we make it the last command within the loop body and place the command(s) that update z before it. (This allows us to focus upon z exclusively without requiring that r be updated simultaneously.) The refined (and annotated) program is as follows:

|[con A : array of int;
  var z : int;
  var r : int;
  { true }
  r, z := 0, 0;
  { invariant I : z = (MAX p,q | 0≤p≤q≤r : S.p.q) }
  do r ≠ #A  --->
     {I  ∧  r ≠ #A}
     z := E;
     {I(r:=r+1), i.e., wp.(r:=r+1).I}
     r := r + 1;
     {I}
  od
  {Q : z = (MAX p,q | 0≤p≤q≤#A : S.p.q) }
]| 

The loop body has been annotated with pre- and post-conditions in accord with proof obligation (ii) on the "loop checklist". The intermediate assertion I(r:=r+1) is motivated by the Hoare Triple Law of Catenation (which says that {P} S;T {Q} follows from {P} S {R} ∧ {R} T {Q}) and the fact that this assertion corresponds to wp.(r:=r+1).I, making it the weakest predicate R satisfying {R} r:=r+1 {I}.

We have reduced our problem to that of finding E to truthify the Hoare triple

{I ∧ r≠#A} z := E { I(r:=r+1) }

Let's try to calculate E using proof obligation (ii):

Assume I ∧ r≠#A.

    wp.(z:=E).I(r:=r+1)

 =    < wp assignment law >

    (I(r:=r+1))(z:=E)

 =    < defn of I >

    ((z = (MAX p,q | 0≤p≤q≤r : S.p.q))(r:=r+1))(z:=E)

 =    < textual substitution >

    (z = (MAX p,q | 0≤p≤q≤r+1 : S.p.q))(z:=E)

 =    < textual substitution >

    E = (MAX p,q | 0≤p≤q≤r+1 : S.p.q) 

 =    < rewrite range so as to "split off" terms in which q=r+1 >

    E = (MAX p,q | 0≤p≤q≤r ∨ 0≤p≤q=r+1 : S.p.q) 

 =    < range split (8.16) or (8.18) >

    E = (MAX p,q | 0≤p≤q≤r : S.p.q)  max  (MAX p,q | 0≤p≤q=r+1 : S.p.q)

 =    < (Gries 8.14) One-point rule (Technically, need to use 
        (8.20) Nesting, then (8.14), and then (8.20) "in reverse".)  >

    E = (MAX p,q | 0≤p≤q≤r : S.p.q)  max  (MAX p | 0≤p≤r+1 : S.p.(r+1))

 =    < assumption I : z = (MAX ... )  >

    E = z  max  (MAX p | 0≤p≤r+1 : S.p.(r+1))

At this point, we employ the strengthening the invariant heuristic by introducing a fresh variable y and including as a new conjunct of the invariant the following:

y = (MAX p | 0≤p≤r+1 : S.p.(r+1))

However, there is a problem with doing this. In order for this to hold upon completion of the final loop iteration (at which point r = #A), it would have to be the case that y = (MAX p | 0≤p≤#A+1 : S.p.(#A+1)). But S is not defined when its second argument exceeds #A, so it will not be possible for y's value to satisfy this condition (unless we insert code to cause y's value to become "undefined", which seems rather silly).

Is there any way to circumvent this obstacle? Well, suppose that we decide to augment the invariant with the new conjunct

I2: y = (MAX p | 0≤p≤r : S.p.r)

instead. (This is exactly what we suggested above as a new conjunct, except that we have replaced the two occurrences of r+1 with r.) Then we arrange the code in the body of the loop so that the value of y is updated to satisfy I2(r:=r+1) before the assignment to z. Note that our choice of z max y as the right-hand side of the assignment to z is correct as long as I2(r:=r+1) holds at the time that that assignment is performed (regardless of whether it was true at the beginning of the current loop iteration). Our program now looks like this:

|[con A : array of int;
  var z,y : int;
  var r : int;
  {#A ≥ 0}
  r, z, y := 0, 0, ?;
  { loop invariant I : I1 ∧ I2, where
       I1 : z = (MAX p,q | 0≤p≤q≤r : S.p.q)
       I2 : y = (MAX p | 0≤p≤r : S.p.r)
  }
  do r ≠ #A  --->
     {I ∧ r≠#A}
     y := F;
     {I1 ∧ I2(r:=r+1)}
     z := z max y;
     {(I1 ∧ I2)(r:=r+1), i.e., I(r:=r+1)}
     r := r + 1;
     {I}
  od
  {Q : z = (MAX p,q | 0≤p≤q≤#A : S.p.q) }
]| 

It remains to figure out how to fill in the right-hand sides of the two assignments to y. Given that r is initialized to zero, the initial value of y must satisfy I2(r:=0), which requires that y = 0. Hence, we initialize y to zero. As for the assignment inside the loop, we calculate F in the context of trying to prove

{I ∧ r≠#A} y := F {I1 ∧ I2(r:=r+1)}

Assume I and r≠#A.
    wp.(y:=F).(I1 ∧ I2(r:=r+1))

 =     < wp assignment law >

    (I1 ∧ I2(r:=r+1))(y:=F)

 =     < textual substitution distributes over operators >

    I1(y:=F) ∧ I2(r:=r+1)(y:=F)

 =     < text. sub. (y does not occur in I1) >

    I1 ∧ I2(r:=r+1)(y:=F)

 =     < assumption I1; (3.39) >

    I2(r:=r+1)(y:=F)

 =     < defn. of I2 and textual substitution (twice) >

    F = (MAX p | 0≤p≤r+1 : S.p.(r+1))

 =     < split off term (8.23); range is non-empty due to assumption 0≤r >

    F = (MAX p | 0≤p≤r : S.p.(r+1))  max  S.(r+1).(r+1)

 =     < from observation (1), S.(r+1).(r+1) = 0 >

    F = (MAX p | 0≤p≤r : S.p.(r+1))  max  0

 =     < from observation (3''), S.p.(r+1) = S.p.r + A.r >

    F = (MAX p | 0≤p≤r : S.p.r + A.r)  max  0

 =     < provided R is non-empty and there are no free occurrences
         of x in V, (MAX x | R : U + V) = (MAX x | R : U) + V      >

    F = ((MAX p | 0≤p≤r : S.p.r) + A.r)  max  0

 =     < assumption I2 >

    F = (y + A.r) max 0 

Now we have a completed program. In order to prove obligations (iv) and (v), we need to include 0≤r≤#A as another conjunct of our loop invariant. (Indeed, we cheated a little above in that we twice used 0≤r as an assumption, as though it had already been included in the invariant.)

|[con A : array of int;
  var z,y : int;
  var r : int;
  {#A ≥ 0}
  r, z, y := 0, 0, 0;
  { loop invariant I : I1 ∧ I2 ∧ I3, where
       I1 : z = (MAX p,q | 0≤p≤q≤r : S.p.q)
       I2 : y = (MAX p | 0≤p≤r : S.p.r)
       I3 : 0≤r≤#A
  }
  { bound function t : #A - r }
  do r ≠ #A  --->
     {I1 ∧ I2 ∧ I3 ∧ r≠#A}
     {I1 ∧ I2 ∧ I3(r:=r+1)}

     y := (y + A.r) max 0;

     {I1 ∧ I2(r:=r+1) ∧ I3(r:=r+1)}

     z := z max y;

     {I(r:=r+1)}

     r := r + 1;
     {I}
  od
  {Q : z = (MAX p,q | 0≤p≤q≤#A : S.p.q) }
]|

To have a complete understanding of how/why this program works, we should understand the role/purpose of each variable. The purposes of r and z should be clear: The value of z is the maximum sum among all sub-segments of the array segment A[0..r).

Somewhat less obvious is the role played by variable y, which was introduced during an application of the Strengthen the Loop Invariant heuristic. The conjunct I2 of the loop invariant that was introduced during that step, unsurprisingly, tells us what we need to know. It is

y = (MAX p | 0≤p≤r : S.p.r)

which says that y is the maximum among the sums of all sub-segments of A[0..r) that are suffixes of that sub-segment (i.e., that end at location r).

Now, during each iteration of the loop, the value of z must be updated from being the largest of the sums of all segments of A[0..r) to being the largest of the sums of all segments of A[0..r] (which includes the value in location r).

Under what circumstances should the value of z increase? (Clearly it should never decrease.)
Answer: In the case that the largest sum among the suffixes of A[0..r] is larger than the largest sum among all sub-segments of A[0..r). But the purpose of y is precisely to hold the former of those two values!