Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 6 – Prefix sums and difference arrays

Prefix sums for instant range totals

Build a prefix-sum array with a 0 in front, answer any range total with one subtraction, keep large totals exact, and spot prefix sums inside other problems.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Build a prefix array with a leading zero to avoid off-by-one errors
  • Answer range-sum queries in O(1) after one pass of preparation
  • Recognise prefix sums inside other problems, such as finding a balance point

Before you start

A prefix sum array holds the running totals of a list, with a 0 in front: prefix[0] = 0 and prefix[i + 1] = prefix[i] + a[i], so prefix[i] is the total of the first i items. The total of any stretch, from item first to item last, is then one subtraction, prefix[last + 1] - prefix[first], however long the stretch is. Building the array takes one pass over the list, O(n), and every range total after that costs O(1). Reach for it when a list that does not change is asked many questions of the form “what is the total from here to there?”. This lesson builds the array, solves one problem completely (the brute force, the fast version, why it is right, a dry run, code that runs in four languages, a stress test and the overflow trap), then finds the same idea inside a problem that never mentions ranges.

Running totals, with a 0 in front

A small shop writes down what it took each day. Here are 14 days of its ledger, with the prefix list built by a loop and again by itertools.accumulate, and four questions about it:

Running totals of a 14-day ledger Python · ledger/ledger.py
from itertools import accumulate

# What a small shop took on each of 14 days, day 0 first.
sales = [420, 380, 0, 515, 610, 290, 700, 455, 390, 660, 0, 530, 480, 625]

# prefix[i] is the total of the first i days: prefix[0] = 0, and prefix has one entry more than sales.
prefix = [0]
for amount in sales:
    prefix.append(prefix[-1] + amount)

print("sales: ", sales)
print("prefix:", prefix)
print("the same list from accumulate:", list(accumulate(sales, initial=0)) == prefix)


def total(first, last):
    """The sales from day `first` to day `last`, both days included."""
    return prefix[last + 1] - prefix[first]


for first, last in [(2, 4), (7, 13), (0, 10), (3, 3)]:
    print(f"days {first} to {last}: {total(first, last)}")

# Without the 0 in front, running[i] covers days 0 to i, and a range starting on day 0 needs a special case.
# Forget it, and running[first - 1] is running[-1]: Python reads the last entry instead of failing.
running = list(accumulate(sales))
print("days 0 to 2 without the 0 in front:", running[2] - running[0 - 1])

Output

sales:  [420, 380, 0, 515, 610, 290, 700, 455, 390, 660, 0, 530, 480, 625]
prefix: [0, 420, 800, 800, 1315, 1925, 2215, 2915, 3370, 3760, 4420, 4420, 4950, 5430, 6055]
the same list from accumulate: True
days 2 to 4: 1125
days 7 to 13: 3140
days 0 to 10: 4420
days 3 to 3: 515
days 0 to 2 without the 0 in front: -5255

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ledger.py

The prefix list has 15 entries for 14 days. prefix[5] is 1,925, the total of the first five days, days 0 to 4, and prefix[2] is 800, the total of days 0 and 1. Their difference is what days 2 to 4 add on top of the first two days, and that is the answer to the first question:

prefix[5], the first five days, splits into prefix[2] and days 2 to 4, so days 2 to 4 total prefix[5] − prefix[2] = 1,125.prefix[5] = 1,925the first five daysdays 2 to 4 = prefix[5] − prefix[2]= 1,925 − 800 = 1,125prefix[2] = 800days 0 and 1420 + 380days 2 to 40 + 515 + 610= 1,125what is leftonce prefix[2]is taken away

One subtraction gives the total of a range

Text description of the diagram

The diagram shows one large box with two smaller boxes inside it, and an arrow to a result below.

  1. The large box is prefix[5] = 1,925, the total of the first five days, days 0 to 4.
  2. Inside it, on the left, is prefix[2] = 800, the total of days 0 and 1 (420 + 380).
  3. Inside it, on the right, are days 2 to 4: 0 + 515 + 610 = 1,125.
  4. An arrow from the right-hand box leads to the result: what is left once prefix[2] is taken away. Days 2 to 4 total prefix[5] − prefix[2] = 1,925 − 800 = 1,125.

The same subtraction answers every question, from the second week to a single day. “Month to date on day 10” is the range from day 0 to day 10, and it needs no special case: prefix[0] is 0, so nothing is taken away.

