""" Five algorithmic solutions to the maximum subarray problem (see Bentley's Programming Pearls). Author: Michael Goldwasser Module can be executed as a test harness. Use -h flag for documentation on usage. """ #----------------------------------------------------------------------- def algorithm1(A): """A brute-force algorithm using cubic-time. This algorithm tries every possible i < j pair, and computes the sum of entries in A[i:j]. Returns tuple designating optimal (maxsum, start, stop). """ n = len(A) maxsofar = (0, 0, 0) # (t, start, stop) with sum(A[start:stop]) = t for i in range(n): # potential start index for j in range(i+1, n+1): # potential stop index total = 0 for k in range(i,j): # compute sum(A[i:j]) total += A[k] if total > maxsofar[0]: # if new best, record parameters maxsofar = (total, i, j) return maxsofar #----------------------------------------------------------------------- def algorithm2a(A): """A quadratic time approach to brute force by using previous sums. Uses fact that sum(A[i:j]) = sum(A[i:j-1]) + A[j-1] to avoid a third nested loop. Returns tuple designating optimal (maxsum, start, stop). """ n = len(A) maxsofar = (0, 0, 0) # (t, start, stop) with sum(A[start:stop]) = t for i in range(n): # potential start index total = 0 # maintain sum(A[i:j]) as j increases for j in range(i+1, n+1): total += A[j-1] # thus total is now sum(A[i:j]) if total > maxsofar[0]: maxsofar = (total, i, j) return maxsofar #----------------------------------------------------------------------- def algorithm2b(A): """A quadratic time approach to brute force using prefix sums. precomputes sum(A[0:j]) for each j. That allows for O(1) time computation of sum(A[i:j]) = sum(A[0:j]) - sum(A[0:i]) Returns tuple designating optimal (maxsum, start, stop). """ n = len(A) cumulative = [0] * (n+1) # cumulative[i] = sum(A[0:i]) for i in range(n): cumulative[i+1] = cumulative[i] + A[i] maxsofar = (0, 0, 0) # (t, start, stop) with sum(A[start:stop]) = t for i in range(n): # potential start index for j in range(i+1, n+1): total = cumulative[j] - cumulative[i] if total > maxsofar[0]: maxsofar = (total, i, j) return maxsofar #----------------------------------------------------------------------- def recurse(A, start, stop): """Recursion for algorithm3 that only considers implicit slice A[start:stop]).""" if stop == start: # zero elements return (0, 0, 0) elif stop == start + 1: # one element if A[start] > 0: return (A[start], start, stop) else: return (0, start, start) else: # two or more elements mid = (start + stop) // 2 # find maximum sum(A[i:mid]) for i < mid total = 0 lmax = (0, mid) # (t,i) such that sum(A[i:mid]) = t for i in range(mid-1, start-1, -1): total += A[i] if total > lmax[0]: lmax = (total,i) # find maximum sum(A[mid:j]) for j > mid total = 0 rmax = (0, mid) # (t, j) such that sum(A[mid:j]) = t for j in range(mid+1, stop+1): total += A[j-1] if total > rmax[0]: rmax = (total,j) overlay = (lmax[0]+rmax[0], lmax[1], rmax[1]) return max(recurse(A, start, mid), recurse(A, mid, stop), overlay) def algorithm3(A): """A divide-and-conquer approach achieving O(n log n) time. We find the maximum solution from the left half, the maximum from the right, and the maximum solution that straddles the middle. One of those three is the true optimal solution. Returns tuple designating optimal (maxsum, start, stop). """ return recurse(A, 0, len(A)) #----------------------------------------------------------------------- def algorithm4(A): """A linear algoirthm based on a simple greedy strategy. As we let index i increase, we keep track of two pieces of information: * maxhere which is maximum subarray ending specifically at index i * maxsofar which is maximum subarray for anything from A[0:i] Returns tuple designating optimal (maxsum, start, stop). """ n = len(A) maxsofar = (0, 0, 0) # (t, start, stop) such that sum(A[start,stop]) = t maxhere = (0, 0) # (t, start) such that sum(A[start:i]) = t for i in range(1, n+1): if maxhere[0] + A[i-1] > 0: maxhere = (maxhere[0] + A[i-1], maxhere[1]) else: maxhere = (0, i) if maxhere[0] > maxsofar[0]: maxsofar = (maxhere[0], maxhere[1], i) return maxsofar #----------------------------------------------------------------------- if __name__ == '__main__': from optparse import OptionParser import sys import time import random def quit(message=None): if message: print(message) sys.exit(1) parser = OptionParser(usage='usage: %prog [options]') parser.add_option('-n', dest='size', type='int', default=512, help='number of elements in data set [default: %default]') parser.add_option('-a', dest='num_alg', metavar='NUM', type='int', default=5, help='number of algorithms to test [default: %default]') parser.add_option('-s', dest='seed', type='int', default=None, help='random seed for data set [default: %default]') (options,args) = parser.parse_args() if options.size <= 0: quit('n must be positive') if not 1 <= options.num_alg <= 5: quit('invalid number of algorithms to test') if options.seed is None: options.seed = random.randrange(1000000) # print('Using seed: {0}'.format(options.seed)) random.seed(options.seed) data = [random.uniform(-100,100) for _ in range(options.size)] algorithms = (algorithm1, algorithm2a, algorithm2b, algorithm3, algorithm4) print('Running tests for {0} algorithms using n={1} and seed={2}'.format(options.num_alg,options.size,options.seed)) for alg in reversed(algorithms[len(algorithms)-options.num_alg:]): start = time.time() answer = alg(data) end = time.time() print("{0:<11} found sum(data[{1}:{2}])={3:.2f} using {4:.3f} seconds of computation".format( alg.__name__, answer[1], answer[2], answer[0], (end-start)))