Appearance
Complexity Analysis
Quick Revisit
- USE WHEN: You see n <= ... in constraints and need to pick algorithm family
- INVARIANT: ~10^8 simple ops per second; n*complexity must fit in time limit
- TIME: O(1) mental check | SPACE: O(1)
- CLASSIC BUG: Forgetting hidden constant factors or nested loops that multiply (e.g., O(n) loop inside O(n) loop = O(n^2))
- PRACTICE: ../12-practice-ladders/01-ladder-1100-to-1400.md
Read the constraints, know the algorithm. This is the single most important meta-skill in competitive programming—translating
Difficulty: Beginner | Prerequisites: Fast I/O and Pragmas
Big-O notation was introduced by German mathematician Paul Bachmann in 1894 in Die Analytische Zahlentheorie. Edmund Landau popularized it—which is why the family of asymptotic symbols (
The practical upshot: "Constraints are the cheat code." Every problem ships with its own hint—the input bounds reveal the intended complexity class before you understand the problem. Read
Contents
- Intuition
- Visual Intuition
- C++ STL Complexity Reference
- Implementation
- Operations Reference
- Problem Patterns & Tricks
- Contest Cheat Sheet
- Gotchas & Debugging
- Space Complexity and Stack Depth
- When Complexity Analysis Fails/Misleads
- Debug This — What's the Real Complexity?
- What Would You Do If...?
- Before You Code Checklist
- If I Had 5 Minutes
- Rating Progression
- The Mistake That Teaches
- Self-Test
- Practice Problems
- Further Reading
Intuition
Reading the Constraints Is Half the Solution
Before you write a single line of code, look at the input bounds. They tell you exactly how fast your solution must be. A 2-second time limit on Codeforces means roughly
The constraint
is not just a limit—it is the problem setter whispering the intended time complexity. Once you internalize the constraint-to-complexity mapping, you can eliminate 90% of wrong approaches in 10 seconds, before writing a single line of code.
Counting Loop Iterations
Single loop—nothing surprising:
cpp
for (int i = 0; i < n; i++) { ... } // invariant: processed i elements so far -> O(n)Independent nested loops—multiply:
cpp
for (int i = 0; i < n; i++) // invariant: completed i full sweeps of j
for (int j = 0; j < m; j++) { ... } // invariant: processed i*m + j pairs -> O(n * m)Dependent inner loop—this one trips people up. Consider:
cpp
for (int i = 0; i < n; i++) // invariant: completed i iterations
for (int j = i; j < n; j++) { ... } // invariant: j starts at i, runs n-i timesWhen
The constant
The Harmonic Sum: Why for(i=1..n, j+=i) Is
This pattern is everywhere—sieve of Eratosthenes, divisor enumeration, computing Euler's totient:
cpp
for (int i = 1; i <= n; i++) // invariant: processed multiples of 1..i-1
for (int j = i; j <= n; j += i) { ... } // invariant: visits floor(n/i) multiples of iFor a fixed
This is why the Sieve of Eratosthenes runs in
Harmonic Sum Visual Proof -- Why
The harmonic series
The function
Similarly, the rectangles sit below the curve shifted by one:
Therefore
text
1/x
^
|
1 +##
|# ##
|# ###
|# | ####
|# | | ######
|# | | | ###########
+----+--+---+-----+-----------+---> x
1 2 3 4 5 ... n
Bars -- rectangles of height 1/k
Curve under bars -- integral of 1/x -- ln(n)
Sum of bar areas -- H_n ~ ln(n)How this applies to the sieve of Eratosthenes:
The sieve marks composites by iterating over multiples of each prime
cpp
for (int p = 2; p <= n; p++) { // invariant: all composites with smallest factor < p are marked
if (!is_prime[p]) continue;
for (int j = p*p; j <= n; j += p) // invariant: marks multiples of p starting at p^2
is_prime[j] = false;
}Work for prime
This is even better than
Recursion Trees Tell You Everything
Forget formulas for a moment. Draw the tree.
Naive Fibonacci—fib(n) = fib(n-1) + fib(n-2):
Each call spawns two children. The tree has depth
With memoization, each value
Binary search—each call eliminates half the array:
One recursive call, problem size halved. The tree is a single chain of depth
Merge sort—two calls on halves, then
Two children per node, each on
The Master Theorem (Just Three Cases)
For recurrences of the form
| Case | Condition | Result | Who dominates? |
|---|---|---|---|
| 1 | Root (top-level work) | ||
| 2 | Every level tied | ||
| 3 | Leaves |
Examples:
Binary search:
. . Case 2 . ✓ Merge sort:
. . Case 2 . ✓ Karatsuba multiplication:
. . Case 3 . Faster than the schoolbook . Naive matrix multiply (Strassen-like split with 8 subproblems):
. Case 3 . No improvement (Strassen reduces from 8 to 7).
text
Work per level in the recursion tree
Case 1 -- d > log_b(a) Case 3 -- d < log_b(a)
Root dominates Leaves dominate
Level 0 |############| Level 0 |## |
Level 1 |######## | Level 1 |#### |
Level 2 |##### | Level 2 |####### |
Level 3 |### | Level 3 |############|
+------------+ +------------+
Result -- n^d Result -- n^log_b(a)
Case 2 -- d = log_b(a)
All levels tied
Level 0 |############|
Level 1 |############|
Level 2 |############|
Level 3 |############|
+------------+
Result -- n^d * log nAmortized: When the Worst Case Lies
Sometimes a single operation is expensive, but it can't happen often enough to matter.
Two-pointer / sliding window:
cpp
int j = 0;
for (int i = 0; i < n; i++) { // invariant: window starts at i
while (j < n && ok(i, j)) j++; // invariant: j only increases across all iterations
// process window [i, j)
}The inner while looks scary—isn't this j only moves forward. Across all iterations of the outer loop, j increments at most
Stack-based problems (next greater element, etc.):
cpp
stack<int> st;
for (int i = 0; i < n; i++) { // invariant: elements 0..i-1 processed
while (!st.empty() && a[st.top()] <= a[i])
st.pop(); // invariant: each element popped at most once total
st.push(i); // invariant: each element pushed exactly once
}Each element is pushed once and popped at most once. Total push + pop operations while can pop many elements in a single iteration.
Dynamic array doubling (std::vector):
Most push_back calls are
text
Array doubling -- amortized O(1)
push 1 2 3 4 5 6 7 8 9
cap 1 2 4 4 8 8 8 8 16
* * * * * <-- realloc here
cost 1 1 2 4 8 <-- copy cost
Total copies -- 1+1+2+4+8 -- 16 -- 2n-1
Spread over n pushes --> O(1) eachThe Constraint Table You Should Memorize
This is the single most useful thing in this file. Burn it into memory:
| Constraint | Target Complexity | Algorithm Families |
|---|---|---|
| brute force, permutations, backtracking | ||
| bitmask DP, subset enumeration | ||
| 4D DP (rare) | ||
| Floyd-Warshall, matrix chain DP | ||
| 2D DP, quadratic sorting, brute pairs | ||
| sqrt decomposition, Mo's algorithm | ||
| sorting, segment trees, BIT, merge sort | ||
| prefix sums, linear scan, two-pointer |
These are approximate.
text
Operations at n = 100000 10^8 budget
|
O(n) * | 10^5 SAFE
O(n log n) *** | 2*10^6 SAFE
O(n sqrt n) ************** | 3*10^7 SAFE
O(n^2) ###############################--> 10^10 TLE
+--------------------------+
0 10^8Worked Example: Picking Your Approach
Problem: Given an array of
integers, find the longest subarray with sum .
Step 1:
Step 2:
Step 3: Think of
Step 4: If all elements are non-negative, two-pointer gives
You picked the algorithm before writing any code, just from the constraint.
What the Code Won't Teach You
Use the constraint table as the first step, not the last. The correct workflow is: read problem, read constraints, decide target complexity, think of algorithms in that class, pick one. Most people do it backwards: think of an algorithm, code it, then check if it is fast enough. That is a recipe for wasted time.
text
WRONG workflow CORRECT workflow
+--------------+ +--------------+
| Read problem | | Read problem |
+------v-------+ +------v-------+
| |
+------v-------+ +------v-------+
| Think of an | | Read n |
| algorithm | | Check table |
+------v-------+ +------v-------+
| |
+------v-------+ +------v-------+
| Code it | | Target cplx |
+------v-------+ +------v-------+
| |
+------v-------+ +------v-------+
| Check if | | Pick algo in |
| fast enough | | that class |
+------v-------+ +------v-------+
| |
TLE -- start over Code + ACtext
Complexity Spectrum at n = 10^5
n n log n n sqrt n n^2
10^5 2*10^6 3*10^7 10^10
| | | |
v v v v
-+-----------+--------------+-------------+---->
| | | |
| FAST | STANDARD | VIABLE | TLE
| | | |
+<-- safe -->+<--- safe --->+<-- safe -->+##TLE##
^
|
Do not skip this range"Will it TLE?" requires estimating the constant, too. The same
text
Same O(n log n) -- different constants
BIT -- 10ns per op
10^6 * 20 * 10ns -- 200ms SAFE
Seg tree -- 100ns per op
10^6 * 20 * 100ns -- 2000ms TIGHTThe analysis above was all reasoning about loops and recurrences. Sometimes a picture makes the argument click faster—here are the key complexity classes drawn out.
Visual Intuition
Recursion Tree: Naive Fibonacci (Exponential Blowup)
text
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) . . . .
/ \
. .
Level 0: 1 call = 1
Level 1: 2 calls = 2
Level 2: 4 calls = 4
Level 3: up to 8 calls = 8
...
Level n: up to 2^n calls
Total ~ 2^n --> O(2^n)Each unique fib(k) is recomputed many times. Memoization collapses this entire tree into a single chain of
Recursion Tree: Merge Sort ( Work)
text
Level 0: [ 1 5 3 7 2 6 4 8 ] merge cost: n = 8
/ \
Level 1: [1 5 3 7] [2 6 4 8] merge cost: 4 + 4 = 8
/ \ / \
Level 2: [1 5] [3 7] [2 6] [4 8] merge cost: 2+2+2+2 = 8
/ \ / \ / \ / \
Level 3: [1][5] [3][7] [2][6] [4][8] merge cost: 0 (base)
log2(8) = 3 levels
Each level does O(n) work
Total: O(n log n)Harmonic Sum: Which Multiples Get Visited
For j the inner loop touches:
text
i=1: * * * * * * * * * * * * (12 hits)
i=2: * . * . * . * . * . * . ( 6 hits)
i=3: * . . * . . * . . * . . ( 4 hits)
i=4: * . . . * . . . * . . . ( 3 hits)
i=5: * . . . . * . . . . * . ( 2 hits)
i=6: * . . . . . * . . . . . ( 2 hits)
i=7: * . . . . . . . . . . . ( 1 hit )
...
j: 1 2 3 4 5 6 7 8 9 10 11 12
Total = 12 + 6 + 4 + 3 + 2 + 2 + 1 + 1 + 1 + 1 + 1 + 1
= 35 (vs n^2 = 144)
~ n * ln(n) = 12 * 2.48 ~ 30 (close!)C++ STL Complexity Reference
Know the cost of what you call. A single set::find inside a loop turns
| Operation | Container / Algorithm | Complexity |
|---|---|---|
sort(a, a+n) | <algorithm> | |
stable_sort(a, a+n) | <algorithm> | |
nth_element(a, a+k, a+n) | <algorithm> | |
lower_bound(a, a+n, x) | <algorithm> | |
unique(a, a+n) | <algorithm> | |
next_permutation | <algorithm> | |
__builtin_popcount(x) | GCC built-in | |
vector::push_back | <vector> | |
vector::insert (middle) | <vector> | |
set/map::find | <set> / <map> | |
set/map::insert | <set> / <map> | |
unordered_set/map::find | <unordered_set> | |
priority_queue::push/pop | <queue> | |
deque::push_front/back | <deque> | |
string::substr(pos, len) | <string> | |
string::find(pattern) | <string> | |
bitset<N>::count() | <bitset> |
Trap: unordered_map is map if you're unsure.
Implementation
Complexity Benchmarker
Paste this into a local file to build intuition for how fast each complexity class actually runs:
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
auto bench = [](string label, auto fn) {
auto t0 = chrono::high_resolution_clock::now();
fn();
auto t1 = chrono::high_resolution_clock::now();
double ms = chrono::duration<double, milli>(t1 - t0).count();
cout << label << ": " << fixed << setprecision(1) << ms << " ms\n";
};
const int N = 1e6;
volatile int sink = 0;
bench("O(n) n=1e6", [&]() {
for (int i = 0; i < N; i++) sink += i; // invariant: summed 0..i-1
});
bench("O(n logn) n=1e6", [&]() {
vector<int> v(N);
iota(v.begin(), v.end(), 0);
sort(v.begin(), v.end(), greater<>()); // invariant: v sorted descending after
});
const int M = 5000;
bench("O(n^2) n=5000", [&]() {
for (int i = 0; i < M; i++) // invariant: processed i outer iterations
for (int j = i; j < M; j++) sink += j; // invariant: summed i..j-1
});
const int K = 500;
bench("O(n^3) n=500", [&]() {
for (int i = 0; i < K; i++) // invariant: processed i outer iterations
for (int j = 0; j < K; j++) // invariant: processed i*K + j middle iterations
for (int k = 0; k < K; k++) sink += k; // invariant: summed 0..k-1
});
return 0;
}Will-It-TLE Estimator
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
// Rule of thumb: ~10^8 simple ops per second on CF judges.
// Time limit is usually 1-2 seconds => budget ~2*10^8 ops.
auto estimate = [](long long ops) -> string {
// 2 * 10^8 ops in 2 seconds is our budget
const long long BUDGET = 2e8;
if (ops <= BUDGET) return "SAFE";
if (ops <= 5 * BUDGET) return "TIGHT";
return "TLE";
};
long long n2 = (long long)n * n;
long long nlogn = (long long)n * (int)(log2(n) + 1);
long long nsqrtn = (long long)n * (int)sqrt(n);
cout << "n = " << n << "\n";
cout << "O(n): " << n << " ops -> " << estimate(n) << "\n";
cout << "O(n log n): " << nlogn << " ops -> " << estimate(nlogn) << "\n";
cout << "O(n sqrt n): " << nsqrtn << " ops -> " << estimate(nsqrtn) << "\n";
cout << "O(n^2): " << n2 << " ops -> " << estimate(n2) << "\n";
// Example: n=200000
// O(n): 200000 -> SAFE
// O(n log n): 3400000 -> SAFE
// O(n sqrt n): 89442000 -> SAFE
// O(n^2): 40000000000 -> TLE
return 0;
}Operations Reference
The Constraint → Complexity Table
This is the expanded reference. When you see a constraint during a contest, jump to the row and read across—the algorithm family column narrows your search instantly. See Recursion and Backtracking for details on the brute-force families and STL Algorithms for built-in
| Max ops ( | Target | Typical algorithms | |
|---|---|---|---|
| brute permutations with checking, TSP brute force | |||
| brute permutations, backtracking, all orderings | |||
| bitmask DP with transitions, Hamiltonian path DP | |||
| bitmask DP, subset enumeration, SOS DP | |||
| subset DP, inclusion-exclusion over subsets | |||
| meet-in-the-middle (split into two halves of 20) | |||
| 4D DP, small brute force, Gaussian elimination | |||
| DP on intervals, matrix exponentiation base | |||
| Floyd-Warshall, matrix chain DP, network flow (small) | |||
| 2D DP, brute pairs, quadratic sorting, LCS | |||
| bubble/insertion sort, 2D DP with small constant | |||
| sqrt decomp, Mo's algorithm, block decomposition | |||
| merge sort tree, CDQ divide-and-conquer, persistent seg tree | |||
| sort, seg tree, BIT, merge sort, convex hull trick, CDQ | |||
| linear sieve, suffix array (SA-IS), Euler tour | |||
| prefix sums, two-pointer, linear DP, simple iteration | |||
| -- | prime factorization, Pollard rho, baby-step giant-step | ||
| -- | number-theoretic functions, prime counting (Lucy DP) | ||
| -- | binary search, matrix exponentiation, fast exponentiation, math |
Operations Per Second Rule of Thumb
On a typical Codeforces judge (and competitive programming judges in general):
- Simple operations (add, compare, array access):
/ sec - With cache misses (random memory access):
/ sec - Heavy operations (modular division,
maplookups):/ sec
A 2-second time limit on CF gives you roughly
Problem Patterns & Tricks
Constraint Reading → Algorithm Selection
The first thing you should do on every problem: read
CF 1462D - Add to Neighbour and Remove:
, so is fine. A nested loop over segments works.
Is This Really ?
Many solutions look quadratic but are actually linear or
- Does the inner variable only increase across outer iterations? Amortized
. - Does each element get processed a bounded number of times?
.
CF 1237D - Balanced Playlist: Two-pointer solution with inner loop that appears
but is .
The Sweet Spot
When
CF 86D - Powerful Array: Classic Mo's algorithm. Offline range queries in
.
When Is Fine
Don't panic about extra log factors. At
Both are well within budget. Merge sort tree, persistent segment tree with binary search—the extra log is almost always acceptable.
The Harmonic Trick in Disguise
Any time a problem involves iterating over divisors or multiples, expect the harmonic sum. This includes:
- Sieve of Eratosthenes variants
- Counting divisors for each number
- GCD/LCM-related preprocessing
Total work:
CF 1154G - Minimum Possible LCM: Iterate over all multiples of each value. Harmonic sum gives
.
With Meet-in-the-Middle
If
CF 888E - Maximum Subsequence:
, modular sum. Classic meet-in-the-middle.
Contest Cheat Sheet
text
+------------------------------------------------------------------+
| COMPLEXITY QUICK REFERENCE |
+------------------------------------------------------------------+
| CONSTRAINT -> COMPLEXITY -> THINK OF |
|------------------------------------------------------------------|
| n <= 10 O(n!) permutations, brute force |
| n <= 20 O(2^n) bitmask DP, subsets |
| n <= 500 O(n^3) Floyd, matrix DP, 3 nested loops |
| n <= 5000 O(n^2) 2D DP, brute pairs |
| n <= 1e5 O(n*sqrt(n)) Mo's, sqrt decomposition |
| n <= 1e6 O(n log n) sort, seg tree, BIT, divide & conquer |
| n <= 1e8 O(n) prefix sums, two-pointer, greedy |
|------------------------------------------------------------------|
| BUDGET: ~10^8 simple ops per second |
| 2s TL => ~2*10^8 ops budget (with safety margin) |
|------------------------------------------------------------------|
| AMORTIZED O(n): |
| two-pointer (j only moves forward) |
| stack (each elem pushed/popped once) |
| vector doubling (geometric sum = 2n) |
|------------------------------------------------------------------|
| HARMONIC SUM: for(i=1..n) for(j=i;j<=n;j+=i) => O(n log n) |
|------------------------------------------------------------------|
| MASTER THEOREM: T(n) = a*T(n/b) + O(n^d) |
| d > log_b(a): O(n^d) d = log_b(a): O(n^d logn) |
| d < log_b(a): O(n^(log_b(a))) |
|------------------------------------------------------------------|
| STL COSTS: sort O(nlogn) set/map O(logn) umap O(1) avg |
| nth_element O(n) push_back O(1) amort |
+------------------------------------------------------------------+Gotchas & Debugging
Hidden Constants Kill
Big-O hides constants. std::map is unordered_map is
Rule of thumb: if you can use an array, use an array. Replace map<int,int> with a plain array when keys are bounded.
Can Lose to
A segment tree with lazy propagation (
Codeforces TLs Are Generous -- But Not Infinite
CF usually sets TL = 2-3x the intended solution's runtime. An
Nested STL Calls Stack Complexity
cpp
for (int i = 0; i < n; i++)
s.erase(s.find(a[i])); // O(log n) per iteration => O(n log n) total // invariant: a[0..i-1] removed from sThis is fine. But watch out for:
cpp
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) // invariant: checked i*n + j pairs so far
if (s.count(a[i] + a[j])) ans++; // O(n^2 log n) -- ouchEach count is unordered_set to drop it to
Integer Overflow ≠ Complexity, But Related
When computing long long:
cpp
long long ops = (long long)n * n; // not int * intIf your estimate overflows, your solution will definitely TLE anyway. But also watch for overflow inside your actual solution—any time you multiply two values that can be up to int.
Mental Traps
Trap: Reading constraints without computing the implied complexity. Many competitors see
text
WRONG RIGHT
----- -----
n = 200000 n = 200000
| |
v v
Pick any algorithm 200000^2 = 4*10^10
| |
v 4*10^10 >> 10^8 budget
Code n^2 solution |
| v
v Target: n log n
+----------+ |
|## TLE ##| v
+----------+ +----------+
|## AC ##|
+----------+Trap: Assuming "O(n log n) means safe" without computing.
text
n = 10^6 n log n n log^2 n
| |
v v
20 * 10^6 400 * 10^6
| |
+----v----+ +----v----+
| 2*10^7 | | 4*10^8 |
| SAFE | | TIGHT |
+---------+ +---------+
|
v
heavy constant --> TLETrap: Not recognizing the harmonic sum pattern. A nested loop where the inner loop runs
text
WRONG analysis RIGHT analysis
-------------- ---------------
for i in 1 to n for i in 1 to n
for j from i step i for j from i step i
| |
v v
Two nested loops inner runs n/i times
| |
v v
Must be n^2 sum = n/1 + n/2 + ... + n/n
| = n * H(n)
v |
# Reject valid # v
# n log n # n log n <-- SAFE
# approach #So far we've focused on how many operations your code runs. But the judge also limits how much memory you can use.
Space Complexity and Stack Depth
Time isn't the only resource the judge measures. Memory Limit Exceeded (MLE) is just as fatal as TLE, and harder to debug because you rarely get a helpful message—just a verdict.
Auxiliary Space vs. Input Space
Input space is the memory the input itself occupies—the array you read in, the graph's adjacency list. You can't avoid this.
Auxiliary space is the extra memory your algorithm uses beyond the input. When a problem says "solve in
| Algorithm | Total Space | Auxiliary Space |
|---|---|---|
| Merge sort | ||
| Quicksort (in-place) | ||
| Heap sort | ||
| BFS on graph | ||
| DFS on graph (recursive) |
In competitive programming, the distinction usually does not matter—you care about total memory. But know the vocabulary for interview-style problems.
Why Space Matters
Typical Codeforces memory limit: 256 MB. Some problems give 64 MB or 512 MB, so always check.
Quick reference for a single allocation:
| Declaration | Calculation | Memory |
|---|---|---|
int a[1000000] | 4 MB | |
long long a[1000000] | 8 MB | |
int a[5000][5000] | 100 MB—tight! | |
long long a[5000][5000] | 200 MB—likely MLE |
Key sizes: sizeof(int) = 4, sizeof(long long) = 8, sizeof(double) = 8, sizeof(bool) = 1 (but vector<bool> is 1 bit per entry).
Counting Memory Usage
| Structure | Memory Formula |
|---|---|
Static array T a[n] | sizeof(T) * n bytes |
vector<T> of size | sizeof(T) * n + ~24 bytes overhead |
| Adjacency list (undirected, | sizeof(pair<int,int>) = |
| Segment tree over | sizeof(node) |
| Recursion of depth |
Rule of thumb: add up all your large arrays and containers. If the total is within ~80 % of the memory limit, you're usually safe. The remaining 20 % covers code, constants, and OS overhead.
Common MLE Causes (and Fixes)
2-D DP with both dimensions
cpp
// dp[100000][100000] = 10^{10} ints = 40 GB -- impossible
int dp[100000][100000]; // MLEFix—rolling array: if each row only depends on the previous row, keep just two rows:
cpp
int dp[2][100000]; // 0.8 MB -- fine
for (int i = 1; i <= n; i++) { // invariant: dp[prv] holds row i-1 results
int cur = i & 1, prv = cur ^ 1;
for (int j = 0; j < m; j++) // invariant: dp[cur][0..j-1] already computed
dp[cur][j] = /* transition using dp[prv][...] */;
}Adjacency matrix for large
cpp
bool adj[100000][100000]; // 10^{10} bytes = 10 GB -- impossibleFix: use an adjacency list. It uses
Storing all BFS/DFS states
Pushing every intermediate state into a vector can blow up memory when the state space is large. Keep only what you need (e.g., distances, not full paths).
MLE Estimation Table
Use this during contests to quickly check whether an array fits:
text
Memory Limit | Max int (4 B) elements | Max long long (8 B) elements
-------------+------------------------+-----------------------------
64 MB | 16 x 10^6 | 8 x 10^6
256 MB | 64 x 10^6 | 32 x 10^6
512 MB | 128 x 10^6 | 64 x 10^6
1 GB | 256 x 10^6 | 128 x 10^6Formula:
Stack Depth and Recursion
The call stack is a separate, limited memory region:
- Default stack size: ~1-8 MB (platform and judge dependent).
- Each recursive call adds a stack frame: local variables + return address + saved registers ~= 50-200 bytes, depending on how many locals you declare.
- Max safe recursion depth: roughly
- calls.
A DFS on a path graph of
Stack depth rules of thumb:
| Recursion Depth | Risk Level | Notes |
|---|---|---|
| Safe | Default stack handles this on all judges | |
| Borderline | Works on most CF judges, may fail locally | |
| Dangerous | Needs stack size increase or iterative conversion | |
| Almost certain crash | Must convert to iterative |
Example: DFS on a tree with
Fixes:
cpp
// Fix 1: Increase stack size via compiler pragma (works on CF with G++)
// Add at the top of your file:
#pragma comment(linker, "/STACK:1000000000") // Windows MSVC
// On Linux, compile with: g++ -O2 -Wl,-z,stacksize=268435456 sol.cpp
// Fix 2: On Linux systems (local testing), before running:
// $ ulimit -s unlimited
// This removes the stack size limit for the current shell session.
// NOTE: This does NOT work on online judges -- it's for local testing only.
// Fix 3: Convert recursion to an explicit stack (always works, recommended)
void dfs_iterative(int start) {
stack<int> st;
st.push(start);
while (!st.empty()) { // invariant: st contains unvisited frontier
int v = st.top(); st.pop();
if (visited[v]) continue;
visited[v] = true;
for (int u : adj[v]) // invariant: all neighbors of v get queued
if (!visited[u]) st.push(u);
}
}
// Fix 4: For DFS specifically, use a parent-tracking iterative version
// that preserves DFS order (unlike the stack version above which reverses it)
void dfs_iterative_ordered(int root) {
struct Frame { int v, parent, child_idx; };
stack<Frame> st;
st.push({root, -1, 0});
visited[root] = true;
while (!st.empty()) { // invariant: simulates call stack
auto& [v, par, ci] = st.top();
if (ci < (int)adj[v].size()) {
int u = adj[v][ci++];
if (u != par && !visited[u]) {
visited[u] = true;
st.push({u, v, 0}); // "recurse" into child
}
} else {
st.pop(); // "return" from this call
}
}
}When to convert recursion to iteration:
- Tree/graph with
nodes and potential chain/path structure - DP with recursion depth proportional to
where - Any problem where you get Runtime Error (RE) and suspect stack overflow
- When the judge does not support stack size pragmas (rare but happens on some OJs)
Bottom line: estimate memory the same way you estimate time. Add up your arrays, check the limit, and watch your recursion depth. See Recursion and Backtracking for more on converting recursive algorithms to iterative ones.
When Complexity Analysis Fails/Misleads
Complexity analysis is your most powerful tool, but it has blind spots. Know when it lies to you:
Constants matter more than you think. An std::map (
Cache effects can dominate. A 2D array traversed column-by-column is
cpp
// Same O(n^2) -- but wildly different wall-clock time for large n
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
a[i][j] += 1; // row-major: FAST (sequential memory access)
for (int j = 0; j < n; j++)
for (int i = 0; i < n; i++)
a[i][j] += 1; // column-major: SLOW (cache thrashing)Worst case vs. average case. Quicksort is std::sort in GCC uses introsort: quicksort that falls back to heapsort if recursion gets too deep, giving worst-case
Amortized ≠ per-operation guarantee.vector::push_back is
The constant in
Debug This -- What's the Real Complexity?
Determine the actual time complexity of each snippet. Answers are below (try before peeking!).
Snippet 1:
cpp
int ans = 0;
for (int i = 1; i <= n; i++) // outer: 1 to n
for (int j = 1; j <= n; j *= 2) // inner: 1, 2, 4, 8, ...
ans += i + j;Snippet 2:
cpp
int ans = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = j; k < n; k++)
ans++;Snippet 3:
cpp
int ans = 0;
for (int i = 1; i <= n; i++) {
int j = i;
while (j > 0) {
ans += j % 10;
j /= 10;
}
}Snippet 4:
cpp
vector<int> v;
for (int i = 0; i < n; i++) {
v.push_back(i);
if (v.size() > 100)
v.erase(v.begin()); // erase from front
}Answers (click to reveal)
Snippet 1:
Snippet 2:
Snippet 3:
Snippet 4: v.size() is capped at 101. Each erase(v.begin()) shifts at most 100 elements. So total work is erase being "O(size)" in general.
What Would You Do If...?
Scenario 1: You read the constraints and see
, . The problem asks for range sum queries after point updates. What complexity do you target? Answer:
. This screams BIT or segment tree. brute force does ops—instant TLE. Each query and update must be .
Scenario 2:
, and you need to find if any subset sums to a target . Your first instinct is bitmask DP over all subsets, but —TLE. Answer: Meet-in-the-middle. Split into two halves of 20. Enumerate
subsets for each half. Sort one half, binary search from the other. Total: .
Scenario 3:
and you need to count the number of divisors for every integer from 1 to . A naive approach checks all divisors up to for each . Answer: Use the "iterate over multiples" pattern: for each
from 1 to , increment the divisor count for . This is the harmonic sum pattern: total ops . Much faster than .
Before You Code Checklist
Run through this list before writing a single line on any contest problem:
- [ ] Read
: What is the largest input dimension? Check all constraints ( , , , , ). - [ ] Compute the budget:
at this is how many ops? Compare to . - [ ] Identify the complexity class: Use the constraint table. What family of algorithms fits?
- [ ] Check space: Will your arrays fit in the memory limit? Especially watch 2D arrays and adjacency matrices.
- [ ] Watch for hidden factors: Does your approach use
set/map(add)? Multiple test cases (multiply by )?
If I Had 5 Minutes
Speed reference—the five things to know if you read nothing else:
rule: A program does ~ simple ops per second. Budget = time limit x . - Constraint → Complexity:
. . . - Harmonic sum:
for(i=1..n) for(j=i; j<=n; j+=i)is, not . - Amortized
: If a variable only moves forward across all iterations, the total is . - Memory:
int a[N]usesbytes. 256 MB fits ~ ints.
Rating Progression
How complexity analysis skills evolve as you grow on Codeforces:
CF 1200 (Beginner): You can look up the constraint table and pick
CF 1500 (Intermediate): You recognize amortized
CF 1800 (Advanced): You spot the harmonic sum pattern in divisor/multiple problems instantly. You know when
CF 2100+ (Expert): You reason about complexity with multiple parameters (
The Mistake That Teaches
It was a Div 2 C. The constraint said
, and I had a clean two-pointer solution—but inside the inner loop I called s.count(x)on astd::set. "It's just a lookup," I thought. "O(1)." Wrong.set::countis, not . My "O(n)" solution was actually . That time it still passed (the extra log fit in budget). But two weeks later, the same mistake on a problem with
and tight TL gave me TLE. I spent 40 minutes optimizing the wrong thing before realizing the issue was one function call. Lesson: Always mentally multiply the complexity of STL operations into your total. An innocent
.count(),.find(), or.substr()inside a loop can silently add a log factor or worse. Refer to the STL Containers reference when in doubt.
Self-Test
Before moving to practice problems, verify you can do these without looking back at the text:
- [ ] Given
, immediately state whether and are safe, tight, or TLE for a 2-second time limit. - [ ] Explain why
for(i=1..n) for(j=i; j<=n; j+=i)is, not , using the harmonic sum argument. - [ ] For the two-pointer pattern where
jonly moves forward, give a rigorous amortized argument that total operations are. - [ ] Name three STL operations and their complexities, including one commonly mistaken for
but actually . - [ ] Given
and an algorithm at 50 ns per operation, compute whether it passes a 2-second time limit. - [ ] Convert the constraint
into a target complexity class and name two algorithm families that fit. - [ ] Explain why
vector::push_backisamortized using the geometric series argument.
Practice Problems
Problems ordered by difficulty. CF ratings shown where applicable.
| # | Problem | Key Skill | CF Rating | Difficulty |
|---|---|---|---|---|
| 1 | CSES - Missing Number | ~800 | **** | |
| 2 | CF 1462D - Add to Neighbour and Remove | 1100 | **** | |
| 3 | CSES - Sum of Two Values | Two-pointer or hash map -> | ~1200 | **** |
| 4 | CF 1251C - Minimize the Integer | Greedy | 1200 | **** |
| 5 | CF 1237D - Balanced Playlist | Two-pointer amortized | 1800 | **** |
| 6 | CF 86D - Powerful Array | Mo's algorithm | 2000 | **** |
| 7 | CF 888E - Maximum Subsequence | Meet-in-the-middle | 1800 | **** |
| 8 | CF 1154G - Minimum Possible LCM | Harmonic sum trick | 1900 | **** |
| 9 | AtCoder ABC 169F - Knapsack for All Subsets | Counting DP—constraint dictates | ~2000 | **** |
Further Reading
- cp-algorithms: Asymptotic Analysis—reference for complexity proofs
- Codeforces Blog: How to Read Problem Constraints—classic blog post on constraint-to-algorithm mapping
- KACTL—see their complexity annotations on every algorithm
- Codeforces Blog: [Tutorial] Sieve of Eratosthenes—harmonic sum analysis in depth
- cp-algorithms: Master Theorem—formal statement and proofs
Next Up: Arrays and Strings—where you apply these complexity skills to the most common data structures in competitive programming. Every array technique has a complexity signature; now you know how to read it.