Reading Constraints to Choose an Approach
The practical version
Big O notation gets taught as theory. In a contest it is a decision tool: you read the maximum input size, and it tells you which approaches can possibly pass. Getting this right before writing code is the difference between four hours well spent and four hours spent optimizing something that was never going to work.
The judge gives you a time limit, typically two seconds for Java. A rough working estimate is that you can perform around 108 simple operations per second. This is imprecise — the real number depends on what the operations are — but it is accurate enough to choose an approach.
The table to memorize
What fits in the time limit
| Max N | Affordable complexity | Typical technique |
|---|---|---|
| N ≤ 10 | O(N!) | Try every permutation |
| N ≤ 20 | O(2N) | Try every subset, bitmask DP |
| N ≤ 500 | O(N3) | Floyd–Warshall, some interval DP |
| N ≤ 5,000 | O(N2) | All pairs, simple DP |
| N ≤ 200,000 | O(N log N) | Sorting, binary search, segment tree, sets |
| N ≤ 106 | O(N) | Single pass, prefix sums, two pointers |
| N ≤ 1018 | O(log N) or O(1) | Math, binary search on the answer, matrix power |
This table is the most useful thing in this module. The input size is not decoration in the problem statement — it is the problem telling you what it wants.
How to use it
Read the constraints first, before you finish reading the statement.
If N ≤ 5,000, a nested loop over all pairs is fine and you should stop looking for something cleverer. If N ≤ 200,000, a nested loop is 4 × 1010 operations and will not finish — you need sorting, a set, or a prefix sum.
Counting operations
To find the complexity, count how many times the innermost work runs as a function of the input size.
// O(N) — the loop body runs n times
long sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
// O(N^2) — for each i, the inner loop runs up to n times
int best = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
best = Math.max(best, a[i] + a[j]);
}
}
The second example runs about N2/2 times, not N2. Big O drops the constant factor, so both are O(N2). That is usually the right call, but not always — see the caveat below.
// O(N log N) — sorting dominates the linear scan after it
Arrays.sort(a);
for (int i = 0; i < n; i++) {
// constant work
}
Where the log comes from
A log N factor appears when something repeatedly halves the search space or when you use a balanced tree structure.
Common operations and their cost
| Operation | Cost |
|---|---|
Arrays.sort on primitives | O(N log N) |
| Binary search in a sorted array | O(log N) |
HashMap get or put | O(1) average |
TreeMap get or put | O(log N) |
PriorityQueue add or poll | O(log N) |
ArrayList add at end | O(1) amortized |
ArrayList remove from middle | O(N) |
LinkedList access by index | O(N) |
For N = 200,000, log N is about 18. So O(N log N) is roughly 3.6 million operations — comfortable. O(N2) is 4 × 1010 — hopeless. That gap is the single most important boundary in intermediate and advanced problems.
Space matters too
Memory limits are usually 256 MB. Java’s overhead is higher than C++’s, so it is worth a quick check.
Rough memory costs
int[1_000_000]is about 4 MB. Fine.int[10_000][10_000]is 400 MB. Too much.long[1_000_000]is about 8 MB. Fine.Integer[1_000_000]is roughly 20 MB — boxed objects carry per-object overhead. Prefer primitive arrays.- A
HashMapwith a million entries can approach 100 MB. Prefer an array if the keys are small integers.
The last two points are Java-specific and catch people who are used to C++ memory estimates. When keys are bounded integers, an array is both faster and much smaller than a map.
When constants actually matter
Big O hides constant factors, and occasionally the constant decides the outcome. Two O(N log N) solutions can differ by a factor of ten if one allocates objects in the inner loop and the other does not.
Practical notes for Java
These do not change the complexity, but they change whether you pass:
- Use primitive arrays (
int[]) rather thanArrayList<Integer>in hot loops. Arrays.sortonint[]uses a quicksort variant; onInteger[]or objects it uses merge sort with boxing overhead. The primitive version is substantially faster.- Avoid creating objects inside a loop that runs 106 times.
- Reading input carelessly can cost more than your algorithm. See Reading Input Fast.
There is one genuine correctness caveat to Arrays.sort on primitives: its quicksort can be driven to O(N2) by adversarial input. This is rare in practice but real in some contests. If you are worried, sort boxed Integer[] (merge sort, guaranteed O(N log N)) or shuffle the array before sorting.
Worked example
A problem gives N ≤ 100,000 integers and asks whether any two of them sum to a target X.
The direct approach is to check every pair: O(N2) = 1010. Too slow.
Consulting the table, N ≤ 100,000 allows O(N log N) or O(N). Two approaches fit:
// O(N) average — one pass with a hash set
Set<Integer> seen = new HashSet<>();
boolean found = false;
for (int i = 0; i < n; i++) {
if (seen.contains(x - a[i])) {
found = true;
break;
}
seen.add(a[i]);
}
// O(N log N) — sort, then two pointers from both ends
Arrays.sort(a);
int lo = 0, hi = n - 1;
boolean found = false;
while (lo < hi) {
int sum = a[lo] + a[hi];
if (sum == x) { found = true; break; }
else if (sum < x) lo++;
else hi--;
}
Both pass. The point is that the constraint told you to look for one of them instead of submitting the nested loop.
Choose the approach
For each set of constraints, name the complexity you should target and one technique that achieves it.
- N ≤ 18, and you must decide which subset of items to take.
- N ≤ 1,000, and you need the shortest path between every pair of nodes.
- N ≤ 300,000, and you need the k-th smallest element.
- N ≤ 1012, and you need to know whether N is prime.
- N ≤ 2,000, and you need the longest common subsequence of two strings of length N.
Answers
- O(2N) — 218 is about 262,000. Enumerate subsets with bitmasks.
- O(N3) is 109, which is too slow in Java. Run Dijkstra from each node instead: O(N · M log N).
- O(N log N) — sort and index, or use a heap of size k.
- O(√N) — trial division up to √N is about 106 steps.
- O(N2) — 4 × 106 DP states. Standard LCS table.