Algorithms from zero
Reading the constraints
You now know Big-O notation and the seven complexity classes. But how do you choose which complexity is right for a problem? The answer: read the input constraints. Before you write a single line of code, the problem statement tells you the limits. Those limits point directly at the complexity class you must aim for. This lesson teaches you to decode that signal and use it to eliminate impossible approaches before you even start.
After this lesson you can read a problem’s input constraints (especially n, the size of the input), estimate how many operations your algorithm can afford (~10⁸ per second), match input bounds to required complexity classes (e.g. n ≤ 10⁶ → O(n) or O(n log n)), and use that mapping to decide which algorithmic approach to attempt.
The golden rule: computers do roughly 10⁸ (100 million) simple operations per second.
Most online judges (LeetCode, Codeforces, etc.) allow 1–2 seconds per test. So you can afford about 10⁸ – 2×10⁸ operations. This is your time budget.
The mapping: input bound n tells you which complexity you need.
Here is the standard table that competitive programmers use:
| Input size n | Feasible complexity | Example |
|---|---|---|
| n ≤ 10–12 | O(n!) or O(2ⁿ) | Brute-force all subsets or permutations |
| n ≤ 20–25 | O(2ⁿ) | Recursive exploration, bitmask DP |
| n ≤ 500 | O(n³) | Triple nested loop, Floyd–Warshall |
| n ≤ 5,000 | O(n²) | Nested loop, bubble sort, simple DP |
| n ≤ 10⁶ | O(n log n) or O(n) | Merge sort, binary search, single loop |
| n ≥ 10⁷ | O(n) or O(log n) | Scan once per item, or logarithmic lookup |
How to use it: work backwards from the constraint.
When you read “n ≤ 10⁶”, you know that:
- O(n²) is impossible — that would be 10¹² operations, way over budget.
- O(n log n) is feasible — that is ~2×10⁷ operations, well within budget.
- O(n) is also feasible — ~10⁶ operations, trivial.
So if n ≤ 10⁶, your algorithm must be at least as fast as O(n log n). Slower approaches (like O(n²)) are ruled out before you start.
The constraint → approach mapping is the bridge from analysis to design.
Instead of “I have no idea where to begin”, the constraint tells you: “You need a Θ(n) scan OR a Θ(n log n) divide-and-conquer (like binary search or merge sort).” This is not the full solution, but it narrows the search space dramatically.
Why constraints matter: a real example.
Suppose the problem asks: “Count pairs (i, j) where i < j and arr[i] + arr[j] = target.”
Your first instinct might be: “Nested loop. For each i, scan all j > i.” That is O(n²).
But then you read the constraint: n ≤ 100,000.
O(n²) would be 10¹⁰ operations. Timeout. Ruled out.
So you must use O(n log n) or O(n). The constraint forces you to think differently. One approach: sort and use two pointers (O(n log n)). Another: use a hash set (O(n) average). Both stay within budget. The O(n²) idea, no matter how simple, is off the table.
Caveat: constants matter, but the rule of thumb is reliable.
The exact threshold varies:
- Modern CPUs can do ~10⁸ – 10⁹ simple operations per second.
- Competitive programmers often use 10⁸ as a safe estimate.
- Complex operations (memory allocation, recursion overhead) may lower the threshold.
- Very simple operations (bit shifts, comparisons) might push it higher.
But the table above, based on 10⁸ ops/sec, is what the competitive programming community relies on. It works.
Let us see how constraints change the algorithm you would write.
Scenario 1: n ≤ 10
Problem: “Find the maximum sum of any subset of the array.”
Constraint: n ≤ 10.
Approach: Brute force. Try all 2^n subsets. That is 2^10 = 1024 operations. Instant.
function maxSubsetSum(arr) {
let max = 0;
// Iterate through all 2^n subsets using bitmask
for (let mask = 0; mask < (1 << arr.length); mask++) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
if (mask & (1 << i)) sum += arr[i];
}
max = Math.max(max, sum);
}
return max; // O(2^n)
}With n ≤ 10, this O(2^n) solution is fine. You are not trying to be clever. The constraint gives you permission to brute force.
Scenario 2: n ≤ 1,000
Problem: “Find the longest common subsequence (LCS) of two strings.”
Constraint: n ≤ 1,000 (length of each string).
Approach: Dynamic programming. Build a 2D table of size n × n. That is 10⁶ operations (filling the table). Feasible.
function lcs(s1, s2) {
const n = s1.length, m = s2.length;
const dp = Array(n + 1).fill(0).map(() => Array(m + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
if (s1[i - 1] === s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[n][m]; // O(n²)
}With n ≤ 1,000, O(n²) is safe. If n were 10⁶, this would not work.
Scenario 3: n ≤ 10⁶
Problem: “Count how many elements in the array are greater than the average.”
Constraint: n ≤ 10⁶.
Approach: Single pass. Compute the sum (O(n)), compute the average, count (O(n)). Total: O(n). ~10⁶ operations. Instant.
function countAboveAverage(arr) {
const sum = arr.reduce((a, b) => a + b, 0);
const avg = sum / arr.length;
return arr.filter(x => x > avg).length; // O(n)
}With n ≤ 10⁶, a single pass is required. O(n²) is 10¹² operations — hopeless. O(n log n) is ~2×10⁷ operations — overkill for this problem (though still acceptable). O(n) is ideal.
These three scenarios show the same principle: the constraint narrows down which complexity is even worth considering.
Let us trace how the constraint eliminates options. Imagine reading a problem:
Problem statement: “Given an array of size n, find if there exists a pair of elements whose sum equals target.”
Now you read the constraint: “1 ≤ n ≤ 100,000.”
1
Constraint: n ≤ 100,000
2
Budget: ~10^8 operations
3
Work backwards to find the complexity you need...
The constraint does not tell you which O(n log n) algorithm to use, but it tells you that anything slower will not work. This is invaluable.
Here is the complete reference table, with reasoning:
| n | Feasible complexity | Typical algorithms | Why |
|---|---|---|---|
| ≤ 10–12 | O(n!), O(2ⁿ) | Brute-force permutations, subsets, backtracking | 2^12 = 4096 ops; n! ≤ 10^6 ops |
| ≤ 20 | O(2ⁿ) | Bitmask DP, subset enumeration | 2^20 ≈ 10^6 ops |
| ≤ 100–300 | O(n³) | Triple nested loop, Floyd–Warshall | n³ ≈ 10^7 ops for n=300 |
| ≤ 1,000–5,000 | O(n²) | Nested loop, simple DP, bubble sort, selection | n² ≈ 10^7 ops for n=3000 |
| ≤ 100,000 | O(n log n) | Merge sort, quicksort, divide-and-conquer | n log n ≈ 1.7M ops for n=100K |
| ≤ 1,000,000 | O(n) | Single scan, hash set, greedy, BFS/DFS | n ≈ 10^6 ops |
| ≥ 10^7 | O(log n), O(1) | Binary search, hash lookup, lookup tables | log n ≈ 30 ops for n=10^9 |
Notice the logarithmic jumps. The difference between n ≤ 1,000 and n ≤ 10⁶ forces you from O(n²) to O(n log n). The difference between n ≤ 10⁶ and n ≤ 10⁹ forces you from O(n) to O(log n).
The x-axis is not linear — it shows how the threshold shifts. Small constraints allow quadratic; large constraints demand linear or logarithmic.
A problem says: 'n ≤ 10'. Which complexity is safe to use?
A problem says: 'n ≤ 1,000,000'. An O(n²) algorithm would do about how many operations?
Constraint: n ≤ 100. Which complexity is *not* feasible?
You are solving a problem with n ≤ 5,000. The naive brute-force approach is O(n³). Should you attempt it?
Constraint: n ≤ 10⁶. Which algorithm is *required*?
Edge cases
Constant factors do matter. The rule “10⁸ operations per second” is an estimate. A tight loop with bit shifts might run at 10⁹ ops/sec. A function with cache misses and malloc() calls might run at 10⁷ ops/sec. Competitive programmers adjust: if the constraint is tight (n ≤ 1,000,000 and time limit is 1 second), they may avoid O(n log n) and aim for O(n). If the constraint is generous and time limit is 2 seconds, they might push an O(n log n) algorithm with higher constants.
Common mistake
Confusing the time limit with the operation budget. The time limit is 1–2 seconds. But not all operations take the same time. A comparison takes ~1 ns. Memory allocation takes ~100 ns. Recursion overhead adds up. When you see “1 second”, think “10⁸ simple operations”, not “I have unlimited time to write fancy code”. Competitive programmers keep this in mind and prefer simple, tight loops over recursive or complex approaches.
Why do competitive programmers always read the constraints first?
Constraints are the signal that tells you which algorithm to attempt.
Key takeaways:
-
Rule of thumb: Computers do ~10⁸ simple operations per second. Most judges give 1–2 seconds. So you have a budget of ~10⁸ – 2×10⁸ operations.
-
The mapping: Input size n tells you which complexity is feasible:
- n ≤ 10–12 → O(2ⁿ) or O(n!) is OK
- n ≤ 20–25 → O(2ⁿ) is OK
- n ≤ 500 → O(n³) is OK
- n ≤ 5,000 → O(n²) is OK
- n ≤ 10⁶ → O(n log n) is required
- n ≥ 10⁷ → O(n) or O(log n) is required
-
How to use it: Before you write code, compute the cost of your first idea. If it does not fit the budget, eliminate it and design a new approach.
-
The constraint is not a suggestion; it is a barrier. An O(n²) algorithm for n = 10⁶ will timeout. No amount of optimization fixes this — you must use a faster algorithm.
-
Work backwards: Read the constraint, pick the fastest complexity that is feasible, then design an algorithm that achieves it. The constraint narrows your search space and saves you from dead ends.
This bridge from constraints to algorithm is your superpower. Before you write a single line of code, the problem tells you what you need to build.