Back to Number Theory Dsa

Sieves & Bulk Precomputation

September 19, 2026 Wasil Zafar 11 min read

Precompute prime and factor information at scale.

Table of Contents

  1. Intuition, Vocabulary and Foundations
  2. Step-by-Step Derivations and Algorithms
  3. Implementation, Complexity and Engineering
  4. Applications, Case Studies and Exercises

Intuition, Vocabulary and Foundations

A sieve treats a range of integers like a checklist. Begin with every integer from $2$ through $n$ as a possible prime, then cross out each number once a known prime proves it composite. For example, sieving through $30$ leaves

$$ 2,3,5,7,11,13,17,19,23,29. $$

This resembles eliminating impossible candidates from a search: each discovered prime supplies a rule that rejects all of its larger multiples. A prime has exactly two positive divisors, while a composite integer can be expressed as a product of smaller positive integers.

In the Sieve of Eratosthenes, a composite $m$ is marked when a prime $p$ divides it. Marking may begin at $p^2$, because every smaller multiple $kp$ with $2\le k\lt p$ already has a prime factor smaller than $p$.

The smallest prime factor, written $\operatorname{spf}(m)$, is the least prime dividing $m$. Repeatedly dividing by this value recovers the full factorization. For example,

$$ 84\longrightarrow42\longrightarrow21\longrightarrow7\longrightarrow1 $$

extracts $2,2,3,7$, so $84=2^2\cdot3\cdot7$.

A segmented sieve marks only an interval $[L,R]$. It first computes all primes through $\lfloor\sqrt R\rfloor$, because every composite not exceeding $R$ has a prime factor no greater than its square root. Those base primes can then process one memory-sized block at a time.

Bulk precomputation can also produce arithmetic-function tables. Euler's totient $\varphi(n)$ counts the integers in $\{1,\ldots,n\}$ coprime to $n$. The Möbius function $\mu(n)$ is $0$ when a squared prime divides $n$; otherwise it is $(-1)^k$ when $n$ is a product of $k$ distinct primes.

MethodStored informationConcrete exampleAssumptionBest question
EratosthenesPrime status through $n$$25$ is marked by $5$A dense prefix fits in memoryWhich values through $n$ are prime?
Odd-only sieveStatus of odd candidatesSlot $k$ represents $2k+1$Handle $2$ separatelyHow can dense prime generation use fewer flags?
SPF table$\operatorname{spf}(m)$ for each $m$$\operatorname{spf}(84)=2$Queries stay within the tableHow can many values be factored quickly?
Segmented sieveStatus within one block $[L,R]$Find primes in $[100,130]$Base primes through $\lfloor\sqrt R\rfloor$ are knownWhich primes occur in a distant interval?
Linear sievePrime list and least factor$12$ is generated from $6\cdot2$A dense prefix is requiredHow can each composite be generated once?
Function tablesValues such as $\varphi(n)$ and $\mu(n)$$\varphi(10)=4$, $\mu(12)=0$Prime-extension recurrences are knownHow can bulk arithmetic queries be answered?

Step-by-Step Derivations and Algorithms

For the ordinary sieve, allocate flags for $0$ through $n$, mark $0$ and $1$ non-prime, and inspect candidates through $\lfloor\sqrt n\rfloor$. Whenever $p$ remains unmarked, mark

$$ p^2,\ p^2+p,\ p^2+2p,\ldots $$

as composite. If some composite $m\le n$ remained unmarked, its least prime factor $p$ would satisfy $p\le\sqrt m\le\sqrt n$, so the iteration for $p$ would mark $m$. This contradiction proves completeness. The total marking work is

$$ O\!\left(n\sum_{p\le n}\frac1p\right)=O(n\log\log n), $$

with $O(n)$ storage. An odd-only representation removes even candidates after handling $2$, improving constants without changing the asymptotic bounds.

For a block $[L,R]$, each base prime $p\le\sqrt R$ begins at

$$ \max\!\left(p^2,\left\lceil\frac Lp\right\rceil p\right). $$

The ceiling term selects the first multiple inside the block, while $p^2$ prevents the prime $p$ itself from being marked. A block of width $B$ needs $O(B)$ working storage in addition to the base-prime table.

The linear sieve maintains a prime list and an SPF table. For each $i$, it generates $ip$ in increasing prime order and stops after processing the first prime $p$ dividing $i$. Thus every composite is assigned to its least prime factor exactly once. SPF factorization then repeatedly replaces $x$ by $x/\operatorname{spf}(x)$, requiring at most $\lfloor\log_2x\rfloor$ divisions.

