Coding Interview Patterns Module 30 – Range queries: Fenwick and segment trees
Segment trees: point updates, range queries
The lowest fare between two days while fares keep changing: the problem with ties, worked examples, the brute force and why a tree of halves fits.
What you will learn
- Build an iterative segment tree of 2n nodes for a minimum, maximum or sum
- Answer a range query and a point update in O(log n) each
- Find where the minimum is, with a fixed rule for ties
- Write a custom merge for a combine that is not commutative, such as the best run of values
- Choose between the bottom-up and the recursive tree, with measured numbers
Before you start
What you’ll learn · Before you start
What you will learn
- Build an iterative segment tree of 2n nodes for a minimum, maximum or sum
- Answer a range query and a point update in O(log n) each
- Find where the minimum is, with a fixed rule for ties
- Write a custom merge for a combine that is not commutative, such as the best run of values
- Choose between the bottom-up and the recursive tree, with measured numbers
Before you start
On this page
A segment tree answers range queries, such as the smallest value from position l to position r, while single values change, and both the query and the change take O(log n). It stores the answer for each half of the array, each quarter and so on down to single values. Any range is then the combination of at most about 2 log₂ n stored answers, and a change recomputes only the log₂ n answers above it. It works for any associative combine with an identity value: minimum, maximum, sum, gcd, or a custom merge such as the best run of consecutive values. Here it finds the cheapest fare between two days while fares change.
The problem: the cheapest fare between two days
The problem
A booking site keeps the cheapest fare of each of the next n days, days numbered from 0. Write a class FareTracker
with update(day, fare), when one day’s fare changes, and cheapest(first, last), which returns the lowest fare of
days first to last, both included, and the day it falls on; when several days share that fare, the earliest. Limits:
1 ≤ n ≤ 200,000, at most 200,000 calls, fares from 1 to 10⁹.
With the fares [450, 380, 520, 380, 610, 300, 700, 340]:
cheapest(0, 4)gives (380, 1): days 1 and 3 tie, and day 1 is earlier.- After
update(1, 500),cheapest(0, 4)gives (380, 3) andcheapest(5, 7)gives (300, 5). - After
update(5, 900),cheapest(4, 7)gives (340, 7), andcheapest(2, 2)gives (520, 2).
The brute force: scan the range
Read every day of the range and keep the best pair (fare, day). A change is one write, and a question reads up to n fares:
"""The cheapest fare over a range of days, found by scanning the range for every question, with the work counted."""
import random
def run_brute(fares, events):
fares, answers, reads = list(fares), [], 0
for kind, x, y in events:
if kind == "update":
fares[x] = y
else:
best = min((fares[d], d) for d in range(x, y + 1)) # the earliest day wins a tie
reads += y - x + 1
answers.append(best)
return answers, reads
fares = [450, 380, 520, 380, 610, 300, 700, 340]
events = [("cheapest", 0, 4), ("update", 1, 500), ("cheapest", 0, 4), ("cheapest", 5, 7),
("update", 5, 900), ("cheapest", 4, 7), ("cheapest", 2, 2)]
print("fares", fares)
print("answers (fare, day):", run_brute(fares, events)[0])
rng = random.Random(8)
last = None
for n in (1_000, 2_000, 4_000):
fares = [rng.randint(100, 5000) for _ in range(n)]
events = []
for _ in range(n):
if rng.random() < 0.5:
events.append(("update", rng.randrange(n), rng.randint(100, 5000)))
else:
a, b = rng.randrange(n), rng.randrange(n)
events.append(("cheapest", min(a, b), max(a, b)))
reads = run_brute(fares, events)[1]
print(f"n = {n:,} days and {n:,} events: {reads:,} fares read" + (f", x{reads / last:.1f}" if last else ""))
last = reads
print(f"at 200,000 days and 200,000 events: about {last * (200_000 / 4_000) ** 2:.1e} fares read (estimated)") Output
fares [450, 380, 520, 380, 610, 300, 700, 340] answers (fare, day): [(380, 1), (380, 3), (300, 5), (340, 7), (520, 2)] n = 1,000 days and 1,000 events: 172,828 fares read n = 2,000 days and 2,000 events: 654,789 fares read, x3.8 n = 4,000 days and 4,000 events: 2,607,959 fares read, x4.0 at 200,000 days and 200,000 events: about 6.5e+09 fares read (estimated)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Doubling the days and the events multiplies the reads by about 4. At the limits that is about 6.5 × 10⁹ fares read, far beyond a time limit of a few seconds in any language.
What to notice
Three faster structures fail here, each for one reason. Prefix sums and Fenwick trees answer a range by subtracting two prefixes, and a minimum cannot be subtracted. A sparse table answers fixed data, and these fares change. What the minimum does have is associativity, with an identity, +∞, which leaves no mark on an answer: the cheapest of a range is the cheaper of the cheapest of its two halves. So store the answer of every half, quarter and single day.
The cheapest fare of every half, quarter and day
Text description of the diagram
The fares of days 0 to 7 are 450, 380, 520, 380, 610, 300, 700 and 340. They sit in the bottom row, in nodes 8 to 15. Each node above holds the cheaper of its two children, with the day of that fare, a tie going to the earlier day.
- Nodes 4 to 7 cover two days each: node 4 holds 380 on day 1, node 5 holds 380 on day 3, node 6 holds 300 on day 5 and node 7 holds 340 on day 7.
- Nodes 2 and 3 cover four days each: node 2 holds 380 on day 1 and node 3 holds 300 on day 5.
- Node 1, at the top, covers all eight days and holds 300 on day 5.
Three nodes are shaded: node 9, which is day 1; node 5, days 2 and 3; and node 6, days 4 and 5. Together they cover days 1 to 5 exactly, so the cheapest fare of those days is the cheapest of their three answers: 300, on day 5.
The tree can be written bottom-up, in one array of 2n nodes, or recursively from the root, which is easier to extend. A tree of minimums can also search: it finds the first day from a given day whose fare fits a budget.
The bottom-up tree, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why the query takes exactly the days of the range: the invariant
- Dry run: days 1 to 5 on the example
- The cost
- FareTracker in five languages
- Checked against a scan on 3,000 random lists
- Custom merges: the best run of consecutive days
- Bottom-up or recursive: measured
- Six bugs reproduced
- Variants
- Edge cases
- Follow-up questions, answered
- Exercise
- The cheapest fare over a range of days
- A practice set with sample answers
- Check yourself
- Interview questions
- How many nodes does a bottom-up segment tree over n values use, and where in its array do the values sit?
- A fare tracker answers range minimums with a segment tree. Product now wants, 100,000 times a day between fare changes, the earliest day at or after day s whose fare is within a traveller's budget. How do you answer it in O(log n), and how do you know it is O(log n)?
- A segment tree returns the best total of consecutive days in a range. It passes every test written by hand, but a random test finds wrong answers for some ranges. The merge function is correct. What do you look at first, and how do you fix it?
- A monitoring service keeps 10⁷ sensor readings and must handle 10⁷ point updates and 10⁷ range-minimum queries a day, within 512 MB. Which segment-tree layout and integer width do you choose, and what do the memory and time come to?
- Key takeaways
7 runnable examples (C++, Go, Java, JavaScript, Python, Python (recursive.py), Python (timing.py)) · 1 exercise with sample tests · 3 interview answers · about 25 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- Competitive Programmer's Handbook (A. Laaksonen): chapter 9, Range queries (CSES)
- Comparisons of sequences and other types (The Python Language Reference: expressions) (Python Software Foundation)
- time.process_time() (The Python Standard Library) (Python Software Foundation)
- std::pair comparison operators (cppreference.com) (cppreference.com)
- The Java Language Specification, Java SE 25: 15.17.2 Division Operator / (Oracle)
- JEP 330: Launch Single-File Source-Code Programs (OpenJDK)
- The Go Programming Language Specification: Struct types (The Go Authors)
- Infinity (JavaScript reference) (MDN Web Docs)
- AddressSanitizer (Clang documentation) (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress