Notes on Maximum Subarray Problem

Our treatment of this problem is taken from Chapter 8 of the book Programming Pearls, second edition, by Jon Bentley. The chapter and the book are wonderful to read, and I highly recommend them. The author provides a brief sketch of the chapter in the form of lecture notes in PDF format, as well as the source code for the algorithms (in C), together with a convenient driver.

I offer my own Python implementation and test harness.

This problem is also introduced in Chapter 4.1 of CLRS, with a presentation of the O(n log n) divide-and-conquer algorithm. (The linear time algorithm is the subject of Exercise 4.1-5.)

The problem is to take as input an array of n values (some of which may be negative), and to find a contiguous subarray which has the maximum sum. For example, consider the array:

31 -41 59 26 -53 58 97 -93 -23 84

I will adopt a mix of notations from the two book, using A as the notation for the array, and assuming it is indexed from 0 to n-1.

We consider five distinct algorithms for solving this problem.


Algorithm 1:   A Cubic Algorithm (i.e., O(n3 ) )

Idea: For all pairs of integers, i ≤ j, check whether the sum of A[i..j] is greater than the maximum sum so far.
maxsofar = 0;
for (i = 0; i < n; i++)
    for (j = i; j < n; j++) {
        sum = 0;
        for (k = i; k <= j; k++)
            sum += A[k];
            if (sum > maxsofar)
                maxsofar = sum;
    }


Algorithm 2:   A Quadratic Algorithm (i.e., O(n2 ) )

Idea: The sum of A[i..j] can be efficiently calculated as (sum of A[i..j-1]) + A[j].
maxsofar = 0;
for (i = 0; i < n; i++) {
    sum = 0;
    for (j = i; j < n; j++) {
        sum += A[j];   // sum is that of A[i..j]
        if (sum > maxsofar)
            maxsofar = sum;
    }
}


Algorithm 2b:   Another Quadratic Algorithm (i.e., O(n2 ) )

Idea: Precalculate cumulative sums A[0..i] for all i. Then you can efficiently compute sum[a..b] = sum[0..b] - sum[0..a-1], when a>0.
maxsofar = 0;
cumarr[-1] = 0;
for (i = 0; i < n; i++)
    cumarr[i] = cumarr[i-1] + A[i];
for (i = 0; i < n; i++) {
    for (j = i; j < n; j++) {
        sum = cumarr[j] - cumarr[i-1];   // sum is that of A[i..j]
        if (sum > maxsofar)
            maxsofar = sum;
    }
}


Algorithm 3:   An O(n log n) Algorithm

Idea: Recursive divide and conquer. Find maximum solution for left half; find maximum solution for right half; find maximum solution which straddles the middle. One of those three will be the true optimal solution.
float recmax(int l, int u)
    if (l > u)  /* zero elements */
        return 0;
    if (l == u)  /* one element */
        return max(0, A[l]);
    m = (l+u) / 2;
    /* find max crossing to left */
    lmax = sum = 0;
    for (i = m; i ≥ l; i--) {
        sum += A[i];
        if (sum > lmax)
            lmax = sum;
    }
    /* find max crossing to right */
    rmax = sum = 0;
    for (i = m+1; i ≤ u; i++) {
        sum += A[i];
        if (sum > rmax)
            rmax = sum;
    }
    return max(max(recmax(l, m),
                   recmax(m+1, u)),    
               lmax + rmax);
}
The toplevel recursion is invoked as recmax(0,n-1).


Algorithm 4:   A Linear Algorithm (i.e., O(n) )

Idea: Do a simple scan, maintaining two values along the way, for index i:
  • "maxhere" : maximum subarray of A[0..i] of those ending precisely at i
  • "maxsofar" : maximum subarray of A[0..i]
  • maxsofar = 0
    maxendinghere = 0;
    for (i = 0; i < n; i++) {
        maxhere = max(maxhere + A[i], 0)
        maxsofar = max(maxsofar, maxhere)
    }
    


    Last modified: Tuesday, 28 August 2012