Prefix, Suffix & Range Updates

Optional deeper study after the foundation: derive prefix/suffix summaries, products excluding elements, pivot indices, difference arrays and batched range totals through worked traces and practice.

Last generated

Lesson 23 of 23 available15 practice questions

SPACED REPETITION Β· 15 practice questions

Make this lesson stick.

Try 3 questions now. No account needed. Sample answers aren't saved.

A range has only two ends

An airline runs 100,000 flights, numbered 0 to 99,999. Overnight, 100,000 group bookings arrive, each shaped like "add 3 seats to every flight from 20,000 to 90,000". In the morning, the finance team sends 100,000 questions shaped like "how many seats did we sell on flights l through r?"

The obvious plan adds each booking one flight at a time and answers each question by adding up its flights. A booking can cover all 100,000 flights, so the bookings alone can cost 100,000 Γ— 100,000 = 10¹⁰ additions. The questions can cost as much again. A judge gives you a second or two.

Both halves waste effort the same way: they walk through the inside of a range, but only its two ends carry information. A booking raises every seat count in its range by the same amount, so compared with its neighbours the count only jumps in two places: where the booking starts and just after it stops. A question needs only the running total before l and the running total through r. Store cumulative information once, and every range update or range question touches two cells. That is this lesson in one sentence.

# Smell in the slow code Move Signature problem
1 Re-adding stretches of an array that never changes Subtract two prefix readings Range Sum Query - Immutable
2 For each position, re-scanning every other position One pass from each end Product of Array Except Self
3 Comparing the left side with the right side at every position Total minus the running left sum Find Pivot Index
4 Adding a value to every cell of a range, many times Mark the two ends, sum once Corporate Flight Bookings

The boss level combines moves 4 and 1 to solve the airline.

This lesson builds on Foundation: Arrays & Strings, especially Move 1 (carry a running summary) and Move 6 (the prefix array and prefix[r + 1] βˆ’ prefix[l]). Counting subarrays with a given sum, which pairs prefix sums with a hash map, belongs to Hash Maps & Sets. Structures that accept updates and queries in any order belong to Trees & Advanced Structures. Traces use zero-based indices, and a range [l, r] includes both ends unless a problem says otherwise.

Warm-up: two readings answer any range

Smell: the array never changes, and many questions ask for the sum of a stretch of it.

A quick recap of Foundation's Move 6. prefix[k] is the sum of the first k elements, so prefix[0] = 0 and the table has n + 1 entries. The readings sit between elements. An inclusive range [l, r] is the reading after r minus the reading before l:

from itertools import accumulate

def build_prefix(nums):
    return list(accumulate(nums, initial=0))   # prefix[k] = sum(nums[:k]); Python 3.8+

def range_sum(prefix, l, r):                   # inclusive, 0 <= l <= r < len(nums)
    return prefix[r + 1] - prefix[l]

prefix = build_prefix([2, 1, 4, 3])
print(prefix, range_sum(prefix, 1, 3))         # [0, 2, 3, 7, 10] 8

accumulate(..., initial=0) builds the same table as Foundation's explicit loop. Building it costs O(n) time and O(n) extra space, where n is the array length, and each query is O(1). Python integers never overflow. In Java or C++, 10⁡ values of up to 10⁡ each add up to 10¹⁰, so store the prefix in a 64-bit type.

Predict first: the trick needs a way to remove everything before l. For which of these can two readings of a prefix table answer any range? (a) the sum; (b) the XOR; (c) how many elements are even; (d) the product; (e) the maximum.

Check your answer
  • Sum: yes. Subtract the two readings.
  • XOR: yes. Since x ^ x = 0, XOR-ing the two readings cancels everything before l, so the answer is px[r + 1] ^ px[l]. That is LeetCode 1310: XOR Queries of a Subarray.
  • Count of evens: yes. Turn each element into a flag (1 if even, 0 if not) and prefix-sum the flags. Counting is adding up 0s and 1s.
  • Product: only if there are no zeros. Removing the prefix means dividing by it, and once a zero appears every later reading is 0, so any range that starts after the zero asks for 0 / 0.
  • Maximum: no. As Foundation showed, a maximum can't be undone, so the readings can't cancel the part before l.

The rule: the operation needs an inverse that cancels the unwanted prefix.

The table has one precondition: the values must not change between queries. Change nums[i] and every reading after position i is stale. If all the changes arrive before the first query, apply them first; the difference array later in this lesson does that cheaply. If changes and queries keep interleaving, you need a structure built for it, such as the Fenwick tree in Trees & Advanced Structures.

