Skip to content

The Hidden O(N log N): Harmonic Series in Algorithms

Quick Revisit

  • USE WHEN: loop iterates over multiples: for i in 1..n: for j = i, 2i, 3i, ..., n
  • INVARIANT: total iterations = n/1 + n/2 + ... + n/n = n·H(n) = O(n log n), not O(n²)
  • TIME: O(n log n) | SPACE: O(n)
  • CLASSIC BUG: assuming the nested loop is O(n²) and rejecting a correct O(n log n) solution
  • PRACTICE: 08-ladder-mixed

You see a loop: for i in 1..n: for j in i, 2i, 3i, ..., n. Your instinct says O(n2). But count the iterations: n/1+n/2+n/3++n/n=n×H(n)nlnn. That's O(nlogn).

Difficulty: [Intermediate]CF Rating Range: 1400-1900 Prerequisites: Number Theory, Problem Pattern Recognition

Contents


The Harmonic Number

The n-th harmonic number is defined as:

H(n)=k=1n1k=1+12+13++1n

It grows as lnn+γ where γ0.5772 is the Euler-Mascheroni constant. For competitive programming, the only fact you need is H(n)=Θ(logn).

When a loop iterates over multiples of i for all i=1n, the total work is:

i=1nni=nH(n)=Θ(nlogn)

This single identity explains the complexity of a surprising number of algorithms.

Five Patterns Where It Appears

1. Sieve of Eratosthenes

The classic prime sieve marks composites by iterating over multiples of each prime:

cpp
for (int i = 2; i <= n; i++) {
    if (!is_composite[i]) {           // i is prime
        for (int j = 2 * i; j <= n; j += i)
            is_composite[j] = true;   // mark multiples
    }
}

The outer loop runs for every prime pn. The inner loop for prime p does n/p work. Total: p primen/pnlnlnn by Mertens' theorem -- even better than O(nlogn) because we skip non-primes.

But the argument generalizes: if the outer loop ran for all i, not just primes, it would be nH(n)=O(nlogn).

2. Counting Divisors of All Numbers 1..n

"For every i from 1 to n, add 1 to d[j] for every multiple j of i."

cpp
vector<int> d(n + 1, 0);
for (int i = 1; i <= n; i++)
    for (int j = i; j <= n; j += i)
        d[j]++;

Inner loop for i: exactly n/i iterations. Total: i=1nn/i=Θ(nlogn).

After this loop, d[j] holds the number of divisors of j. Simple, fast, and the harmonic argument is the proof.

3. "For Each Value v, Process All Multiples of v in the Array"

This is where the harmonic argument actually shows up. For each value v, walk every multiple v,2v,3v, up to the maximum value:

cpp
for (int v = 1; v <= max_val; v++)
    for (int m = v; m <= max_val; m += v)
        if (cnt[m] > 0) /* process elements equal to m */;

Inner loop for v: max_val/v steps. Total vmax_val/v=Θ(max_vallogmax_val). This pattern arises in GCD/LCM problems where you ask "which elements are divisible by v?" or "which elements share a divisor with v?"

Contrast — the linear lookalike. Grouping elements by their exact value (by_val[a[i]].push_back(i) and then iterating over each by_val[v] once) is not harmonic — it's linear. Each element is visited once. The harmonic sum only appears when the inner loop steps through multiples; it is easy to mis-label a linear bucket pass as harmonic.

4. GCD-Based Problems: Iterate Over Possible GCD Values

"Count pairs (i,j) with gcd(ai,aj)=g." A standard approach: for each candidate g, collect all elements divisible by g, then count pairs among them.

cpp
for (int g = 1; g <= max_val; g++) {
    // collect indices where a[i] % g == 0
    for (int m = g; m <= max_val; m += g)
        if (cnt[m] > 0) /* add cnt[m] to group */;
    // count pairs in the group
}

The nested loop is our familiar g=1MM/g=O(MlogM) where M is the maximum value. Combined with Möbius inversion, this solves many "count by GCD" problems efficiently.

5. Contribution by Divisor: "Sum of d(i) for i=1..n"

"Compute i=1nσ0(i)" -- the total number of divisors across 1 to n. Flip the sum: instead of iterating over i and counting its divisors, iterate over each divisor d and count how many multiples of d lie in [1,n]:

i=1nσ0(i)=d=1nnd

This is exactly nH(n). No factoring needed -- a clean O(n) scan if you precomputed the sieve, or an O(n) computation using the Dirichlet hyperbola method.

The same "swap the order of summation" trick applies to σ1(i) (sum of divisors), ϕ(i) (Euler's totient), and similar multiplicative functions.

Visual: How the Sum Grows

n        H(n)       n*H(n)       n²
──────────────────────────────────────────
10       2.93          29         100
100      5.19         519       10000
1000     7.49        7490     1000000
10000    9.79       97876    10^8
100000  12.09     1209015    10^10    <- n*H(n) fits in time; n² does not

The gap between nlogn and n2 widens fast. At n=105, the ratio is roughly 8000×. That's the difference between AC and TLE.

The Trap: Nested != Quadratic

The most common mistake when analyzing harmonic-style loops:

"I see two nested for loops, so it must be O(n2)."

Not all nested loops are quadratic. The key is how the inner loop's range depends on the outer variable. If the inner loop iterates over multiples (step size = i), the total work is harmonic. If the inner loop always runs n times regardless of i, then it's quadratic.

Rule of thumb: if the inner loop looks like for (j = i; j <= n; j += i), suspect O(nlogn), not O(n2). Write out the sum and verify.

This insight extends beyond number theory. Any algorithm where you "for each bucket of size s, do n/s work" and the bucket sizes cover 1n yields harmonic total work. Sorting algorithms like patience sort and some cache-oblivious strategies rely on exactly this.

Practice Problems

ProblemKey IdeaSource
CF 236B -- Easy Number ChallengeSieve-style divisor countingCF
CF 1154G -- Minimum Possible LCMIterate multiples to find pairs sharing a divisorCF
CF 1033D -- DivisorsCounting divisors via prime factorization and harmonic analysisCF
CF 1175E -- Minimal Segment CoverSparse table over intervals with harmonic structureCF
SPOJ DIVSUM -- Divisor SummationSum of proper divisors; contribution trickSPOJ

See also: Number Theory for the deeper theory of multiplicative functions and Möbius inversion. The harmonic argument is the bread-and-butter complexity tool for all of them.

Built for the climb from Codeforces 1100 to 2100.