Python Arena
Step 5 of 6

Prefix sums

A step counter keeps how many steps you walked each day. How many steps from day 3 to day 10? sum(steps[3:11]) answers in O(k) for a range of k days. That's fine once. But an app that answers 100,000 such questions about 100,000 days adds up billions of numbers, the same ones again and again.

Prefix sums

Do the adding once. A prefix sum list holds, at index i, the sum of the first i values:

values:     5   2   7   1   4
prefix:  0  5   7  14  15  19

prefix[0] is 0, the sum of no values, so the list is one longer than values. The sum of values[start:end] is then prefix[end] - prefix[start]: prefix[end] adds up everything before end, prefix[start] everything before start, and taking one from the other leaves exactly the values from start to end - 1. One subtraction, O(1), for any range:

def make_prefix(values):
    prefix = [0]
    for value in values:
        prefix.append(prefix[-1] + value)
    return prefix


values = [5, 2, 7, 1, 4]
prefix = make_prefix(values)
print(prefix)
print(prefix[4] - prefix[1], sum(values[1:4]))
print(prefix[5] - prefix[0], sum(values))
Output
[0, 5, 7, 14, 15, 19]
10 10
19 19

Building it is O(n) time and O(n) memory, once. After that, every range sum is O(1). The start and end work like a slice's, so they're easy to remember. The common mistake is to leave out the 0 at the front: then prefix[i] includes values[i], every index is off by one, and a range that starts at 0 needs a special case.

itertools.accumulate

The standard library makes the running totals for you, in C:

In the Python interpreter
>>> from itertools import accumulate
>>> list(accumulate([5, 2, 7, 1, 4]))
[5, 7, 14, 15, 19]
>>> list(accumulate([5, 2, 7, 1, 4], initial=0))
[0, 5, 7, 14, 15, 19]

Without initial=0 you get the totals without the 0 at the front; with it, the prefix list above.

How many stretches add up to k?

A harder question: how many stretches of values in a row (subarrays) add up to exactly k? Trying every start and end is O(n²). Prefix sums turn it into lookups. The stretch from start to end adds up to k when prefix[end] - prefix[start] == k, that is, when prefix[start] == prefix[end] - k. So walk along the list keeping the running total, and at each value count how many earlier prefix sums were total - k. A dict of counts answers that in O(1):

def count_stretches(values, k):
    seen = {0: 1}
    total = 0
    count = 0
    for value in values:
        total += value
        if total - k in seen:
            count += seen[total - k]
        if total not in seen:
            seen[total] = 0
        seen[total] += 1
    return count


print(count_stretches([3, 4, -2, 5, 1, -4, 6], 7))
Output
3

The three stretches are 3, 4, then 4, -2, 5, and then 3, 4, -2, 5, 1, -4. seen maps each prefix sum to how many times it has come up so far, and it starts as {0: 1}: that's the empty prefix before the first value, so stretches that start at index 0 are counted too. Each value costs O(1) on average, so the function is O(n) time and O(n) memory. It works with negative values as well, which the sliding windows of the next step can't handle.

The biggest sum of a stretch

Another classic: which stretch of values in a row has the biggest sum, when values can be negative? The sum of a stretch is prefix[end] - prefix[start], so this is much like best_profit from the first step, run on the prefix sums: remember the smallest prefix sum so far. Only much like, though: best_profit never gives less than 0, as if you could always buy and sell on the same day. On prefix sums that's a stretch of no values at all, so it gives 0 when every value is negative. The biggest sum is usually found another way, known as Kadane's algorithm: keep the best sum of a stretch that ends at the current value. That stretch either starts fresh at the value or adds the value to the best stretch that ended just before it, whichever is bigger. For a list that isn't empty:

def biggest_stretch(values):
    best = values[0]
    ending = 0
    for value in values:
        ending = max(value, ending + value)
        best = max(best, ending)
    return best


print(biggest_stretch([3, -5, 4, -1, 2, 1, -6, 2]))
print(biggest_stretch([-4, -2, -7]))
Output
6
-2

The best stretch is 4, -1, 2, 1. At the 4, the best stretch ending just before it adds up to -2, so starting fresh is better, and the 3 and the -5 are left behind. When every value is negative, the best stretch is the biggest value on its own, -2, which is why best starts at the first value and not at 0. One pass, O(n) time and O(1) memory.

Your task

Challenge 1 of 3

minutes holds how many minutes you practiced the piano each day, and practiced(first_day, last_day) should give the minutes from first_day to last_day, both days included. Fill in the blank with the right index of prefix. The program should print:

75
140
20

Challenge 2

Write range_sums(values, queries). Each query is a tuple (start, end), and its answer is the sum of values[start:end]. Return a list of the answers, in the order of the queries. The hidden test has 200,000 values and 100,000 queries: build the prefix sums once and answer each query in O(1).

For example, range_sums([5, 2, 7, 1, 4], [(1, 4), (0, 5), (2, 3), (3, 3)]) returns:

[10, 19, 7, 0]

Challenge 3

Write longest_stretch(values, k): the length of the longest stretch of values in a row that adds up to exactly k, or 0 if there is none. Values can be negative. The hidden test has 200,000 values, so trying every start and end is too slow.

For example, longest_stretch([3, 4, -2, 5, 1, -4, 6], 7) returns (3, 4, -2, 5, 1, -4):

6