Your turn: LeetCode 2559: Count Vowel Strings in Ranges. You get up to 10⁡ words and up to 10⁡ queries [l, r]. For each query, count the words in that inclusive index range that start and end with a vowel. Predict the answers for words = ["aba", "bcb", "ece", "aa", "e"] and queries [[0, 2], [1, 4], [1, 1]], then write it in O(n + q) time, where n is the number of words and q the number of queries.

Check your answer

Turn each word into a flag: [1, 0, 1, 1, 1]. The prefix table of the flags is [0, 1, 1, 2, 3, 4]. The answers are prefix[3] βˆ’ prefix[0] = 2, prefix[5] βˆ’ prefix[1] = 3 and prefix[2] βˆ’ prefix[1] = 0, which gives [2, 3, 0].

from itertools import accumulate

def vowel_strings(words, queries):
    vowels = set("aeiou")
    flags = [1 if w[0] in vowels and w[-1] in vowels else 0 for w in words]
    prefix = list(accumulate(flags, initial=0))
    return [prefix[r + 1] - prefix[l] for l, r in queries]

A single-element query is the classic test for a missing + 1, but pick one whose answer isn't 0. Without the + 1, the query [1, 1] computes prefix[1] βˆ’ prefix[1] = 0, which happens to be right because "bcb" doesn't count. The query [2, 2] should be 1, and the buggy version still says 0. Scanning each query instead would cost up to 10⁡ steps per query, or 10¹⁰ in total.

Everything but me: one pass from each end

Smell: for each position, the slow code loops over all the other positions.

LeetCode 238: Product of Array Except Self asks for an array answer where answer[i] is the product of every element except nums[i]. For [1, 2, 3, 4] the answer is [24, 12, 8, 6]. There are up to 10⁡ elements, the solution must run in O(n) time, and division is not allowed.

Predict first: the tempting shortcut ignores that last rule. Multiply everything once, then divide by nums[i]. What happens on [3, 0, 4], and what is the right answer?

Check your answer

The total product is 0. Index 0 gives 0 // 3 = 0, which is correct. Index 1 divides 0 by 0, and Python raises ZeroDivisionError. The right answer is [0, 12, 0]. Every position except the zero's own includes the zero as a factor. The zero's own position is the product of the rest, 3 Β· 4 = 12. With two zeros, as in [0, 0, 4], every position includes a zero, so the answer is all zeros. The shortcut needs special cases for zeros; the method below needs none.

The slow version multiplies the other n βˆ’ 1 values at every index: about nΒ² multiplications, around 10¹⁰ at n = 10⁡. Look at what it repeats. The answer at i splits into two groups that never overlap:

answer[i] = (nums[0] Β· … Β· nums[i-1])  Γ—  (nums[i+1] Β· … Β· nums[n-1])
              everything left of i          everything right of i

The left group for i + 1 is the left group for i times nums[i]. That is Foundation's running summary again, carried once from the left end and once from the right end.

i 0 1 2 3
nums[i] 1 2 3 4
product left of i 1 1 2 6
product right of i 24 12 4 1
answer 24 12 8 6

You don't need two extra arrays. Store the left products in the output, then multiply the right products in on a backward pass:

def product_except_self(nums):
    n = len(nums)
    res = [1] * n
    left = 1                          # product of nums[0 .. i-1]
    for i in range(n):
        res[i] = left                 # write first...
        left *= nums[i]               # ...then fold nums[i] in
    right = 1                         # product of nums[i+1 .. n-1]
    for i in range(n - 1, -1, -1):
        res[i] *= right
        right *= nums[i]
    return res

Trace on [1, 2, 3, 4]:

pass i running value before the step res afterward
left 0 left = 1 [1, 1, 1, 1]
left 1 left = 1 [1, 1, 1, 1]
left 2 left = 2 [1, 1, 2, 1]
left 3 left = 6 [1, 1, 2, 6]
right 3 right = 1 [1, 1, 2, 6]
right 2 right = 4 [1, 1, 8, 6]
right 1 right = 12 [1, 12, 8, 6]
right 0 right = 24 [24, 12, 8, 6]

Why it works

There is one invariant per pass. Before the left pass handles i, left is the product of nums[0..iβˆ’1]. Before the right pass handles i, right is the product of nums[i+1..nβˆ’1]. The left pass stores the first factor in res[i], the right pass multiplies in the second, and nothing else touches res[i]. Zeros need no special case, because a zero simply sits in the groups it belongs to.

