Appearance
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
Difficulty: [Intermediate]CF Rating Range: 1400-1900 Prerequisites: Number Theory, Problem Pattern Recognition
Contents
- The Harmonic Number
- Five Patterns Where It Appears
- Visual: How the Sum Grows
- The Trap: Nested != Quadratic
- Practice Problems
The Harmonic Number
The
It grows as
When a loop iterates over multiples of
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
But the argument generalizes: if the outer loop ran for all
2. Counting Divisors of All Numbers 1..n
"For every d[j] for every multiple
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
After this loop, d[j] holds the number of divisors of
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
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
Contrast — the linear lookalike. Grouping elements by their exact value (
by_val[a[i]].push_back(i)and then iterating over eachby_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
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
5. Contribution by Divisor: "Sum of d(i) for i=1..n"
"Compute
This is exactly
The same "swap the order of summation" trick applies to
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 notThe gap between
The Trap: Nested != Quadratic
The most common mistake when analyzing harmonic-style loops:
"I see two nested
forloops, so it must be."
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 =
Rule of thumb: if the inner loop looks like for (j = i; j <= n; j += i), suspect
This insight extends beyond number theory. Any algorithm where you "for each bucket of size
Practice Problems
| Problem | Key Idea | Source |
|---|---|---|
| CF 236B -- Easy Number Challenge | Sieve-style divisor counting | CF |
| CF 1154G -- Minimum Possible LCM | Iterate multiples to find pairs sharing a divisor | CF |
| CF 1033D -- Divisors | Counting divisors via prime factorization and harmonic analysis | CF |
| CF 1175E -- Minimal Segment Cover | Sparse table over intervals with harmonic structure | CF |
| SPOJ DIVSUM -- Divisor Summation | Sum of proper divisors; contribution trick | SPOJ |