Sieves & Bulk Precomputation algorithm map
flowchart TD
    A["Choose range and required outputs"] --> B{"Dense prefix fits in memory?"}
    B -->|Yes| C{"Need factors or functions?"}
    B -->|No| D["Generate base primes through sqrt R"]
    C -->|No| E["Run Eratosthenes or odd-only sieve"]
    C -->|Yes| F["Run linear sieve with SPF, phi, and mu"]
    D --> G["Mark one segment at a time"]
    E --> H["Answer prime queries"]
    F --> I["Answer factor and function queries"]
    G --> H

Correctness comes from the least-prime-factor witness: every composite has a prime divisor no greater than its square root. Eratosthenes uses $O(n\log\log n)$ time, the linear sieve uses $O(n)$ time, segmented sieving uses $O(\sqrt R+B)$ storage for block width $B$, and an SPF factorization takes $O(\log x)$ divisions in the worst case.

Implementation, Complexity and Engineering

A robust implementation rejects invalid bounds, represents $0$ and $1$ explicitly as non-prime, and uses integer square roots. In a fixed-width language, the loop condition $p\le n/p$ avoids overflowing while evaluating $p^2\le n$. Packed bitsets minimize memory, whereas byte arrays often offer simpler indexing and good cache behavior.

Segment width is an engineering parameter. Blocks that are too small repeat setup work; blocks that are too large may leave cache. Benchmark representative ranges and query batches instead of selecting a size from asymptotic notation alone.

from math import isqrt


def eratosthenes(limit: int) -> list[int]:
    """Return every prime p satisfying 2 <= p <= limit."""
    if limit < 0:
        raise ValueError("limit must be non-negative")

    flags = bytearray(b"\x01") * (limit + 1)
    flags[0] = 0
    if limit >= 1:
        flags[1] = 0

    for prime in range(2, isqrt(limit) + 1):
        if flags[prime]:
            start = prime * prime
            count = (limit - start) // prime + 1
            flags[start : limit + 1 : prime] = b"\x00" * count

    return [value for value, flag in enumerate(flags) if flag]


def linear_sieve(limit: int):
    """Return primes, SPF, Euler phi, and Moebius mu through limit."""
    if limit < 0:
        raise ValueError("limit must be non-negative")

    primes: list[int] = []
    spf = [0] * (limit + 1)
    phi = [0] * (limit + 1)
    mu = [0] * (limit + 1)
    if limit >= 1:
        phi[1] = 1
        mu[1] = 1

    for value in range(2, limit + 1):
        if spf[value] == 0:
            spf[value] = value
            primes.append(value)
            phi[value] = value - 1
            mu[value] = -1

        for prime in primes:
            product = value * prime
            if product > limit:
                break
            spf[product] = prime
            if value % prime == 0:
                phi[product] = phi[value] * prime
                mu[product] = 0
                break
            phi[product] = phi[value] * (prime - 1)
            mu[product] = -mu[value]

    return primes, spf, phi, mu


def factor_with_spf(value: int, spf: list[int]) -> dict[int, int]:
    if value < 1 or value >= len(spf):
        raise ValueError("value must be positive and covered by the table")
    factors: dict[int, int] = {}
    while value > 1:
        prime = spf[value]
        if prime == 0:
            raise ValueError("SPF table is incomplete")
        factors[prime] = factors.get(prime, 0) + 1
        value //= prime
    return factors


