CMPS 144L Spring 2024
Lab #11 (April 18,19): Maximum Segment Sums Running Times

You will need the following files:

Download the relevant Java source code files, compile them, and run the MSS_Tester application. An example of a user/program dialog with that application is as follows:

Enter array length:10
Enter element value lower bound:-5
Enter element value upper bound:6
Enter seed for pseudo-random # generation:25
Array generated:
[0, -5, 6, 1, -4, 1, 6, 4, -5, 5]
MaxSegSum1 object reports: max segment sum is 14 and work measure is 285
MaxSegSum2 object reports: max segment sum is 14 and work measure is 74
MaxSegSum3 object reports: max segment sum is 14 and work measure is 20

The reported maximum segment sum is 14; that follows from the fact that the elements in locations [2..8) sum to 14 (as do those in locations [2..10)), and no other segment's sum is greater.

The MaxSegSum1 class that you have been given is complete. The same is not true about the MaxSegSum2 and MaxSegSum3 classes, however. (As given, they should report phony results of −1 for both max segment sum and work measure.)


Activity #1

Run the MSS_Tester application several times, using increasingly larger arrays. Indeed, start with an array of length ten, then one of length 20, then one of length 40, etc. (That is, double the length each time.) Ignore the outputs pertaining to MaxSegSum2 and MaxSegSum3, as those classes are not fully implemented. But notice the work measures reported by MaxSegSum1. They should increase by a factor of about eight each time. This is indicative of a program that runs in O(N3) time. Why? Because (2N)3 = 23·N3 = 8N3.

Activity #2

The MaxSegSum2 class, as given, has a stubbed version of the computeMaxSegSum() method. (This is where all the work takes place.) Replace it by the body of the same-named method found in the MaxSegSum1 class. Then modify it, as described below.

What makes this method so inefficient is that, during each iteration of the nested for-loop, the sum of the elements in array segment a[left..right) is computed, from scratch, by calling the segmentSum() method.

A better idea is to make use of the prefix sums array (call it ps[]) of a[]. The method prefixSums() produces this array. The nature of ps[] is this:

For each i satisfying 0 ≤ i ≤ a.length, ps[i] = sum of the elements in a[0..i).

This is significant because, once ps[] has been computed, we can (in constant time) obtain the sum of the elements in any array segment a[left..right) via a single subtraction: ps[right] - ps[left]. Why does this work? Because subtracting the sum of the elements in a[0..left) from the sum of the elements in a[0..right) leaves the sum of the elements in a[left..right)!

Modify the body of the computeMaxSegSum() method to exploit this fact. (Specifically, replace the call to segmentSum() to instead make use of the observation made above.)

Having made these modifications, run the tester program again and see what kind of "work measure" that you get from MaxSegSum2. Each time you double the length of the array, its work measure should increase by something close to a factor of four. This is indicative of a program whose asymptotic running time is O(N2). Why? Because (2N)2 = 22·N2 = 4N2.

Of course, you should also verify that the maximum segment sum values being produced by MaxSegSum1 and MaxSegSum2 are the same. If not, your computeMaxSegSum() method in the latter is most likely wrong.

Submit your completed MaxSegSum2 class to the folder.


Activity #3

More improvement (in running time) is possible! At a moderate level of abstraction, the computeMaxSegSum() method in each of the classes MaxSegSum1 and MaxSegSum2 carries out this algorithm:

maxSegSum = 0
do for each left in 0..N-1
   do for each right in left+1 .. N
      segSum = sum of elements in a[left..right)
      if segSum > maxSegSum then
         maxSegSum = segSum
      fi
   od
od

The final result, of course, is the value in variable maxSegSum. The difference between the computeMaxSegSum() methods in MaxSegSum1 and MaxSegSum2 is that the calculation of "sum of elements in a[left..right)" requires O(N) time (on average) in the former (which does that calculation via a call to the segmentSum() method) but only O(1) (constant) time in the latter (which does that calculation via the subtraction ps[right] - ps[left]) This accounts for why the latter runs faster by a factor of N.

The insight that can be used to obtain an even faster version of computeMaxSegSum() is that it is not necessary to consider, explicitly, every segment a[left..right) of a[]. (There are about N2/2 such segments, so if each one is explicitly considered, we are doomed to achieve a running time of no better than O(N2.)

Recall that, for all values of left and right satisfying 0 ≤ left < right ≤ N, the sum of the elements in a[left..right) is given by ps[right] - ps[left], where ps[] is the prefix sums array of a[]. We seek the largest among all these sums. Hence, what we seek is the largest value among all differences ps[right] - ps[left].

But that largest difference can be computed by making a single pass over the ps[] array! How? Iterate over the locations of the ps[] array using, say, variable k. The maximum among all sums of segments that end at location k is the largest among the values ps[k] - ps[0]ps[k] - ps[1], ps[k] - ps[2], ..., ps[k] - ps[k-1]. But it is not necessary to explictly compute all those differences, because the largest of them is ps[k] - ps[j], where ps[j] is the minimum of all the elements in ps[0..k)!

Thus, a key to making this improvement is to design the loop (in which k iterates over the range 1..N) so that it makes use of a variable j that satisfies this invariant:

ps[j] is the minimum of values in ps[0..k).

Use these ideas to develop the body of computeMaxSegSum in the MaxSegSum3 class. Then use the tester program to verify that its running time is O(N). That would mean that each time you double the length of the array, the work measure should double. This is indicative of a linear-time (i.e., O(N)) algorithm.

Of course, you should also verify that the maximum segment sum values being produced by MaxSegSum1 and MaxSegSum3 are the same. If not, your computeMaxSegSum() method in the latter is most likely wrong.

Submit your completed MaxSegSum3 class to the folder.