Two passes give O(n) time. The extra space is two integers, O(1), because LeetCode doesn't count the output array.

Two lines that decide correctness

The empty side is the identity. Nothing sits left of index 0, so left starts at 1, the value that leaves a product unchanged. For sums the identity is 0. Ordinary integers have no neutral value for a maximum, so you add one, -inf, which every number beats. Or you use whatever the problem says a missing side means. The next exercise starts its maximum at 0, which works because heights are never negative.

⚠️ Write, then fold. Swap the two lines in both loops and nums[i] leaks into its own answer. res[i] becomes the product of nums[0..i] times the product of nums[i..nβˆ’1], which is the total times nums[i], so [1, 2, 3, 4] returns [24, 48, 72, 96]. It is the same ordering rule as Foundation's seen set: check first, then add.

Your turn: LeetCode 42: Trapping Rain Water. Bars of width 1 have heights h, up to 2 Γ— 10⁴ of them. After rain, water above bar i rises to the lower of two walls: the tallest bar on its left and the tallest bar on its right. So the bar holds min(tallest on the left, tallest on the right) βˆ’ h[i] units, and never less than 0. Work out h = [2, 1, 3, 1, 2] by hand, then write it in O(n) time. Which summary does each pass carry, and how do you combine them?

Check your answer

Carry a running maximum from each end instead of a running product. Include bar i itself in both maxima. Then the water level min(left_max[i], right_max[i]) can never fall below h[i], so nothing needs clamping at zero, and the two edge bars get 0 automatically.

For [2, 1, 3, 1, 2]: left_max = [2, 2, 3, 3, 3] and right_max = [3, 3, 3, 2, 2]. The levels are [2, 2, 3, 2, 2], the water per bar is [0, 1, 0, 1, 0], and the total is 2.

def trap(height):
    n = len(height)
    left_max = [0] * n
    best = 0                          # identity for max here: heights are >= 0
    for i in range(n):
        best = max(best, height[i])
        left_max[i] = best            # tallest bar in height[0..i]
    right_max = [0] * n
    best = 0
    for i in range(n - 1, -1, -1):
        best = max(best, height[i])
        right_max[i] = best           # tallest bar in height[i..n-1]
    return sum(min(left_max[i], right_max[i]) - height[i] for i in range(n))

print(trap([2, 1, 3, 1, 2]), trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]))   # 2 6

Compared with the products, three things changed: the summary (maximum instead of product), the combiner (the smaller of the two sides instead of their product), and a last step that subtracts h[i] and adds up the results. The skeleton stayed the same: summarize the left, summarize the right, combine. This version takes O(n) time and O(n) extra space. A two-pointer version brings the space down to O(1), but derive this one first.

Left against right: pivot index and balanced split

Smell: at every position, or at every cut, the slow code adds up the left side and the right side from scratch.

LeetCode 724: Find Pivot Index, which is the same problem as LeetCode 1991. A pivot index is an index where the sum of everything strictly to its left equals the sum of everything strictly to its right. An empty side sums to 0. Return the leftmost pivot, or βˆ’1 if there is none. There are up to 10⁴ elements, and values may be negative.

Predict first: find the pivot of [1, 7, 3, 6, 5, 6], of [2, 1, βˆ’1] and of [1, 2, 3] by hand.

Check your answer

3, 0 and βˆ’1. In the first array, index 3 has 1 + 7 + 3 = 11 on its left and 5 + 6 = 11 on its right. In the second, index 0 has an empty left side (0), and its right side is 1 + (βˆ’1) = 0. The third has no pivot: the (left, right) pairs are (0, 5), (1, 3) and (3, 0).

The slow version computes sum(nums[:i]) and sum(nums[i + 1:]) for every i: O(nΒ²) time, and every slice makes a copy. You could fix it the way product-except-self did, with a left pass and a right pass. But addition has an inverse, so there is a shortcut. Every element sits in exactly one of three places:

left + nums[i] + right = total

So the total, together with the running left sum, determines the right sum: right = total βˆ’ left βˆ’ nums[i]. One number replaces a whole suffix array.

def pivot_index(nums):
    total = sum(nums)
    left = 0                              # sum of nums[0 .. i-1]
    for i, x in enumerate(nums):
        if left == total - left - x:      # right side = total - left - x
            return i
        left += x                         # fold x in only after the check
    return -1