def segmented_primes(left: int, right: int) -> list[int]:
    if left > right:
        raise ValueError("left must not exceed right")
    if right < 2:
        return []

    low = max(left, 2)
    flags = bytearray(b"\x01") * (right - low + 1)
    for prime in eratosthenes(isqrt(right)):
        first = max(prime * prime, ((low + prime - 1) // prime) * prime)
        if first <= right:
            count = (right - first) // prime + 1
            offset = first - low
            flags[offset : right - low + 1 : prime] = b"\x00" * count

    return [low + offset for offset, flag in enumerate(flags) if flag]


assert eratosthenes(1) == []
assert eratosthenes(30) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

primes, spf, phi, mu = linear_sieve(100)
assert primes == eratosthenes(100)
assert factor_with_spf(1, spf) == {}
assert factor_with_spf(84, spf) == {2: 2, 3: 1, 7: 1}
assert (phi[1], phi[10], phi[36]) == (1, 4, 12)
assert (mu[1], mu[6], mu[12], mu[30]) == (1, 1, 0, -1)
assert segmented_primes(100, 130) == [101, 103, 107, 109, 113, 127]
assert segmented_primes(-10, 3) == [2, 3]
assert segmented_primes(14, 16) == []

for invalid in (
    lambda: eratosthenes(-1),
    lambda: linear_sieve(-1),
    lambda: segmented_primes(10, 5),
    lambda: factor_with_spf(101, spf),
):
    try:
        invalid()
        raise AssertionError("expected ValueError")
    except ValueError:
        pass

print("all sieve tests passed")
ImplementationComplexityMain constraintCommon failureMitigation
Boolean Eratosthenes$O(n\log\log n)$ time, $O(n)$ flagsLanguage booleans may consume a byte or moreMemory exhaustionUse bytes, packed bits, or odd-only indexing
Odd-only sieveSame time, about half the candidate flagsSlot indices differ from represented valuesOff-by-one conversionCentralize the slot mapping and test small limits
Linear sieve with SPF$O(n)$ time, $O(n)$ integer storageSeveral arrays dominate memoryComputing unused tablesMaterialize only the arrays required by the workload
Segmented sieveBlock marking plus base-prime generationBlock width affects cache and setup costWrong first multipleUse $\max(p^2,\lceil L/p\rceil p)$ and test intervals containing $p$
Fixed-width implementationSame asymptotic boundsProducts and allocation sizes can overflowUnsafe loop or allocationUse integer square roots and checked arithmetic
SPF query service$O(\log x)$ worst-case query stepsEvery query must satisfy $1\le x\le n$Out-of-range accessValidate bounds and store the table limit

Applications, Case Studies and Exercises

Dense sieves are ideal when many queries share a moderate upper bound. One preprocessing pass can support primality checks, factorizations, divisor statistics, totients, and Möbius values. Segmented sieves target a different workload: the right endpoint may be enormous, but only a manageable interval needs to reside in memory.

A strong test oracle compares optimized output with trial division on small random ranges. Useful invariants include a strictly increasing prime list, $\operatorname{spf}(x)\mid x$, primality of every nonzero SPF value, and reconstruction of each input from its factorization.

Practice

Apply Sieves & Bulk Precomputation

Case study 1 — batch factorization. Build tables through $100$, then answer queries for $84$, $90$, $97$, and $100$.

  1. Generate the prime list and SPF table with linear_sieve(100).
  2. Follow SPF links until each value reaches $1$.
  3. Verify $84=2^2\cdot3\cdot7$, $90=2\cdot3^2\cdot5$, $97$ is prime, and $100=2^2\cdot5^2$.
  4. Reconstruct every input from its factor map.

Success criterion: every extracted factor is prime, every reconstructed product equals its input, and every lookup remains within the table.

Case study 2 — distant interval scan. Find the primes in $[100,130]$ without allocating a table for every integer through $130$.

  1. Generate base primes through $\lfloor\sqrt{130}\rfloor=11$.
  2. For each $p\in\{2,3,5,7,11\}$, begin at $\max(p^2,\lceil100/p\rceil p)$.
  3. Verify that the survivors are $101,103,107,109,113,127$.
  4. Split the interval into smaller blocks and confirm that concatenating their results gives the same list.

Success criterion: the segmented result equals trial division, including intervals whose endpoints are below $2$ or equal to a base prime.

Tiered exercises

  1. Recall: Explain why neither $0$ nor $1$ is prime.
  2. Trace: List each value newly marked while processing $p=3$ through $40$.
  3. Derivation: Prove that every composite $m$ has a prime divisor no greater than $\sqrt m$.
  4. Implementation: Replace the dense sieve with an odd-only representation and document its index mapping.
  5. Debugging: Find the error in a segmented sieve that always starts at $\lceil L/p\rceil p$.
  6. Analysis: Measure the actual memory of a packed sieve and an SPF table through $10^7$.
  7. Extension: Compute divisor counts from SPF factorizations for every integer through $n$.
  8. Challenge: Process a range too large for one block and verify its output against the dense method on smaller ranges.

Choose a dense sieve for repeated prefix queries, an SPF or linear-sieve table for bulk factorization and arithmetic functions, and a segmented sieve for distant or memory-constrained ranges. Common mistakes include treating $1$ as prime, marking before $p^2$, overflowing products, and mishandling interval endpoints; these invariants prepare the way for modular arithmetic and fast exponentiation.

Next in the Series

In Modular Arithmetic & Fast Exponentiation, we'll explore Compute safely and efficiently in residue classes..