Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
You want primes in a high window [L, R] where R can be up to 10¹² but R−L is small. A plain sieve of size R won't fit in memory. The fix: sieve normally up to √R to get the 'base primes', then use only those base primes to cross out composites inside a boolean window of size R−L+1. Any composite x in [L, R] has a prime factor ≤ √x ≤ √R, so the base primes are enough to mark every composite in the window. Time is O((R−L) log log R + √R log log √R), memory is O(R−L + √R).
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you realise that primes up to √R are sufficient to sieve any window ending at R, and whether you can map a number x to its slot x − L in a shifted boolean array without off-by-one errors.
Where candidates slip
Trying to allocate a boolean array of size R (out of memory for R ~ 10¹²), forgetting the x − L index shift, or mishandling L = 1 so that 1 is reported as prime.
How to stand out
State the invariant out loud — 'every composite ≤ R has a prime factor ≤ √R' — then derive that the base sieve only needs √R, and compute the first in-window multiple as ceil(L/p)·p instead of looping up from p.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewStage 1: plain sieve up to floor(sqrt(R))for each base prime pdoneExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Sieve Everything Up To R (Baseline)
1Hint 1Explore hintHide hint
The most direct idea: run a normal Sieve of Eratosthenes up to R, then read off the primes that happen to fall in [L, R]. What goes wrong when R is 10¹²?
2Hint 2Explore hintHide hint
A boolean array of size R + 1 needs about R bytes. For R = 10¹² that's a terabyte — it simply will not allocate.
3Hint 3Explore hintHide hint
We don't actually need primality for every number up to R, only for the R − L numbers in the window. Can we restrict the work to that band?
A · Core idea
The literal reading of 'primes in [L, R]' is: sieve all primes up to R the usual way, then keep the ones that land in the window. Allocate isPrime[0..R], cross out multiples of each prime, and finally collect indices in [L, R].
Why it is correct. It is just the plain Sieve of Eratosthenes followed by a range filter. Correctness is inherited directly from the standard sieve: every composite ≤ R gets crossed out, every prime survives, and we report only those in [L, R].
Why it does not ship. The fatal flaw is space, not time. The boolean array has R + 1 entries. With R around 10¹² that is on the order of a terabyte of memory — the allocation fails before the algorithm even starts. Even when R is merely 10⁹, you are burning a gigabyte to discard all but a thin slice of it. We are computing primality for 10¹² numbers when the question only asked about the 10⁶ in the window.
The wasted-work insight. Almost all of the array — every index outside [L, R] — is computed and then thrown away. That mismatch between work done and answer needed is the signal to segment: bring the sieve to the window instead of bringing the window to a full sieve.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
L = 10, R = 20sieve to 20
cross out multiples of 2,3 up to 20
primes ≤ 20 = {2,3,5,7,11,13,17,19}[11, 13, 17, 19] (but infeasible once R is large)C · Code with teaching comments
Reference implementation previewpublic List<Long> primesInRange(long L, long R) { // Baseline: only works when R is small enough to allocate R+1 booleans. boolean[] composite = new boolean[(int) (R + 1)]; for (long p = 2; p * p <= R; p++) { if (!composite[(int) p]) {Explore the full codeHide the full code
D · Why it works and complexityO(R log log R) timeO(R) spaceOpen the invariant and proofExplore the proofHide the proof
This baseline is correct and is the right tool when R is small (say ≤ 10⁷). It fails for large R purely on memory: you allocate O(R) but only O(R − L) of it matters. Recognising that mismatch is what motivates sieving only the window.
ComplexityO(R log log R) time, O(R) space — A full sieve up to R does O(R log log R) crossing-out work and allocates an O(R) boolean array. The time is fine asymptotically, but the O(R) space is the killer: for R near 10¹² the array cannot fit in memory.
E · Edge cases and pitfalls3Allocating an array of size R when R is large — out of memory.Explore edge casesHide edge cases
Pitfalls
- Allocating an array of size R when R is large — out of memory.
- Casting R to int for the array size — overflows when R exceeds 2³¹.
- Believing it's 'fast enough' because the time bound looks fine — the blocker is space, not time.
Approach 2 of 2 · Optimal
Segmented Sieve (Optimal)
1Hint 1Explore hintHide hint
Every composite number x has a prime factor no bigger than √x. If x ≤ R, that factor is ≤ √R. What does that tell you about which primes you actually need to cross out composites in [L, R]?
2Hint 2Explore hintHide hint
Step 1: run an ordinary sieve up to √R to get the 'base primes'. Step 2: make a boolean window of size R − L + 1 where slot i represents the number L + i.
3Hint 3Explore hintHide hint
For each base prime p, find the first multiple of p that is ≥ L (use ceil(L / p) * p), but never start below p*p, then cross out that multiple, the next, and so on up to R — writing to slot m − L.
A · Core idea
Split the job into two stages. Stage 1 — base primes. Run a normal Sieve of Eratosthenes up to floor(√R) and collect those primes. Stage 2 — sieve the window. Allocate a boolean array mark of length R − L + 1, where slot i stands for the number L + i, and assume every number in the window is prime. For each base prime p, cross out its multiples that fall inside [L, R]. Whatever stays marked is prime.
Why base primes up to √R suffice. Take any composite x in [L, R]. Write x = a·b with a ≤ b; then a ≤ √x ≤ √R. The smallest prime factor of x is therefore at most √R, so it appears in our base list — and when that base prime sweeps the window it crosses x out. No composite can escape, because each has a witness ≤ √R. Primes larger than √R are never needed as erasers; they only ever appear as survivors.
Why the shift `x → x − L`. The window doesn't start at 0, so we can't index it by the number itself (that would need an array of size R). Instead slot i holds number L + i, i.e. number x lives at slot x − L. Every read and write goes through that offset. Dropping the − L is the classic bug — it silently writes out of bounds or to the wrong cell.
Why the start is `max(p*p, ceil(L/p)·p)`. We want the first multiple of p that is both inside the window (≥ L) and not smaller than p*p (multiples below p*p have a smaller prime factor that already handles them, exactly as in the plain sieve). The first multiple of p at or above L is ceil(L / p) · p, computed as ((L + p − 1) / p) · p with integer division — no scanning loop needed. Taking the max with p*p keeps the start from dipping below where this prime should begin.
Why `L == 1` needs a special line. The number 1 is not prime, but it has no prime factor, so no base prime ever crosses it out. If L == 1, slot 0 (which represents 1) survives by default and would wrongly be reported. Clear it explicitly. (If L == 0, clear slots for 0 and 1.)
Why it's overflow-safe. With R near 10¹², products like p*p and the multiple arithmetic exceed a 32-bit int. In Java everything in the multiple computation is long; in Python integers are unbounded so there is nothing to guard.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
L = 10, R = 20Array state
init
all slots assumed prime (numbers 10..20)
[11, 13, 17, 19]C · Code with teaching comments
D · Why it works and complexityO((R − L) log log R + √R log log √R) timeO(R − L + √R) spaceOpen the invariant and proofExplore the proofHide the proof
The segmented sieve is the plain sieve with the eraser set trimmed to exactly what the window needs: primes up to √R. The two ideas that make it work are independent — 'only √R primes can witness a composite ≤ R' (correctness) and 'index by offset from L' (fitting the window in memory). Master the ceil(L/p)·p start and the L = 1 edge and the rest is mechanical.
ComplexityO((R − L) log log R + √R log log √R) time, O(R − L + √R) space — Stage 1 sieves up to √R in O(√R log log √R). Stage 2: each base prime p crosses out about (R − L)/p slots, and summing 1/p over primes ≤ √R gives a log log factor, so the window work is O((R − L) log log R). Space is the window O(R − L) plus the base sieve O(√R).
E · Edge cases and pitfalls8L = 1, R = 1 — Window has one slot for the number 1; the L == 1 line clears it → empty result.Explore edge casesHide edge cases
Edge cases handled
L = 1, R = 1Window has one slot for the number 1; the L == 1 line clears it → empty result.L = R = 999999999989 (a prime)No base prime ≤ √R divides it, so its single slot survives → [999999999989].L = 8, R = 10Base primes {2,3}: 8,9,10 all crossed out → empty list, correctly no primes in [8,10].
Pitfalls
- Forgetting the x − L index shift — writes land in the wrong slot or out of bounds.
- Sizing the base sieve to R instead of √R — that reintroduces the memory blowup.
- Computing the first multiple by looping up from p instead of ceil(L/p)·p — slow when L is huge.
- Not clearing slot 0 when L == 1 — reports 1 as prime.
- Using int for p*p or multiple arithmetic in Java with R ~ 10¹² — overflow.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic List<Long> primesInRange(long L, long R) { int limit = (int) Math.sqrt((double) R) + 1; // Stage 1: plain sieve up to sqrt(R) for the base primes. boolean[] isComposite = new boolean[limit + 1]; List<Integer> basePrimes = new ArrayList<>();Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Indexing the window by the number instead of the offset
Slot i represents L + i, so number m must be written at m − L. Writing mark[m] indexes far outside the size-(R−L+1) array and throws (or corrupts memory in C-like languages).
Explore the code comparisonHide the code comparison
Indexing the window by the number instead of the offset
Slot i represents L + i, so number m must be written at m − L. Writing mark[m] indexes far outside the size-(R−L+1) array and throws (or corrupts memory in C-like languages).
Don't write this
for (long m = firstMultiple; m <= R; m += p) {
mark[(int) m] = true; // index up to R — out of bounds
}Write this instead
for (long m = firstMultiple; m <= R; m += p) {
mark[(int) (m - L)] = true; // shift number into window slot
}2Finding the first in-window multiple by scanning
Looping m from p upward until m ≥ L wastes ceil(L/p) iterations — catastrophic when L is ~10¹². Compute the first multiple in O(1) with ceil(L/p)·p.
Explore the code comparisonHide the code comparison
Finding the first in-window multiple by scanning
Looping m from p upward until m ≥ L wastes ceil(L/p) iterations — catastrophic when L is ~10¹². Compute the first multiple in O(1) with ceil(L/p)·p.
Don't write this
m = p
while m < L: # up to 10^12 / p iterations
m += p
start = max(p * p, m)Write this instead
start = max(p * p, ((L + p - 1) // p) * p) # O(1) ceil(L/p)*p3Letting 1 leak through when L = 1
1 has no prime factor, so no base prime ever crosses out its slot. Without an explicit clear, the survivor scan reports 1 as prime.
Explore the code comparisonHide the code comparison
Letting 1 leak through when L = 1
1 has no prime factor, so no base prime ever crosses out its slot. Without an explicit clear, the survivor scan reports 1 as prime.
Don't write this
mark = [False] * size
# ... cross out base-prime multiples ...
return [L + i for i in range(size) if not mark[i]] # includes 1Write this instead
mark = [False] * size
# ... cross out base-prime multiples ...
if L == 1:
mark[0] = True
return [L + i for i in range(size) if not mark[i]]Revise
Carry the pattern into the interview
Pattern to remember
Sieve base primes up to √R, then cross out their multiples inside the window [L, R]
Code pattern to memorize
base = primesUpTo(floor(sqrt(R))) # plain sieve
mark = [True] * (R - L + 1) # index i ↔ number L+i
for p in base:
start = max(p*p, ((L + p - 1) // p) * p) # first multiple of p ≥ L, but ≥ p*p
for m from start to R step p:
mark[m - L] = False
if L == 1: mark[0] = False # 1 is not prime
primes = [L + i for i, ok in enumerate(mark) if ok]Rules to remember
- 1The base primes only need to reach √R, never R — that is the whole point and the reason the window fits in memory.
- 2Offset every window index by L: number
xlives atmark[x − L]. Forgetting the− Lshift is the signature bug of this problem. - 3Start crossing out p's multiples at
max(p*p, first multiple of p ≥ L)— and compute that first multiple asceil(L / p) * p, not by scanning. - 4Handle
L == 1explicitly: 1 is not prime but no base prime crosses it out, so it leaks into the result unless you clearmark[0]. - 5Use long for
p*pand for the multiple arithmetic in Java — with R near 10¹² these products overflow a 32-bit int.
Use this pattern when
- You need every prime in a **high range** [L, R] where R is far too large to sieve directly
- The window width R − L is small (fits in memory) even though R itself is huge
- You already need a √R base sieve and just want to extend it to a shifted window
- Counting or listing primes in [L, R] for number-theory queries
Choose another pattern when
- You need all primes from 2 up to a modest n → the plain Sieve of Eratosthenes is simpler
- You only need to test **one** number for primality → trial division to √x or Miller–Rabin
- The window R − L itself is enormous (also ~10¹²) → no boolean window fits; you need a different idea
- R is small (≤ 10⁷) → just sieve directly, no segmentation needed
Interview takeaway
The segmented sieve is the plain sieve with one realisation: a composite in [L, R] is betrayed by a prime factor no larger than √R, so you never need primes beyond √R to clean the window. Precompute those base primes once, slide them across a size-(R−L) boolean buffer, and you sieve ranges that would never fit if addressed from 0. The index shift `x → x − L` and the `L == 1` special case are the two details that decide whether your code is correct.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternSegmented Sieve over a high window. Reach for it whenever you need primes (or a multiplicative property) in [L, R] with R too large to sieve directly but R − L small. The recipe — base primes up to √R, then offset crossing-out — generalises to segmented totient and segmented smallest-prime-factor tables.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Sieve of EratosthenesThe base-prime stage IS a plain sieve; segmenting just extends it to a shifted window.
- Count PrimesComing soonCount survivors instead of collecting them — same sieve, tally the window.
- Smallest Prime Factor SieveComing soonStore smallest prime factors over a range to factorise any x in the window quickly.
- Euler's Totient FunctionSegment the totient sieve the same way to compute φ over a high range.