Trace on [1, 7, 3, 6, 5, 6], where the total is 28:

i x left right = 28 βˆ’ left βˆ’ x equal?
0 1 0 27 no
1 7 1 20 no
2 3 8 17 no
3 6 11 11 yes: return 3

Why it works. Invariant: when the loop reaches i, left is the sum of nums[0..iβˆ’1]. The equation above then gives the exact right sum, so the check tests the definition of a pivot at every index, from left to right, and the first hit is the leftmost pivot. It takes O(n) time (one pass inside sum, one in the loop) and O(1) extra space.

Edge cases. Negative values are fine, because nothing here relies on sums growing. Both ends are real candidates: index 0 compares 0 with the rest, and index n βˆ’ 1 compares the rest with 0. A one-element array always returns 0. And the order rule from the previous section applies again: fold x into left only after the check.

Your turn: cut instead of exclude

A pivot leaves out one element. Now every element must go to one side. Given an array of positive integers, can you cut it into two non-empty contiguous parts with equal sums? Try [1, 2, 3, 6], [1, 3, 5, 7] and [3, 1, 1, 2], then write it in O(n) time and O(1) extra space.

Then change the constraint: zeros and negative numbers are now allowed. Does your loop bound still matter? Try [3, βˆ’3] and [0].

Check your answer

[1, 2, 3, 6]: the total is 12, and the prefix 1 + 2 + 3 = 6 is half of it, so True (cut before the 6). [1, 3, 5, 7]: the total is 16 and half is 8, but the prefix sums go 1, 4, 9 and jump over 8, so False. [3, 1, 1, 2]: the total 7 is odd, so no two equal integer sums exist. False, decided before the search starts.

The left part sums to prefix and the right part to total βˆ’ prefix, so they are equal exactly when prefix == total / 2:

def can_split(nums):
    total = sum(nums)
    if total % 2:                    # odd total: impossible
        return False
    half = total // 2
    prefix = 0
    for i in range(len(nums) - 1):   # the last element stays on the right
        prefix += nums[i]
        if prefix == half:
            return True
    return False

The loop bound. With positive values, the prefix of the whole array is total, which never equals half, so one extra step would be harmless. Allow zeros or negatives and the total can be 0, which makes half 0 as well. Now the whole-array prefix does equal half. A loop over range(len(nums)) returns True for [3, βˆ’3], whose parts 3 and βˆ’3 are not equal, and for [0], which has nothing to cut. range(len(nums) - 1) keeps the right part non-empty, and both correctly return False.

Two more details. With positive values the prefix only grows, so you may stop as soon as prefix > half. With negatives that shortcut is wrong, because a later element can bring the prefix back down; the equality check itself still works. And looping over nums[:-1] instead of range(len(nums) - 1) would copy the list: O(n) extra memory for no benefit.

Difference arrays: touch two cells, not the whole range

Smell: many operations each say "add v to every cell from l to r", and nobody reads the array until the operations are done.

Here is the hook's first half, 0-indexed. There are n flights numbered 0 to n βˆ’ 1, all starting at 0 seats. Each booking (l, r, v) adds v seats to every flight from l to r inclusive. After all bookings, return the seat count of every flight. That is LeetCode 370: Range Addition, a premium problem. LeetCode 1109: Corporate Flight Bookings is the same task with flights numbered 1 to n.

The slow version is a nested loop:

def range_adds_naive(n, ops):
    arr = [0] * n
    for l, r, v in ops:
        for i in range(l, r + 1):     # touches every cell in the range
            arr[i] += v
    return arr

With m bookings that each cover most of the n flights, that is O(n · m) work: 10¹⁰ additions at n = m = 10⁡. Every booking walks through the inside of its range, where nothing interesting happens.

Predict first: start from [0, 0, 0, 0, 0] and add 2 to indices 1 through 3, which gives [0, 2, 2, 2, 0]. Now write down each cell minus the cell to its left, counting the cell left of index 0 as 0. How many of those differences are non-zero? Would that number change if the range covered 1,000 cells?

Check your answer

The differences are [0, 2, 0, 0, βˆ’2]: a +2 where the range starts and a βˆ’2 just after it ends. Inside the range, neighbours rose together, so the differences between them didn't change. A range of 1,000 cells still changes exactly two differences, or only one if it runs to the last cell.