accumulate(sales, initial=0) builds the same list in one call. The initial argument arrived in Python 3.8, and the documentation says that with it the output has one more element than the input, which is exactly the 0 in front. Without it, accumulate gives the totals of days 0 to i, and a range that starts on day 0 needs an if. The last line of the output shows what happens when that if is forgotten: running[0 - 1] is running[-1], which Python reads as the last entry of the list, so the program prints a negative total and raises no error.

A worked problem: totals for many ranges

The problem below is the one this module’s pattern was made for. Here it is worked in full, in the order a coding round asks for: the brute force, what to notice, the fast version, why it is correct, a dry run, the cost, code, a stress test, edge cases and follow-up questions.

The problem

A shop’s ledger lists what it took on each day, sales[0] to sales[n - 1]. Its accounts page asks many questions of one kind: what were the total sales from day first to day last, both days included? Write range_totals(sales, queries), which returns the answer to every query, in order. There are 1 to 200,000 days and 1 to 200,000 queries, each day’s sales are a whole number from 0 to 10⁹, and every query has 0 ≤ first ≤ last ≤ n − 1.

For the first week of the ledger above, sales = [420, 380, 0, 515, 610, 290, 700], and the queries (2, 4), (0, 6), (3, 3), (0, 0) and (6, 6) have the answers 1,125, 2,915, 515, 420 and 700.

Brute force first

Add up the days of each query again:

The brute force: add up every range again Python · range_totals/brute.py
def range_totals_brute(sales, queries):
    """Adds up the days of every query again: up to n additions per query."""
    answers = []
    for first, last in queries:
        total = 0
        for day in range(first, last + 1):
            total += sales[day]
        answers.append(total)
    return answers


if __name__ == "__main__":
    sales = [420, 380, 0, 515, 610, 290, 700]
    print(range_totals_brute(sales, [(2, 4), (0, 6), (3, 3), (0, 0), (6, 6)]))

Output

[1125, 2915, 515, 420, 700]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute.py

It is correct by construction, and it is slow. A query can cover the whole ledger, so each one costs up to n additions and all of them cost O(n × q). At the limits that is up to 200,000 × 200,000 = 4 × 10¹⁰ additions. Even compiled code, budgeted at 10⁸ simple steps a second as in the lesson on reading constraints, would need about 400 seconds, against a time limit of a second or two. Keep the brute force anyway: it checks the fast version below.

What to notice

The ledger never changes between queries, yet the brute force adds the same days again and again: every query that covers day 3 adds sales[3] once more. That is the cue for prefix sums. Look for three things together: many queries, each asking for a total (or a count) over a contiguous range, on data that stays the same while the queries run. Do the shared work once, as running totals, and each query becomes a lookup.

The fast version, step by step

  1. Make a list prefix of n + 1 zeros.
  2. In one pass over the days, set prefix[day + 1] = prefix[day] + sales[day].
  3. Answer each query (first, last) with prefix[last + 1] - prefix[first].

Here it is in Python and JavaScript, which you can run on this page, and in Java and C++:

The prefix-sum solution in four languages

Python · range_totals/range_totals.py

def range_totals(sales, queries):
    """Builds the prefix totals once, in one pass, then answers each query with one subtraction."""
    prefix = [0] * (len(sales) + 1)  # prefix[0] stays 0
    for day, amount in enumerate(sales):
        prefix[day + 1] = prefix[day] + amount
    return [prefix[last + 1] - prefix[first] for first, last in queries]


if __name__ == "__main__":
    sales = [420, 380, 0, 515, 610, 290, 700]
    print(range_totals(sales, [(2, 4), (0, 6), (3, 3), (0, 0), (6, 6)]))

    # The largest ledger the limits allow: 200,000 days of 1,000,000,000 each.
    big = [1_000_000_000] * 200_000
    print(range_totals(big, [(0, 199_999)])[0])

Output

[1125, 2915, 515, 420, 700]
200000000000000

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 range_totals.py

JavaScript · range_totals/range_totals.mjs

// Builds the prefix totals once, in one pass, then answers each query with one subtraction.
function rangeTotals(sales, queries) {
  const prefix = new Array(sales.length + 1).fill(0);
  for (let day = 0; day < sales.length; day++) prefix[day + 1] = prefix[day] + sales[day];
  return queries.map(([first, last]) => prefix[last + 1] - prefix[first]);
}