That is the whole trick. Store the differences instead of the values. A range add becomes two writes: +v at l and βˆ’v at r + 1. To get the values back, take a running sum of the differences. This array of differences is called a difference array: diff[i] holds the change that begins at index i.

def range_adds(n, ops):
    diff = [0] * (n + 1)              # slot n catches r + 1 when r == n - 1
    for l, r, v in ops:
        diff[l] += v                  # the change starts at l...
        diff[r + 1] -= v              # ...and is undone right after r
    values = []
    running = 0
    for i in range(n):                # a prefix sum over diff; slot n is never read
        running += diff[i]
        values.append(running)
    return values

print(range_adds(5, [(1, 3, 2), (2, 4, 3)]))   # [0, 2, 5, 5, 3]

Trace with n = 5 and the bookings (1, 3, +2) and (2, 4, +3):

index            0    1    2    3    4    5
after (1,3,+2)   0   +2    0    0   -2    0
after (2,4,+3)   0   +2   +3    0   -2   -3
running sum      0    2    5    5    3    -     <- the answer [0, 2, 5, 5, 3]

The βˆ’3 in slot 5 is never read. Spot-check index 3: both bookings cover it, and 2 + 3 = 5. Index 4 is covered only by the second booking, so it holds 3.

Why the running sum rebuilds the values

The running sum at index i adds up every change recorded at an index ≀ i. Take one booking's pair of changes, +v at l and βˆ’v at r + 1:

  • If i < l, neither change is included, so the booking contributes 0.
  • If l ≀ i ≀ r, only the +v is included, so it contributes v.
  • If i > r, both are included, so it contributes v βˆ’ v = 0.

Each booking acts like a switch that turns on at l and off at r + 1, and the switches of different bookings simply add. That is why overlapping and nested ranges need no special handling.

πŸ’‘ Differences and prefix sums undo each other. Take the differences of any array and prefix-sum them, and you get the array back, because a[0] + (a[1] βˆ’ a[0]) + (a[2] βˆ’ a[1]) + … + (a[i] βˆ’ a[iβˆ’1]) collapses to a[i]. (The first difference is a[0] itself, since the cell left of index 0 counts as 0.) Differences make updates cheap and prefix sums make queries cheap. The boss level uses both.

Job Nested loop Difference array
Apply m range adds O(n Β· m) worst case O(m)
Produce the final array nothing extra O(n)
Total, including creating the array O(n + n Β· m) O(n + m)
Extra space besides the output O(1) O(n) for diff

Where the boundary goes, and where the trick stops

⚠️ The r + 1 slot. With only n slots, diff[r + 1] raises IndexError whenever r = n βˆ’ 1. Allocate n + 1 slots and never read the last one. A booking that covers every flight then writes +v at index 0 and βˆ’v into that spare slot.

  • l = 0, l = r, negative v, overlapping ranges: no special cases needed.
  • 1-indexed input, as in LeetCode 1109: subtract 1 from both ends before recording.
  • Half-open ranges. Some problems say a range stops before its end point, like passengers who get off at stop to. Then the change stops at to: write βˆ’v at to, not at to + 1. Look for the word "inclusive" in the statement.
  • Huge coordinates. If positions go up to 10⁹, you can't allocate diff. Keep the changes in a dictionary instead and walk its keys in sorted order: O(m log m) time and O(m) space for m ranges. This version is often called a sweep line.

⚠️ The real limit: all updates first. The running sum at i is final only after every booking has been recorded. If a problem makes you answer a query between updates, each answer costs an O(n) rebuild. That is fine for a handful of queries and hopeless for 10⁡ of them. There is one exception. If you read positions left to right and every new update starts after the positions you have already read, those readings stay final. For heavy interleaving, a Fenwick tree over diff supports range add and single-cell reads in O(log n) each. Range add together with range sums needs two Fenwick trees or a segment tree with lazy propagation. Trees & Advanced Structures introduces Fenwick and segment trees.

Your turn: LeetCode 1094: Car Pooling. A car with capacity seats drives east. Each trip [p, from, to] means p passengers board at kilometre from and get off at kilometre to. At the same point, passengers get off before new ones board. Can the car do every trip without ever holding more than capacity passengers? There are up to 1,000 trips, with 0 ≀ from < to ≀ 1000. Predict [[2, 1, 5], [3, 3, 7]] with capacity 4, then with capacity 5. Then predict the case that catches boundary bugs, [[2, 1, 5], [3, 5, 7]] with capacity 3. Write it in O(m + L) time, where m is the number of trips and L = 1,001 is the number of kilometre points.