const sales = [420, 380, 0, 515, 610, 290, 700];
console.log(JSON.stringify(rangeTotals(sales, [[2, 4], [0, 6], [3, 3], [0, 0], [6, 6]])));

// The largest total the limits allow, 2 × 10^14, is still an exact JavaScript number (below 2^53 - 1).
const big = new Array(200_000).fill(1_000_000_000);
const total = rangeTotals(big, [[0, 199_999]])[0];
console.log(`${total} is a safe integer: ${Number.isSafeInteger(total)}`);

Output

[1125,2915,515,420,700]
200000000000000 is a safe integer: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node range_totals.mjs

Java · compiled/RangeTotals.java

import java.util.Arrays;

public class RangeTotals {
    /** Builds the prefix totals once, in one pass, then answers each query with one subtraction. */
    static long[] rangeTotals(long[] sales, int[][] queries) {
        long[] prefix = new long[sales.length + 1]; // Java fills it with zeros, so prefix[0] = 0
        for (int day = 0; day < sales.length; day++) {
            prefix[day + 1] = prefix[day] + sales[day];
        }
        long[] answers = new long[queries.length];
        for (int q = 0; q < queries.length; q++) {
            int first = queries[q][0], last = queries[q][1];
            answers[q] = prefix[last + 1] - prefix[first];
        }
        return answers;
    }

    public static void main(String[] args) {
        long[] sales = {420, 380, 0, 515, 610, 290, 700};
        int[][] queries = {{2, 4}, {0, 6}, {3, 3}, {0, 0}, {6, 6}};
        System.out.println(Arrays.toString(rangeTotals(sales, queries)));

        // The largest ledger the limits allow: 200,000 days of 1,000,000,000 each.
        long[] big = new long[200_000];
        Arrays.fill(big, 1_000_000_000L);
        System.out.println(rangeTotals(big, new int[][] {{0, 199_999}})[0]);
    }
}

C++ · compiled/range_totals.cpp

#include <iostream>
#include <numeric>
#include <utility>
#include <vector>

// Builds the prefix totals once, in one pass, then answers each query with one subtraction.
std::vector<long long> range_totals(const std::vector<long long>& sales,
                                    const std::vector<std::pair<int, int>>& queries) {
    std::vector<long long> prefix(sales.size() + 1, 0);  // prefix[0] stays 0
    std::partial_sum(sales.begin(), sales.end(), prefix.begin() + 1);
    std::vector<long long> answers;
    answers.reserve(queries.size());
    for (auto [first, last] : queries) answers.push_back(prefix[last + 1] - prefix[first]);
    return answers;
}

int main() {
    std::vector<long long> sales{420, 380, 0, 515, 610, 290, 700};
    std::vector<std::pair<int, int>> queries{{2, 4}, {0, 6}, {3, 3}, {0, 0}, {6, 6}};
    const char* gap = "";
    for (long long total : range_totals(sales, queries)) {
        std::cout << gap << total;
        gap = " ";
    }
    std::cout << '\n';

    // The largest ledger the limits allow: 200,000 days of 1,000,000,000 each.
    std::vector<long long> big(200'000, 1'000'000'000);
    std::cout << range_totals(big, {{0, 199'999}})[0] << '\n';
}

The Java and C++ versions, compiled and run:

Java and C++, compiled and run Python · compiled/solutions_session.py
from session import run

run("""
g++-16 --version | head -n 1
g++-16 -std=c++23 -O2 -Wall -Wextra range_totals.cpp -o range_totals && ./range_totals
java -version 2>&1 | head -n 1 | cut -d ' ' -f 1-3
java RangeTotals.java
""")

Output

$ g++-16 --version | head -n 1
g++-16 (Homebrew GCC 16.2.0) 16.2.0
$ g++-16 -std=c++23 -O2 -Wall -Wextra range_totals.cpp -o range_totals && ./range_totals
1125 2915 515 420 700
200000000000000
$ java -version 2>&1 | head -n 1 | cut -d ' ' -f 1-3
openjdk version "27"
$ java RangeTotals.java
[1125, 2915, 515, 420, 700]
200000000000000

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 solutions_session.py

All four print the same five answers, then the total of the largest ledger the limits allow, 200,000 days of 10⁹, which is 2 × 10¹⁴. Java and C++ keep the prefix totals in 64-bit integers (long and long long); the section on overflow below shows why. The C++ version builds the list with std::partial_sum from <numeric>, writing from prefix.begin() + 1 so that prefix[0] stays 0. In Go, the same loop needs int64 for the totals: the language specification allows int to be 32 or 64 bits wide.

Why it is correct

The loop keeps one fact true, its invariant: after it has handled day d, prefix[d + 1] is the total of days 0 to d. It holds at the start, where prefix[0] is the total of no days at all, 0, and each step adds exactly the next day. So once the loop ends, prefix[i] is the total of the first i days for every i from 0 to n.

A query then subtracts two such totals. prefix[last + 1] is the total of days 0 to last, and prefix[first] is the total of days 0 to first − 1. The days before first are in both, so they cancel, and what is left is the total of days first to last, which is the answer. When first is 0, nothing is taken away, and the formula still holds.

A dry run

For the first week, prefix is [0, 420, 800, 800, 1315, 1925, 2215, 2915], and each query reads two entries:

  • (2, 4): prefix[5] - prefix[2] = 1,925 − 800 = 1,125
  • (0, 6): prefix[7] - prefix[0] = 2,915 − 0 = 2,915
  • (3, 3): prefix[4] - prefix[3] = 1,315 − 800 = 515
  • (0, 0): prefix[1] - prefix[0] = 420 − 0 = 420
  • (6, 6): prefix[7] - prefix[6] = 2,915 − 2,215 = 700

Day 2 had no sales, so prefix[2] and prefix[3] are equal, and the query (3, 3) returns sales[3] alone.

The cost

Answering q range totals over n days
Operation Time Extra space
Brute force: add up each range again O(n × q) O(1)
Prefix sums: build once, then subtract O(n + q) O(n): n + 1 numbers

At the limits, the prefix version does 200,000 additions and 200,000 subtractions instead of up to 4 × 10¹⁰ additions. The price is memory: n + 1 extra numbers, 1.6 MB as 64-bit integers for 200,000 days.

In a live round

Say the plan in two sentences before you code it: “Adding up each range again is O(n × q), up to 4 × 10¹⁰ steps here. The ledger doesn’t change, so I’ll build running totals once with a 0 in front, and each query becomes one subtraction: O(n + q) time and O(n) extra memory.”

Check it against the brute force

A stress test, as in the lesson on brute force to optimal, compares the fast version with the brute force on many small random ledgers, the shortest first. It also tries a version with a plausible slip, prefix[last] - prefix[first]:

1,000 random ledgers against the brute force Python · range_totals/stress.py
import random

from brute import range_totals_brute
from range_totals import range_totals


def range_totals_short(sales, queries):
    """A plausible slip: prefix[last] - prefix[first] stops one day early."""
    prefix = [0]
    for amount in sales:
        prefix.append(prefix[-1] + amount)
    return [prefix[last] - prefix[first] for first, last in queries]


def first_failure(candidate, cases=1_000, seed=6):
    """Runs candidate and the brute force on small random ledgers, shortest first, until they disagree."""
    rng = random.Random(seed)
    for case in range(cases):
        days = 1 + case * 8 // cases  # 1 to 8 days
        sales = [rng.randint(0, 9) for _ in range(days)]
        queries = []
        for _ in range(3):
            first = rng.randrange(days)
            queries.append((first, rng.randrange(first, days)))
        got, expected = candidate(sales, queries), range_totals_brute(sales, queries)
        if got != expected:
            return f"fails on sales {sales}, queries {queries}: got {got}, expected {expected}"
    return f"agrees with the brute force on {cases:,} random ledgers"


print("range_totals:      ", first_failure(range_totals))
print("range_totals_short:", first_failure(range_totals_short))

Output

range_totals:       agrees with the brute force on 1,000 random ledgers
range_totals_short: fails on sales [9], queries [(0, 0), (0, 0), (0, 0)]: got [0, 0, 0], expected [9, 9, 9]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stress.py

The correct version agrees on all 1,000 ledgers. The slip fails on the first ledger with a day that sold something: a single day of 9 and the query (0, 0). It returns 0, because prefix[last] - prefix[first] is the total of days first to last − 1, which stops one day early, and for a one-day range that leaves no day at all.

Edge cases and common bugs

  • A range starting on day 0 needs no special case, thanks to the 0 in front.
  • A range of one day, first == last, returns prefix[first + 1] - prefix[first], which is sales[first].
  • Days numbered from 1 in the input: convert each query once, or use prefix[last] - prefix[first - 1]. Settle this in the clarifying step, before you write the formula.
  • Negative values, such as refunds, change nothing: prefix sums never needed the numbers to be positive.
  • A ledger that changes after the prefix list is built makes every later prefix out of date. Prefix sums suit data that stays the same while the queries run.
  • Totals that outgrow their type, the bug that tests with small numbers never catch. It has its own section below.

Variants

The same list answers more than totals:

  • Month to date is the range from the first day of the month to today.
  • An average over a range is the range total divided by last - first + 1, the number of days.
  • A count of days that match a condition is a prefix sum of ones and zeros, one for each day that matches. The exercise below counts days with no sales this way.
  • Any operation that can be undone works like addition. XOR is one: x ^ x is 0, so a range’s XOR is prefix[last + 1] ^ prefix[first]. A minimum or a maximum cannot be undone, so range minimums need a different structure, which a later module builds.

Follow-up questions

Can you answer the queries with O(1) extra memory? Yes, by turning sales itself into running totals (sales[day] += sales[day - 1] from day 1 on). A range is then sales[last], minus sales[first - 1] when first is above 0. You lose the original figures, and the special case for day 0 comes back.

The shop adds a new day to the ledger. Do you rebuild everything? No: append prefix[-1] + new_day to the prefix list. It costs O(1), and the old prefixes stay correct, because a new day only ever comes after them.

When the total does not fit

The largest number the prefix list holds is its last entry, the total of the whole ledger: up to 200,000 × 10⁹ = 2 × 10¹⁴. The largest Java int is 2,147,483,647 (section 4.2.1 of the Java Language Specification), and the specification adds that integer operators give no sign of an overflow (section 4.2.2) and that an addition never throws an exception (section 15.18.2). Here is the largest ledger the problem allows, added up into an int[] and into a long[]:

The same ledger in int and long prefix totals Python · compiled/overflow_session.py
from session import run

run("""
java Overflow.java
""")

Output

$ java Overflow.java
int prefix,  days 0 to 199999: 552894464
long prefix, days 0 to 199999: 200000000000000

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 overflow_session.py

The int version prints 552,894,464: the true total minus 46,566 × 2³², a positive number that looks plausible. The fix is a 64-bit type for the prefix list and the answers: a Java long, a C++ long long (or std::int64_t), a Go int64. Java can keep each day’s sales in an int, as Overflow.java does, because each day fits and Java widens it when it is added to a long. The C++ version above stores the sales as long long as well, so that std::partial_sum adds in 64 bits. In JavaScript every whole number up to 2⁵³ − 1, about 9 × 10¹⁵, is exact, so 2 × 10¹⁴ is safe, which the JavaScript version above checks with Number.isSafeInteger. Python’s integers have unlimited precision. The lesson on edge cases compares how each of five languages behaves when a sum overflows.

Ledgers kept in paise

Amounts are often stored in paise, as whole numbers, to avoid rounding errors. A 32-bit int holds at most 2,147,483,647 paise, which is ₹2,14,74,836.47, a little over ₹2.14 crore. A shop that takes ₹10 lakh a day passes that in 22 days, so a month-to-date total in paise needs a 64-bit integer.

Big Number Calculator Multiply your own limits, such as 200,000 × 10⁹, exactly, and compare the result with 2,147,483,647.

Prefix sums inside other problems: the balance day

Prefix sums often appear in problems that never say “range”. Take this one: find the first day on which the sales of all the days before it equal the sales of all the days after it, the day itself counting on neither side, or return −1 if there is no such day.

The brute force adds up both sides again for every day, which is O(n²). Now notice what the two sides are. The days before day d total prefix[d], and the days after it total the whole ledger minus prefix[d] minus sales[d]. So one running total of the days so far, together with the total of the whole ledger, checks each day in O(1):

The balance day, brute force and one pass Python · balance/balance_day.py
import random


def balance_day_brute(sales):
    """Adds up both sides again for every day: O(n²)."""
    for day in range(len(sales)):
        if sum(sales[:day]) == sum(sales[day + 1:]):
            return day
    return -1


def balance_day(sales):
    """One running total: the days after `day` hold the whole total minus the days before it minus `day` itself."""
    whole = sum(sales)
    before = 0
    for day, amount in enumerate(sales):
        if before == whole - before - amount:
            return day
        before += amount
    return -1


for sales in ([3, 8, 2, 6, 5], [4, -1, 9, 3], [0, 6, -6], [5, 5], [7]):
    print(sales, balance_day_brute(sales), balance_day(sales))

rng = random.Random(4)
ledgers = [[rng.randint(-3, 3) for _ in range(rng.randint(1, 7))] for _ in range(2_000)]
agree = sum(balance_day(s) == balance_day_brute(s) for s in ledgers)
print(f"same answer on {agree:,} of {len(ledgers):,} random ledgers")

Output

[3, 8, 2, 6, 5] 2 2
[4, -1, 9, 3] 2 2
[0, 6, -6] 0 0
[5, 5] -1 -1
[7] 0 0
same answer on 2,000 of 2,000 random ledgers

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 balance_day.py

Both versions agree on the five ledgers and on 2,000 random ones. The cases worth saying aloud in a round: the first day can balance, because the side before it is empty and totals 0 ([0, 6, -6]); a ledger of one day balances on that day; negative values are fine ([4, -1, 9, 3]); and a ledger with no balance day returns −1 ([5, 5]). This version keeps a single running total instead of a list, because it reads the prefixes in order and never needs one twice.

The signal is a condition that compares what comes before a position with what comes after it: “left and right”, “before and after”, “every way to split”. Running totals from the left, and the whole total, usually make each check O(1).

Prefix sums in each language

How each language builds the prefix list, and the type its totals need:

  • Python: accumulate(sales, initial=0) or a loop; an int has no upper limit.
  • JavaScript: a loop; a number is exact up to 2⁵³ − 1, and BigInt goes beyond it.
  • Java: a loop, or Arrays.parallelPrefix; totals in long.
  • C++: std::partial_sum; long long for the totals and for the sales.
  • Go: a loop; totals in int64.

Two library calls need a word of care. Arrays.parallelPrefix(a, Long::sum) turns a Java array into its own running totals, in place, so the result has no 0 in front. std::partial_sum writes wherever its third argument points, which is why the C++ version above passes prefix.begin() + 1 and leaves prefix[0] at 0.

Python Online Compiler Paste a ledger of your own into range_totals and check a few totals by hand.

Exercise

Exercise · Easy · Python

Totals and quiet days for many ranges

The shop's accounts page now shows two numbers for every range of days: the total sales, and the number of quiet days, the days in the range on which nothing was sold. Write range_report(sales, queries) in report.py.

  • sales[i] is what the shop took on day i, a whole number from 0 to 10⁹.
  • Each query is a pair (first, last) with 0 <= first <= last < len(sales): the days first to last, both included.
  • Return a list with one pair (total, quiet_days) for each query, in the order of the queries.
range_report([420, 0, 515, 0, 0, 290], [(0, 2), (1, 4), (5, 5)])
# [(935, 1), (515, 3), (290, 0)]

There can be up to 200,000 days and 200,000 queries, so adding up each range again is too slow. The sample tests check the example, single days, the whole ledger, a ledger of quiet days only and the largest values. The last test gives your function 20,000 days and 20,000 queries, counts how many values it reads from sales, and stops it once it has read more than four times the number of days.

Starter code · report.py

def range_report(sales, queries):
    """For each query (first, last): the total sales of those days and how many of them had no sales."""
    # Replace this line with your code.
    return []
The sample tests · test_report.py
import random

from report import range_report


def trusted(sales, queries):
    """Answers the tests trust: add up each range again, and count its zeros."""
    return [(sum(sales[first:last + 1]), sales[first:last + 1].count(0)) for first, last in queries]


def as_pairs(result):
    """Accept tuples or lists for the pairs."""
    return [tuple(pair) for pair in result]


class Ledger(list):
    """A list of sales that counts the values read from it, and stops a solution that reads too many."""

    def __init__(self, values, limit):
        super().__init__(values)
        self.reads = 0
        self.limit = limit

    def _read(self, count):
        self.reads += count
        if self.reads > self.limit:
            raise AssertionError(
                f"too slow: more than {self.limit:,} values were read from a ledger of {len(self):,} days; "
                "build the prefix lists once and answer each query without reading the ledger again"
            )

    def __getitem__(self, index):
        self._read(len(range(*index.indices(len(self)))) if isinstance(index, slice) else 1)
        return super().__getitem__(index)

    def __iter__(self):
        for day in range(len(self)):
            self._read(1)
            yield super().__getitem__(day)


def test_prompt_example():
    """the example from the prompt"""
    assert as_pairs(range_report([420, 0, 515, 0, 0, 290], [(0, 2), (1, 4), (5, 5)])) == [(935, 1), (515, 3), (290, 0)]


def test_single_days():
    """ranges of one day, first == last, quiet and not"""
    sales = [7, 0, 12]
    assert as_pairs(range_report(sales, [(0, 0), (1, 1), (2, 2)])) == [(7, 0), (0, 1), (12, 0)]


def test_whole_ledger():
    """a range from the first day to the last"""
    sales = [5, 0, 9, 0, 3]
    assert as_pairs(range_report(sales, [(0, 4)])) == [(17, 2)]


def test_only_quiet_days():
    """a ledger in which nothing was sold"""
    assert as_pairs(range_report([0, 0, 0, 0], [(0, 3), (1, 2), (3, 3)])) == [(0, 4), (0, 2), (0, 1)]


def test_largest_values():
    """200,000 days of 10^9, with one quiet day: totals far above 2^31"""
    sales = [1_000_000_000] * 200_000
    sales[150_000] = 0
    queries = [(0, 199_999), (100_000, 199_999), (150_000, 150_000)]
    assert as_pairs(range_report(sales, queries)) == [(199_999_000_000_000, 1), (99_999_000_000_000, 1), (0, 1)]


def test_random_small_ledgers():
    """300 random ledgers of up to 10 days, against answers the tests trust"""
    rng = random.Random(8)
    for _ in range(300):
        days = rng.randint(1, 10)
        sales = [rng.choice([0, 0, 1, 5, 40]) for _ in range(days)]
        queries = []
        for _ in range(4):
            first = rng.randrange(days)
            queries.append((first, rng.randrange(first, days)))
        assert as_pairs(range_report(sales, queries)) == trusted(sales, queries), f"range_report({sales}, {queries})"


def test_many_long_queries():
    """20,000 days and 20,000 long queries, reading at most four values per day"""
    rng = random.Random(3)
    days = 20_000
    values = [rng.choice([0, rng.randint(1, 1_000_000_000)]) for _ in range(days)]
    queries = [(rng.randrange(10), rng.randrange(days - 10, days)) for _ in range(20_000)]
    prefix, quiet = [0], [0]
    for amount in values:
        prefix.append(prefix[-1] + amount)
        quiet.append(quiet[-1] + (amount == 0))
    expected = [(prefix[last + 1] - prefix[first], quiet[last + 1] - quiet[first]) for first, last in queries]
    assert as_pairs(range_report(Ledger(values, limit=4 * days), queries)) == expected
A hint

A quiet day is a day whose sales are 0. Count it as 1 and every other day as 0: the number of quiet days in a range is then the total of those ones and zeros over the range. One pass over sales can build two prefix lists with a 0 in front, one of the sales and one of the quiet days.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 sales = [4, 5, 0, 6], so prefix = [0, 4, 9, 9, 15]. What is the total of days 1 to 3, both included?

    Type a number.

    Show the answer to question 1

    Answer: 11

    The total of days 1 to 3 is prefix[3 + 1] − prefix[1] = 15 − 4 = 11, which is 5 + 0 + 6. prefix[4] holds days 0 to 3, and prefix[1] holds day 0, which the subtraction takes away.

  2. Question 2 of 6 With a 0 at the front of the prefix list, which expression gives the total of days first to last, both included?

    Choose one answer.

    Show the answer to question 2

    Answer: prefix[last + 1] - prefix[first]

    prefix[i] is the total of the first i days, so prefix[last + 1] covers days 0 to last and prefix[first] covers days 0 to first − 1. The first choice stops one day early, the third leaves out day first, and the fourth is the formula for running totals without the 0 in front, which breaks when first is 0.

  3. Question 3 of 6 balance_day.py prints each ledger with the answers of the brute force and of the one-pass version, then a check on 2,000 random ledgers.

    What does this program print? Choose one answer.

    import random
    
    
    def balance_day_brute(sales):
        """Adds up both sides again for every day: O(n²)."""
        for day in range(len(sales)):
            if sum(sales[:day]) == sum(sales[day + 1:]):
                return day
        return -1
    
    
    def balance_day(sales):
        """One running total: the days after `day` hold the whole total minus the days before it minus `day` itself."""
        whole = sum(sales)
        before = 0
        for day, amount in enumerate(sales):
            if before == whole - before - amount:
                return day
            before += amount
        return -1
    
    
    for sales in ([3, 8, 2, 6, 5], [4, -1, 9, 3], [0, 6, -6], [5, 5], [7]):
        print(sales, balance_day_brute(sales), balance_day(sales))
    
    rng = random.Random(4)
    ledgers = [[rng.randint(-3, 3) for _ in range(rng.randint(1, 7))] for _ in range(2_000)]
    agree = sum(balance_day(s) == balance_day_brute(s) for s in ledgers)
    print(f"same answer on {agree:,} of {len(ledgers):,} random ledgers")
    Show the answer to question 3

    Answer: it prints

    [3, 8, 2, 6, 5] 2 2
    [4, -1, 9, 3] 2 2
    [0, 6, -6] 0 0
    [5, 5] -1 -1
    [7] 0 0
    same answer on 2,000 of 2,000 random ledgers

    In [3, 8, 2, 6, 5] day 2 balances: 3 + 8 = 11 before it and 6 + 5 = 11 after it. In [0, 6, -6] day 0 balances, because nothing comes before it (a total of 0) and 6 + (−6) = 0 comes after it. A one-day ledger balances on its only day, with nothing on either side, and [5, 5] has no balance day.

  4. Question 4 of 6 A list does not change while many queries run. Which of these can a prefix list answer for any range with one subtraction (or one XOR) per query?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • The total of the values in the range
    • The number of zeros in the range
    • The XOR of the values in the range

    Totals work directly, a count of zeros is the total of a list of ones and zeros, and XOR can be undone because x ^ x is 0. A maximum or a median cannot be taken back out of a running result, so those need other structures.

  5. Question 5 of 6 In Java, the ledger can have 200,000 days of up to 10⁹ each. Which type should the prefix totals have?

    Choose one answer.

    Show the answer to question 5

    Answer: long[]

    The last prefix can reach 200,000 × 10⁹ = 2 × 10¹⁴, far above the int limit of 2,147,483,647, and int addition wraps without any error. A check for negative totals does not help: the lesson's run of the largest ledger wrapped to 552,894,464, which is positive. long holds up to about 9.2 × 10¹⁸.

  6. Question 6 of 6 The days before day d total before, and the whole ledger totals whole. What do the days after day d total?

    Choose one answer.

    Show the answer to question 6

    Answer: whole − before − sales[d]

    The whole ledger is the days before d, day d itself and the days after d. Take away the first two and the days after d are left, so each day is checked in O(1) with one running total.

Interview questions

What is a prefix sum array, and how does it give the total of a range in O(1)? It is the list of running totals of an array, with a 0 in front: prefix[0] = 0 and prefix[i + 1] = prefix[i] + a[i], so prefix[i] is the total of the first i items. Building it takes one pass, O(n) time and n + 1 numbers of memory. The total of a[first] to a[last] is prefix[last + 1] - prefix[first]: both prefixes contain the items before first, so they cancel, and exactly the range is left. That is one subtraction, whatever the length of the range. It fits an array that does not change and is asked many range totals: for q queries over n items it turns O(n × q) into O(n + q).

Why do many prefix-sum solutions put a 0 at the start, so that the array has n + 1 entries? With the 0, prefix[i] is the total of the first i items, and every range has the same formula, prefix[last + 1] - prefix[first], including ranges that start at item 0, where nothing is taken away. Without it, the array holds the totals of items 0 to i, and a range starting at 0 needs a special case, because the formula would read the entry before index 0. In Python that mistake does not even fail: index −1 is the last entry, so the answer is silently wrong. The extra entry costs one number of memory and removes a whole family of off-by-one errors.

Key takeaways

  • A prefix list holds running totals with a 0 in front: prefix[i] is the total of the first i items.
  • The total of items first to last is prefix[last + 1] - prefix[first]: O(n) to build, then O(1) per query.
  • The cue is many range totals or counts over data that does not change while the queries run.
  • The invariant, “prefix[i] is the total of the first i items”, is the whole proof: the shared prefix cancels.
  • Check the fast version against the brute force with a stress test; an off-by-one slip fails on the smallest input.
  • Size the type from the largest prefix: 2 × 10⁵ days of 10⁹ need 64-bit totals in Java, C++ and Go.
  • Conditions that compare what comes before a position with what comes after it are prefix sums in disguise.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.