Check your answer

False, True, True. In the first two cases, 2 + 3 = 5 passengers ride together between kilometres 3 and 5. In the third case, the first group gets off at kilometre 5 before the second group boards there, so the car never holds more than 3.

def car_pooling(trips, capacity):
    diff = [0] * 1001                 # kilometres 0..1000
    for p, start, end in trips:
        diff[start] += p              # they board at start
        diff[end] -= p                # gone AT end: the range is half-open
    load = 0
    for change in diff:
        load += change
        if load > capacity:
            return False
    return True

For the first example, the load at kilometres 0 to 7 is 0, 2, 2, 5, 5, 3, 3, 0. The 5 exceeds capacity 4. If you write diff[end + 1] -= p, the first group is still counted at kilometre 5, so the third example reports a load of 5 and wrongly returns False. Recording costs O(m), the sweep costs O(L), and diff takes O(L) extra space. If kilometres could reach 10⁹, the dictionary-and-sort version would cost O(m log m) instead.

Check it against a brute force that counts the passengers at every kilometre:

import random

def car_pooling_brute(trips, capacity):
    return all(sum(p for p, s, e in trips if s <= km < e) <= capacity
               for km in range(1001))

for _ in range(1000):
    trips = []
    for _ in range(random.randint(1, 5)):
        s = random.randint(0, 12)
        trips.append([random.randint(1, 5), s, random.randint(s + 1, 14)])
    cap = random.randint(1, 12)
    assert car_pooling(trips, cap) == car_pooling_brute(trips, cap), (trips, cap)
print("all matched")

Boss level: bookings first, questions after

Back to the airline. All bookings arrive first, and then come many questions of the form "total seats on flights l through r". You now have both halves:

  1. Changes β†’ values: record every booking in a difference array, then take its running sum.
  2. Values β†’ totals: build a prefix table over those values, and answer each question with two readings.

Try it first: with n = 5, bookings [(1, 3, 2), (2, 4, 3)] and queries [(0, 4), (1, 2), (4, 4)], what are the three answers? And why can't a single running sum over diff answer the questions?

Check your answer

15, 7 and 3. The values are [0, 2, 5, 5, 3] (the previous section's trace), and their prefix table is [0, 0, 2, 7, 12, 15]. So (0, 4) is 15 βˆ’ 0, (1, 2) is 7 βˆ’ 0, and (4, 4) is 15 βˆ’ 12.

One running sum over diff turns changes into values, not totals. Taking two readings of that running sum gives values[r] βˆ’ values[l βˆ’ 1], which is the difference between two seat counts and not a sum of seat counts. You need a second running sum over the values.

# uses range_adds (difference arrays) and build_prefix / range_sum (warm-up)
def booking_totals(n, bookings, queries):
    values = range_adds(n, bookings)       # pass 1: changes -> values
    prefix = build_prefix(values)          # pass 2: values -> running totals
    return values, [range_sum(prefix, l, r) for l, r in queries]

print(booking_totals(5, [(1, 3, 2), (2, 4, 3)], [(0, 4), (1, 2), (4, 4)]))
# ([0, 2, 5, 5, 3], [15, 7, 3])
index      0    1    2    3    4    5
diff       0   +2   +3    0   -2   -3
values     0    2    5    5    3          <- running sum of diff[0..4]
prefix     0    0    2    7   12   15     <- prefix[k] = sum of the first k values

For n flights, m bookings and q questions this is O(n + m + q) time. On the hook's numbers that is a few hundred thousand steps instead of up to 2 Γ— 10¹⁰. The extra space is O(n) for diff, the values and the prefix table, plus O(q) for the answers.

⚠️ Build the prefix table after the last booking. A table built earlier describes the old seat counts. Every query then returns a plausible number about the wrong array, and nothing crashes to warn you.

If bookings and questions interleave instead, both tables go stale after every booking. That is a job for the tree structures from the previous section's warning.

Your turn: the flights already hold base = [2, 0, 5, 1] seats. Apply the bookings [(0, 1, 3), (2, 3, 2)], then answer the queries [0, 3] and [1, 2]. Can you fold base into the difference array so that a single running sum produces the final seat counts? Finally, test your function against a direct simulator.

Check your answer

The bookings add [3, 3, 2, 2], so the final counts are [5, 3, 7, 3], their prefix table is [0, 5, 8, 15, 18], and the answers are 18 and 10.

To fold base in, treat each starting value as a one-cell booking: +base[i] at i and βˆ’base[i] at i + 1. That is the same as seeding diff with the differences of base, and since a running sum undoes differences, it rebuilds base plus every booking:

from itertools import accumulate
# the last lines use build_prefix and range_sum from the warm-up

def range_adds_on(base, ops):
    n = len(base)
    diff = [0] * (n + 1)
    for i, x in enumerate(base):      # each starting value is a one-cell booking
        diff[i] += x
        diff[i + 1] -= x
    for l, r, v in ops:
        diff[l] += v
        diff[r + 1] -= v
    return list(accumulate(diff[:n]))

values = range_adds_on([2, 0, 5, 1], [(0, 1, 3), (2, 3, 2)])
prefix = build_prefix(values)
print(values, range_sum(prefix, 0, 3), range_sum(prefix, 1, 2))   # [5, 3, 7, 3] 18 10

Adding base element by element after range_adds is just as correct and also O(n). The mistake to avoid is building the query table from base before the bookings are applied.

A simulator makes a trustworthy reference because it is too simple to get wrong. Random small cases cover the risky ones: a booking on the last cell, a booking over every cell, overlaps, negative adjustments and no bookings at all.

import random
# uses range_adds_on from above and build_prefix / range_sum from the warm-up

def simulate(base, ops, queries):
    arr = base[:]
    for l, r, v in ops:
        for i in range(l, r + 1):
            arr[i] += v
    return arr, [sum(arr[l:r + 1]) for l, r in queries]

for _ in range(2000):
    n = random.randint(1, 7)
    base = [random.randint(-5, 5) for _ in range(n)]
    ops, queries = [], []
    for _ in range(random.randint(0, 5)):
        l = random.randint(0, n - 1)
        ops.append((l, random.randint(l, n - 1), random.randint(-5, 5)))
    for _ in range(3):
        l = random.randint(0, n - 1)
        queries.append((l, random.randint(l, n - 1)))
    values = range_adds_on(base, ops)
    prefix = build_prefix(values)
    got = (values, [range_sum(prefix, l, r) for l, r in queries])
    assert got == simulate(base, ops, queries), (base, ops, queries)
print("all matched")

Final round: no label on the problem

Real problems don't say which move they want. For each one, find the smell, pick the move, and settle the boundaries before you write code.

Challenge 1: Maximum Population Year

LeetCode 1854: Maximum Population Year. Each log [birth, death] describes one person, who counts as alive in every year from birth to death βˆ’ 1, but not in the year of death. Years run from 1950 to 2050. Return the earliest year with the largest population. [[1950, 1961], [1960, 1971], [1970, 1981]] should give 1960.

  1. Which move applies, and what plays the role of the range?
  2. Where does the βˆ’1 go, and why?
  3. How do you make ties go to the earliest year?
  4. What would you change if years ran from 0 to 10⁹?
Check your answer

Each person is a range add of +1 over their living years, and the question is read only after all logs are in: a difference array over the years. The range is half-open (dead in the year of death), so the βˆ’1 goes at death, not at death + 1. A strict > when updating the best keeps the earliest year on ties.

def maximum_population(logs):
    diff = [0] * 101                      # index y - 1950 for years 1950..2050
    for birth, death in logs:
        diff[birth - 1950] += 1
        diff[death - 1950] -= 1           # not alive in the death year
    best_year, best, alive = 1950, 0, 0
    for i in range(101):
        alive += diff[i]
        if alive > best:                  # strict: an equal count later doesn't win
            best, best_year = alive, 1950 + i
    return best_year

print(maximum_population([[1950, 1961], [1960, 1971], [1970, 1981]]))   # 1960

This is O(m + Y) time for m logs and Y = 101 years, with O(Y) extra space. Honestly, the limits here are tiny (at most 100 people), so checking every year against every person would pass too. The difference array is the version that still works with 10⁡ logs. For years up to 10⁹, keep the +1/βˆ’1 changes in a dictionary and sweep its sorted keys, for O(m log m).

Challenge 2: Find Good Days to Rob the Bank

LeetCode 2100: Find Good Days to Rob the Bank. security[i] is the number of guards on day i. Day i is good if the time days before it are non-increasing into day i and the time days after it are non-decreasing out of it: security[i βˆ’ time] β‰₯ … β‰₯ security[i] ≀ … ≀ security[i + time]. Return every good day. Both n and time go up to 10⁡. security = [5, 3, 3, 3, 5, 6, 2] with time = 2 gives [2, 3].

  1. What does checking each day directly cost at these limits?
  2. What should each pass remember about day i?
  3. What value does an edge day get?
Check your answer

Checking 2 Β· time neighbours for each day that has them is O(n Β· time); at these limits that peaks at about 2.5 Γ— 10⁹ comparisons, when time is about n / 4. Each day needs one fact about its left side and one about its right side, which is the everything-but-me shape. The summaries are streak lengths this time, Foundation's current-run counter carried once from each end:

  • down[i]: how many steps in a row lead into day i without going up.
  • up[i]: how many steps in a row lead out of day i without going down.
def good_days(security, time):
    n = len(security)
    down = [0] * n                        # empty side: a streak of 0
    for i in range(1, n):
        if security[i - 1] >= security[i]:
            down[i] = down[i - 1] + 1
    up = [0] * n
    for i in range(n - 2, -1, -1):
        if security[i] <= security[i + 1]:
            up[i] = up[i + 1] + 1
    return [i for i in range(n) if down[i] >= time and up[i] >= time]

print(good_days([5, 3, 3, 3, 5, 6, 2], 2))   # [2, 3]

Day 0 has nothing before it, so down[0] = 0, the length of an empty streak. The condition "at least time days on each side" needs no separate check, because down[i] can never exceed i and up[i] can never exceed n βˆ’ 1 βˆ’ i. With time = 0 every day is good. This is O(n) time and O(n) extra space.

Challenge 3: Sum of Absolute Differences in a Sorted Array

LeetCode 1685: Sum of Absolute Differences in a Sorted Array. nums is sorted in non-decreasing order, with up to 10⁡ elements. Return result where result[i] is the sum of |nums[i] βˆ’ nums[j]| over all j. [2, 3, 5] gives [4, 3, 5].

  1. What does sortedness let you do with the absolute value?
  2. After that, which two prefix readings does each side need?
Check your answer

Sortedness removes the absolute value. The i elements left of x = nums[i] are all ≀ x, so together they contribute x Β· i βˆ’ (their sum). The n βˆ’ 1 βˆ’ i elements to its right are all β‰₯ x, so they contribute (their sum) βˆ’ x Β· (n βˆ’ 1 βˆ’ i). Each side's sum is two readings of one prefix table, or one reading plus the total:

from itertools import accumulate

def abs_diff_sums(nums):
    n = len(nums)
    prefix = list(accumulate(nums, initial=0))
    total = prefix[n]
    result = []
    for i, x in enumerate(nums):
        left = x * i - prefix[i]                              # i values, all <= x
        right = (total - prefix[i + 1]) - x * (n - 1 - i)     # n-1-i values, all >= x
        result.append(left + right)
    return result

print(abs_diff_sums([2, 3, 5]), abs_diff_sums([1, 4, 6, 8, 10]))   # [4, 3, 5] [24, 15, 13, 15, 21]

The direct double loop is O(n²), up to 10¹⁰ steps. This is O(n) time. The prefix table takes O(n) extra space, and a running left sum plus the total would bring that down to O(1) besides the output. On unsorted input you would sort first, but then you would have to carry the original indices along to put each answer back in its place.

Cheat sheet: smell β†’ move

When the slow code… Reach for Key line
re-adds stretches of an array that never changes a prefix table prefix[r + 1] - prefix[l]
rescans everything except i, at every i one pass from each end write res[i], then fold nums[i] in
compares the left and right sums at every i total minus the running left sum right = total - left - nums[i]
adds v across a range, many times, before any read a difference array with n + 1 slots diff[l] += v, diff[r + 1] -= v
does range adds, then answers range sums both, in that order changes β†’ values β†’ running totals

Before moving on, pick one problem from this lesson and solve it aloud from a blank editor. Say what each running variable means at a specific moment in the loop, where the empty side gets its identity value, why the boundary write goes at r + 1 (or at to for a half-open range), and which changed constraint would break your solution: a zero under division, a negative number under an early exit, or a query that arrives before the last update. If you can do that, you can take these moves to problems that don't announce them.

Next: Hash Maps & Sets combines prefix sums with a hash map to count subarrays with a target sum. Sliding Window Patterns handles ranges that move instead of ranges that are queried. Trees & Advanced Structures covers the Fenwick and segment trees for updates and queries that interleave.