C++ STL: Zero to Hero
Part 0 · Why STL Exists
Imagine you need a list of integers that can grow. In raw C, you would:
- Allocate an array with
malloc. - Track its size and capacity in two extra variables.
- When full, allocate a bigger array, copy everything over, free the old one.
- Remember to
free()at the end so you don't leak memory. - Repeat all of this for every other data structure you need: linked list, hash map, heap…
That's hundreds of lines of bug-prone boilerplate before you've solved a single problem. STL — the Standard Template Library — gives you all of that, tested and tuned, in one line:
vector<int> v; // done. resizes itself. cleans up on its own.
The 5 Pillars of STL
Everything in STL is built from five kinds of building blocks. Memorize these names — every chapter below maps back to them.
| Pillar | What it is | Examples |
|---|---|---|
| Containers | Objects that hold data. | vector, set, map, queue… |
| Iterators | Smart pointers that walk over a container. | v.begin(), v.end() |
| Algorithms | Free functions that do something to a range. | sort, find, reverse, count… |
| Functors | Function-like objects that customize behavior. | less<>, greater<>, lambdas |
| Adapters | Wrappers that change the interface of a container. | stack, queue, priority_queue |
Your Very First STL Program
One header unlocks everything:
#include <bits/stdc++.h> // pulls in the entire STL — perfect for contests
using namespace std; // so we can write `vector` instead of `std::vector`
int main() {
vector<int> a = {3, 1, 4, 1, 5, 9, 2, 6};
sort(a.begin(), a.end()); // algorithm works on iterator range
for (int x : a) cout << x << ' '; // 1 1 2 3 4 5 6 9
}
That tiny program touched all five pillars:
- Container:
vector<int> - Iterators:
a.begin(),a.end() - Algorithm:
sort() - Functor: implicit
less<int>(the default comparator) - Adapter: the range-for loop adapts the iterator pair into a clean syntax
<bits/stdc++.h> is non-standard but works on GCC/Clang (which all contest judges use). It saves you from listing 30 separate headers. In real production code, prefer specific headers — <vector>, <algorithm>, etc.Where to go next
Read in order. Each Part assumes you have understood the previous one. If you only have 60 minutes, read Parts 0, 1.1 (vector), 2.1 (set), 2.3 (map), and 4 (adapters) — that covers ~80% of contest needs.
Part 1.1 · vector — The Workhorse
vector is a magic resizing array. It looks and acts like a regular array (you can write v[i], you get O(1) indexing) but it grows on its own when you push to it. You will use it in ~70% of every program you write.Internal model — capacity vs size
A vector keeps a contiguous chunk of memory. Two numbers describe it:
- size() — how many elements you've actually stored.
- capacity() — how much memory has been reserved (≥ size).
When size == capacity and you push_back one more, the vector allocates a bigger block (typically 2× capacity), copies everything over, and frees the old one. This doubling strategy is what makes push_back O(1) amortized.
Declaration & initialization (5 ways)
vector<int> a; // empty
vector<int> b(10); // 10 zeros
vector<int> c(10, -1); // 10 copies of -1
vector<int> d = {3, 1, 4, 1, 5}; // from initializer list
vector<int> e(d.begin(), d.end()); // copy from range
The full operation cheat sheet
| Operation | What it does | Complexity |
|---|---|---|
v.push_back(x) | Append x to the end. | O(1) amortized |
v.pop_back() | Remove the last element. | O(1) |
v.emplace_back(args) | Build element in place — faster for objects. | O(1) amort. |
v[i] | Direct access. No bounds check. | O(1) |
v.at(i) | Access with bounds check (throws on bad index). | O(1) |
v.front() / v.back() | First / last element. | O(1) |
v.size() / v.empty() | Number of elements / is it empty? | O(1) |
v.clear() | Erase everything (size becomes 0). | O(n) |
v.resize(n) | Make the size exactly n; new entries default-initialized. | O(|n − old size|) |
v.reserve(n) | Pre-allocate capacity for n elements (avoids reallocs). | O(n) |
v.insert(it, x) | Insert x before iterator it. | O(n) |
v.erase(it) | Remove element at it; everything after shifts left. | O(n) |
v.begin() / v.end() | Iterator to first / one-past-last. | O(1) |
v.rbegin() / v.rend() | Reverse iterators (for iterating backwards). | O(1) |
Three ways to iterate
vector<int> v = {10, 20, 30};
// 1) classic index loop — when you need the index
for (int i = 0; i < (int)v.size(); ++i)
cout << i << ":" << v[i] << ' ';
// 2) range-based for — cleanest, when you only need the value
for (int x : v) cout << x << ' ';
for (int& x : v) x *= 2; // note the & — modifies in place
// 3) iterator loop — verbose but very explicit; needed for erase()
for (auto it = v.begin(); it != v.end(); ++it)
cout << *it << ' ';
v.size() returns an unsigned type (size_t). Comparing a signed loop variable with an unsigned size is the source of countless bugs. Either write (int)v.size() or use a size_t loop variable.2D vectors — vectors of vectors
// a 5×7 grid of zeros
vector<vector<int>> grid(5, vector<int>(7, 0));
grid[2][3] = 42;
int rows = grid.size();
int cols = grid[0].size();
⚠️ Iterator invalidation — the #1 bug source
When a vector grows past its capacity, it reallocates. Every iterator, pointer, and reference into the old memory becomes invalid. Using them is undefined behavior.
vector<int> v = {1, 2, 3};
int& ref = v[0]; // ref points into v's buffer
v.push_back(4); // vector might reallocate!
cout << ref; // 💥 might crash or print garbage
Rules:
- push_back / emplace_back / insert / resize / reserve — may invalidate everything if a reallocation happens.
- erase(it) — invalidates
itand everything after it. - pop_back — invalidates only the iterator/reference to the last element.
- operator[] / at() — never invalidate (read-only operations don't reallocate).
The 5 most common beginner mistakes
| Mistake | Fix |
|---|---|
for (int i=0; i<v.size()-1; ++i) on an empty vector → infinite loop (unsigned underflow) | Cast: (int)v.size()-1, or check !v.empty() first. |
Erasing while iterating: for (auto x : v) if (x==0) v.erase(...) | Use the erase–remove idiom (see Part 6.3). |
Storing references through push_back | reserve() first, or just store indices. |
Reading v[v.size()] — out of bounds! | Last valid index is v.size()-1. |
Forgetting the inner vector size in 2D: vector<vector<int>> g(n); | Each row is empty — pre-size with vector<int>(m, 0). |
Part 1.2 · deque — Double-Ended Queue
vector, but you can push and pop from both ends in O(1). Internally it's a chain of fixed-size blocks, not one contiguous array.What it adds over vector
| Operation | vector | deque |
|---|---|---|
push_back / pop_back | O(1) | O(1) |
push_front / pop_front | O(n) (shift everyone) | O(1) |
operator[] | O(1) | O(1) (slightly slower constant) |
| Memory layout | One contiguous block | Chain of blocks |
deque<int> dq;
dq.push_back(1); dq.push_back(2); // dq: [1, 2]
dq.push_front(0); // dq: [0, 1, 2]
cout << dq[1]; // 1
dq.pop_front(); // dq: [1, 2]
The killer use-case: sliding window
To find the maximum of every window of size k in an array, you keep a deque of indices in decreasing order of their values. The front is always the current window's max.
// Sliding-window maximum, O(n)
vector<int> slidingMax(vector<int>& a, int k) {
deque<int> dq; // stores indices, values strictly decreasing
vector<int> ans;
for (int i = 0; i < (int)a.size(); ++i) {
while (!dq.empty() && dq.front() <= i - k) dq.pop_front();
while (!dq.empty() && a[dq.back()] < a[i]) dq.pop_back();
dq.push_back(i);
if (i >= k - 1) ans.push_back(a[dq.front()]);
}
return ans;
}
deque only if you genuinely need push_front/pop_front. Otherwise vector wins on cache locality and constant factors.Part 1.3 · array — Fixed-Size Container
std::array<T, N> is a thin wrapper around a C-style array. The size N is part of the type and known at compile time.
array<int, 5> a = {1, 2, 3, 4, 5};
a.fill(0); // all zeros
cout << a.size(); // 5 (compile-time known)
for (int x : a) cout << x;
| Why pick array over C array | Why pick array over vector |
|---|---|
It knows its own size (.size()). | No heap allocation — lives on the stack. |
Works with all STL algorithms via begin()/end(). | Tiny constant factor, perfect for known-size data. |
| Can be returned from a function and copied safely. | Use it for fixed-size grids, look-up tables, etc. |
Part 1.4 · list & forward_list — Linked Lists
vector with O(n) insert is faster than a list with O(1) insert until n is in the millions. Use list only when the algorithm specifically needs iterator stability or O(1) splice.- list — doubly-linked list.
push_front,push_back,insert,eraseare all O(1) given an iterator.splice()moves nodes between lists in O(1) without copying — this is the only situation wherelisttruly shines. - forward_list — singly linked. Smaller per-node memory but you can only iterate forward. Rarely used.
list<int> A = {1, 2, 3};
list<int> B = {10, 20};
auto it = A.begin(); ++it; // points at 2
A.splice(it, B); // A: [1, 10, 20, 2, 3]; B is now empty — O(1)
Part 2.1 · set — Sorted Bag of Unique Elements
set is a filing cabinet that auto-sorts itself and refuses duplicates. Every time you drop a paper in, it slides into its correct alphabetical spot. Try to add a paper that's already inside? Nothing happens.What it gives you
- Elements are kept in sorted order (ascending by default).
- No duplicates — inserting an existing element is a no-op.
- All operations are O(log n): insert, erase, find, count.
- Internally a balanced BST (red-black tree).
The full operation table
| Operation | What it does | Complexity |
|---|---|---|
s.insert(x) | Insert x. Returns pair<iterator, bool> — the bool is true if newly inserted, false if already present. | O(log n) |
s.erase(x) | Remove element with value x. Returns 0 or 1 (count removed). | O(log n) |
s.erase(it) | Remove element at iterator. Faster — no search needed. | O(1) amort. |
s.find(x) | Returns iterator to x, or s.end() if absent. | O(log n) |
s.count(x) | Returns 0 or 1 (set has unique elements). | O(log n) |
s.contains(x) | C++20 only. true/false. Cleaner than find. | O(log n) |
s.lower_bound(x) | Iterator to first element ≥ x. | O(log n) |
s.upper_bound(x) | Iterator to first element > x. | O(log n) |
s.size() / s.empty() | Standard. | O(1) |
*s.begin() | The smallest element. | O(1) |
*s.rbegin() | The largest element. | O(1) |
Working example
set<int> s;
s.insert(5); s.insert(2); s.insert(8); s.insert(2);
// s now contains: {2, 5, 8} — duplicate 2 was ignored
for (int x : s) cout << x << ' '; // 2 5 8 (sorted)
if (s.find(5) != s.end()) cout << "yes";
if (s.count(5)) cout << "yes"; // equivalent
s.erase(2); // {5, 8}
cout << *s.begin(); // 5 (smallest)
cout << *s.rbegin(); // 8 (largest)
lower_bound vs upper_bound — finally explained
This is the most-asked STL question. Both return an iterator into the sorted set. The difference is what they do with elements equal to the query.
| Function | Returns iterator to first element that is… |
|---|---|
lower_bound(x) | ≥ x (the first one that is not less than x) |
upper_bound(x) | > x (strictly greater) |
set<int> s = {10, 20, 20, 30, 40}; // (set has no dups, so really {10,20,30,40})
s.lower_bound(20); // → iterator to 20
s.upper_bound(20); // → iterator to 30
s.lower_bound(25); // → iterator to 30 (no exact match, returns next ≥)
s.upper_bound(40); // → s.end() (nothing strictly greater)
std::lower_bound on a set/map!
The free function std::lower_bound(s.begin(), s.end(), x) works in O(n) on a set because set iterators aren't random-access. Always use the member function s.lower_bound(x) — it's O(log n).The "find closest" pattern
// Find the element in s closest to x
auto it = s.lower_bound(x); // first element ≥ x
int best = INT_MAX;
if (it != s.end()) best = min(best, *it - x);
if (it != s.begin()) {
--it;
best = min(best, x - *it);
}
Part 2.2 · multiset — Sorted Bag With Duplicates
A multiset is identical to a set except it allows the same value to appear multiple times.
multiset<int> ms;
ms.insert(5); ms.insert(5); ms.insert(3);
// ms: {3, 5, 5}
cout << ms.count(5); // 2 — really counts now (not just 0/1)
⚠️ The classic erase trap
ms.erase(5); // removes ALL copies of 5
ms.erase(ms.find(5)); // removes EXACTLY ONE copy
ms.erase(value) wipes every matching element. To remove a single copy, always do ms.erase(ms.find(value)). Make sure to check find() != end() first.Common use: priority-like structure with random delete
multiset is essentially a heap that also lets you remove arbitrary elements in O(log n) — something a priority_queue can't do.
multiset<int> ms = {3, 1, 4, 1, 5, 9, 2};
int mn = *ms.begin(); // min in O(1)
int mx = *ms.rbegin(); // max in O(1)
ms.erase(ms.find(4)); // delete arbitrary value in O(log n)
Part 2.3 · map — Sorted Phone Book
map is a sorted phone book: each entry is a name → number pair. The names are kept in alphabetical order, each name appears at most once, and you can look up someone in O(log n).The basics
map<string, int> age;
age["alice"] = 30;
age["bob"] = 25;
age["carol"] = 28;
cout << age["bob"]; // 25
age.erase("alice");
cout << age.size(); // 2
for (auto& [name, a] : age) // C++17 structured binding — beautiful
cout << name << ":" << a << '\n'; // in alphabetical order!
operator[] vs at() vs find() — pick the right one
| Form | Behavior when key exists | Behavior when key is missing |
|---|---|---|
m[k] | Returns reference to value. | Inserts a default-constructed value (e.g., 0 for int) and returns reference to it. |
m.at(k) | Returns reference to value. | Throws std::out_of_range. |
m.find(k) | Returns iterator to (key, value). | Returns m.end(). |
m.count(k) | Returns 1. | Returns 0. |
m.contains(k) (C++20) | Returns true. | Returns false. |
m[k] for an unknown key creates an entry. This corrupts your map and inflates its size. Use m.find(k) or m.count(k) when you only want to check.
if (m[k] == 5) // 💀 inserts m[k]=0 if k absent!
if (m.count(k) && m[k] == 5) // ✅ correctCounting frequencies (the killer use)
map<string, int> freq;
for (string& w : words) freq[w]++; // auto-creates with 0, then ++
for (auto& [word, c] : freq)
cout << word << ": " << c << '\n';
map of map (2D mapping)
map<string, map<string, int>> grid;
grid["alice"]["math"] = 95;
grid["alice"]["phys"] = 88;
map vs unordered_map — quick reference
| map (red-black tree) | unordered_map (hash table) | |
|---|---|---|
| Lookup | O(log n) | O(1) average |
| Order | Sorted by key | Random |
Need operator<? | Yes | No, but needs hash |
| Worst case | O(log n) guaranteed | O(n) on adversarial input |
| Use when… | You need sorted iteration, or floor/ceiling queries. | You only need fast lookup. |
Part 2.4 · multimap — One Key, Many Values
A multimap allows many entries with the same key. operator[] doesn't exist (which value would it return?). You access groups of equal keys via equal_range:
multimap<string, int> mm;
mm.insert({"alice", 95});
mm.insert({"alice", 88});
mm.insert({"bob", 72});
auto [first, last] = mm.equal_range("alice");
for (auto it = first; it != last; ++it)
cout << it->second << ' '; // 88 95 (sorted by key, kept in insertion order within key in newer C++)
map<Key, vector<Value>> instead. It's clearer and easier to manipulate.Part 3 · unordered_set & unordered_map (Hash-based)
set is a sorted filing cabinet, unordered_set is a magic locker room: each item gets thrown into a locker whose number is computed from a hash of the item. Average O(1) for everything — but the items are not in any meaningful order.The interface mirrors set/map
Same operations, same names — only the complexity and ordering change.
unordered_set<int> us;
us.insert(5); us.insert(2); us.insert(8);
if (us.count(5)) cout << "present";
us.erase(2);
unordered_map<string, int> um;
um["alice"] = 30; // same operator[] as map
um.erase("alice");
Decision: ordered vs unordered
| Use ORDERED (set / map) when… | Use UNORDERED (unordered_set / unordered_map) when… |
|---|---|
| You need elements in sorted order. | You only need fast lookup. |
You need lower_bound / upper_bound / floor / ceiling. | Order doesn't matter. |
| You iterate the smallest/largest often. | You'll use find/count/insert heavily on huge n. |
| You need O(log n) worst-case. | You're OK with O(n) worst-case (rare on random data). |
Custom comparator is easy: just operator<. | Custom hash is easy for built-ins; harder for structs. |
The hash collision worst case
Hash tables are O(1) average. If many keys hash to the same bucket, all those keys live in a chain that must be linearly scanned — operations degrade to O(n).
For random data this never happens. But contest setters know this. They craft inputs that hash to the same bucket on purpose. The result: your O(n log n) solution times out as O(n²).
unordered_map<long long, int> by exploiting GCC's predictable hash. Defend yourself with a custom hash (see Part 9.4 — Custom Hash).Quick-fix: hash + a random seed
struct SafeHash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t SEED =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + SEED);
}
};
unordered_map<long long, int, SafeHash> safe_map;
Part 4.1 · stack — Last In, First Out
The whole interface (only 5 things)
stack<int> st;
st.push(10); st.push(20); st.push(30);
cout << st.top(); // 30
st.pop(); // removes 30 (returns void!)
cout << st.size(); // 2
cout << st.empty(); // false
pop() returns void.
To both read and remove the top, do int x = st.top(); st.pop(); — never x = st.pop().Classic problem: balanced brackets
bool isBalanced(const string& s) {
stack<char> st;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') st.push(c);
else {
if (st.empty()) return false;
char t = st.top(); st.pop();
if (c == ')' && t != '(') return false;
if (c == ']' && t != '[') return false;
if (c == '}' && t != '{') return false;
}
}
return st.empty();
}
Part 4.2 · queue — First In, First Out
queue<int> q;
q.push(10); q.push(20); q.push(30);
cout << q.front(); // 10 (oldest)
cout << q.back(); // 30 (newest)
q.pop(); // removes 10 (the front)
The killer use: BFS (breadth-first search)
vector<int> bfs(vector<vector<int>>& g, int src) {
int n = g.size();
vector<int> dist(n, -1);
queue<int> q;
dist[src] = 0;
q.push(src);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : g[u]) if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
return dist;
}
Part 4.3 · priority_queue — The Heap
Default: max-heap
priority_queue<int> pq; // MAX-heap by default
pq.push(3); pq.push(1); pq.push(5); pq.push(2);
cout << pq.top(); // 5
pq.pop();
cout << pq.top(); // 3
push(x),pop()— both O(log n)top(),size(),empty()— O(1)- You cannot iterate or search a priority_queue. If you need that, use
multiset.
Three ways to make a min-heap
// Way 1: use greater<> comparator (the standard way)
priority_queue<int, vector<int>, greater<int>> minPQ;
// Way 2: negate values, use max-heap (cheap hack — int only!)
priority_queue<int> tricky;
tricky.push(-x); // store -x, top is the smallest x
// Way 3: lambda comparator (verbose but flexible)
auto cmp = [](int a, int b){ return a > b; };
priority_queue<int, vector<int>, decltype(cmp)> minPQ2(cmp);
Custom comparator with pairs
// Min-heap of (distance, node) pairs — for Dijkstra
priority_queue<
pair<int, int>,
vector<pair<int, int>>,
greater<pair<int, int>>
> pq;
The "stale entry" trick (for Dijkstra)
Since priority_queue can't update an element in place, Dijkstra uses a clever trick: push the new (smaller) distance and ignore stale entries when they pop later.
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue; // stale — ignore
for (auto& [v, w] : g[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v}); // might create stale entry — that's ok
}
}
}
Part 5 · Iterators — The Glue of STL
*it.Why iterators exist
STL algorithms don't know what container they're working on. sort doesn't care if you give it a vector, a deque, or a plain C array — it just walks from one iterator to another. That separation is the genius of STL: N containers × M algorithms = N × M combinations, with N + M code.
The half-open range [first, last)
Every STL algorithm takes a pair of iterators: first (start, inclusive) and last (one-past-end, exclusive). This convention has three big benefits:
- An empty range is naturally expressed as
first == last. last - firstdirectly gives the size.- You never have to special-case "did I include the last element?".
vector<int> v = {10, 20, 30, 40, 50};
sort(v.begin(), v.end()); // whole range
sort(v.begin(), v.begin() + 3); // first 3 elements only: [10,20,30)
The 5 iterator categories
| Category | Capabilities | Examples |
|---|---|---|
| Input | Read once, ++ once. Single-pass. | istream_iterator |
| Output | Write once, ++ once. | back_inserter, ostream_iterator |
| Forward | Read/write, ++, multi-pass. | forward_list |
| Bidirectional | ++ and -- | list, set, map |
| Random Access | + k, − k, [k], O(1) jump | vector, deque, array, raw pointer |
Why this matters: some algorithms require random-access iterators. sort() needs random access — that's why you can't sort a list with std::sort. (Use list::sort() member instead.)
begin/end families
| Pair | Meaning |
|---|---|
begin() / end() | Forward iteration over the container. |
cbegin() / cend() | Same, but const — guarantees you won't write through it. |
rbegin() / rend() | Reverse iteration: *rbegin() is the last element. |
crbegin() / crend() | Const reverse. |
// reverse-print without reversing
for (auto it = v.rbegin(); it != v.rend(); ++it)
cout << *it << ' ';
Iterator arithmetic
auto it = v.begin();
advance(it, 3); // move it forward by 3 — works on any iterator
auto nx = next(it); // returns it+1 without modifying it
auto pv = prev(it); // returns it-1 (bidirectional+)
int d = distance(v.begin(), it); // how many steps from begin to it
Iterator adapters — the secret weapons
Sometimes you want to write into a container via an algorithm. These adapters wrap a container so you can use it as an output iterator.
| Adapter | What it does |
|---|---|
back_inserter(c) | Each *= x through this iterator becomes c.push_back(x). |
front_inserter(c) | Same but push_front (works on deque/list). |
inserter(c, it) | Calls c.insert(it, x) for each item. |
vector<int> src = {1, 2, 3};
vector<int> dst;
copy(src.begin(), src.end(), back_inserter(dst));
// dst is now {1, 2, 3}
// transform into a brand-new vector
vector<int> squared;
transform(src.begin(), src.end(), back_inserter(squared),
[](int x){ return x*x; });
// squared is now {1, 4, 9}
How range-based for actually works
for (int x : v) { ... }
// is exactly equivalent to:
for (auto __it = v.begin(); __it != v.end(); ++__it) {
int x = *__it;
...
}
So range-for works on anything with begin() and end() — including raw arrays, your own classes, and STL containers.
Part 6.1 · Sorting & Ordering Algorithms
sort — the workhorse
sort uses introsort (quicksort + heapsort fallback) — guaranteed O(n log n) worst case.
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
sort(v.begin(), v.end()); // ascending → 1 1 2 3 4 5 6 9
sort(v.begin(), v.end(), greater<int>()); // descending → 9 6 5 4 3 2 1 1
// Lambda comparator — sort by absolute value
sort(v.begin(), v.end(),
[](int a, int b){ return abs(a) < abs(b); });
// Sort pairs — by .second descending, then .first ascending
vector<pair<int,int>> pairs;
sort(pairs.begin(), pairs.end(),
[](auto& a, auto& b){
if (a.second != b.second) return a.second > b.second;
return a.first < b.first;
});
< not <=. Returning true for equal elements crashes sort with undefined behavior.Other sorting flavors
| Function | What it does | Complexity |
|---|---|---|
stable_sort | Like sort but preserves the relative order of equal elements. | O(n log n) |
partial_sort(first, mid, last) | Sort just enough to put the smallest mid-first elements at the front. | O(n log k) |
nth_element(first, kth, last) | Puts the k-th element in its final sorted position. Everything before is ≤, everything after is ≥. The rest is unsorted. | O(n) average |
is_sorted(first, last) | Check if a range is non-decreasing. | O(n) |
reverse(first, last) | Reverse the range in place. | O(n) |
rotate(first, mid, last) | Rotate so that mid becomes the new first. | O(n) |
shuffle(first, last, rng) | Random shuffle. Pass mt19937 for good randomness. | O(n) |
// k-th smallest in O(n) average — much faster than full sort
nth_element(v.begin(), v.begin() + k, v.end());
int kth = v[k];
// Random shuffle — DON'T use random_shuffle, it's deprecated/removed
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
shuffle(v.begin(), v.end(), rng);
Part 6.2 · Searching Algorithms
Linear search family
auto it = find(v.begin(), v.end(), 7); // returns iterator or end()
if (it != v.end()) cout << *it;
auto e = find_if(v.begin(), v.end(),
[](int x){ return x > 100; }); // first element > 100
auto ne = find_if_not(v.begin(), v.end(),
[](int x){ return x % 2 == 0; }); // first odd
int n = count(v.begin(), v.end(), 5); // how many 5s
int e2 = count_if(v.begin(), v.end(),
[](int x){ return x % 2 == 0; }); // count evens
Binary search family — for SORTED ranges only
| Function | Returns |
|---|---|
binary_search(first, last, x) | bool — does x exist? Returns no position! |
lower_bound(first, last, x) | Iterator to first element ≥ x. |
upper_bound(first, last, x) | Iterator to first element > x. |
equal_range(first, last, x) | Pair: (lower_bound, upper_bound). All occurrences in this range. |
All four are O(log n) on random-access iterators (vector, array). On set/map iterators (bidirectional only) the free functions become O(n) — use the member versions.
vector<int> v = {1, 3, 5, 5, 5, 7, 9};
// Does 5 exist?
bool ok = binary_search(v.begin(), v.end(), 5); // true
// How many 5s?
auto [lo, hi] = equal_range(v.begin(), v.end(), 5);
int cnt = hi - lo; // 3
// Index of first ≥ 4?
auto it = lower_bound(v.begin(), v.end(), 4);
int idx = it - v.begin(); // 2 (points at 5)
Min/max algorithms
auto mn = *min_element(v.begin(), v.end());
auto mx = *max_element(v.begin(), v.end());
auto [mn2, mx2] = minmax_element(v.begin(), v.end()); // pair of iterators
// With custom comparator: longest string
auto longest = *max_element(words.begin(), words.end(),
[](auto& a, auto& b){ return a.size() < b.size(); });
Part 6.3 · Modifying Algorithms
copy / fill / replace
vector<int> src = {1,2,3,4,5}, dst(5);
copy(src.begin(), src.end(), dst.begin());
copy_if(src.begin(), src.end(), back_inserter(dst),
[](int x){ return x % 2 == 0; });
fill(v.begin(), v.end(), 0); // every element = 0
fill_n(v.begin(), 5, 42); // first 5 elements = 42
replace(v.begin(), v.end(), 3, 99); // every 3 → 99
replace_if(v.begin(), v.end(),
[](int x){return x<0;}, 0); // negatives → 0
transform — apply a function to every element
// Single range: square every element in place
transform(v.begin(), v.end(), v.begin(),
[](int x){ return x*x; });
// Two ranges: element-wise add
vector<int> a = {1,2,3}, b = {10,20,30}, c(3);
transform(a.begin(), a.end(), b.begin(), c.begin(),
[](int x, int y){ return x + y; });
// c = {11, 22, 33}
The erase–remove idiom (the trickiest STL pattern)
remove doesn't actually remove anything — it shifts non-matching elements forward and returns an iterator to the new logical end. You then call erase on the tail to shrink the container.
// Remove all 0s from v
v.erase(remove(v.begin(), v.end(), 0), v.end());
// Remove with a predicate
v.erase(remove_if(v.begin(), v.end(),
[](int x){ return x < 0; }),
v.end());
// C++20 simplified: std::erase / std::erase_if (one call)
erase(v, 0);
erase_if(v, [](int x){ return x < 0; });
unique — collapse consecutive duplicates
Same idea as remove: must combine with erase, must be on a sorted range to actually remove all duplicates.
vector<int> v = {1, 3, 1, 3, 5};
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
// v is now {1, 3, 5}
Other modifiers
| Function | What it does |
|---|---|
reverse_copy | Like copy but in reverse. |
swap(a, b) | Swap two values (or two whole containers — O(1) for most STL containers). |
swap_ranges(f1, l1, f2) | Element-wise swap two ranges. |
iter_swap(it1, it2) | Swap the two pointed-to elements. |
generate(first, last, gen) | Fill range by calling gen() for each slot. |
Part 6.4 · Set Operations (on sorted ranges)
These operate on two sorted ranges and produce a sorted result. They work on any container — vector, set, deque — as long as input is sorted.
| Function | Result |
|---|---|
set_union | Elements in A or B (or both), with duplicates merged. |
set_intersection | Elements in both A and B. |
set_difference | Elements in A but not in B. |
set_symmetric_difference | Elements in A or B but not both. |
includes(A, B) | Is B a subset of A? |
merge(A, B, out) | Merge two sorted ranges into one sorted range. |
inplace_merge | Merge two consecutive sorted halves of one range. |
vector<int> A = {1, 2, 3, 5};
vector<int> B = {2, 4, 5, 6};
vector<int> C;
set_intersection(A.begin(), A.end(),
B.begin(), B.end(), back_inserter(C));
// C = {2, 5}
C.clear();
set_union(A.begin(), A.end(),
B.begin(), B.end(), back_inserter(C));
// C = {1, 2, 3, 4, 5, 6}
Part 6.5 · Numeric Algorithms (<numeric>)
accumulate — sum, product, anything
vector<int> v = {1, 2, 3, 4, 5};
int sum = accumulate(v.begin(), v.end(), 0); // 15
int prod = accumulate(v.begin(), v.end(), 1,
multiplies<int>()); // 120
// Sum of squares with a lambda
int sq = accumulate(v.begin(), v.end(), 0,
[](int acc, int x){ return acc + x*x; }); // 55
accumulate(v.begin(), v.end(), 0) returns an int even if v contains long long — and overflows silently. Use 0LL as the initial value when summing big numbers.Other numeric goodies
| Function | What it does | Example |
|---|---|---|
iota(first, last, v) | Fill range with v, v+1, v+2, … | iota(v.begin(), v.end(), 0) → 0,1,2,… |
partial_sum | Prefix sums. | {1,2,3} → {1,3,6} |
adjacent_difference | Difference of consecutive elements. | {1,3,6} → {1,2,3} |
inner_product | Dot product of two ranges. | (1,2)·(3,4) = 11 |
gcd(a, b) / lcm(a, b) | C++17. Greatest common divisor / least common multiple. | gcd(12, 18) = 6 |
// Build prefix sums in one line
vector<long long> ps(n+1, 0);
partial_sum(a.begin(), a.end(), ps.begin() + 1);
// ps[i] = a[0] + ... + a[i-1]; query sum(l..r) = ps[r+1] - ps[l]
Part 6.6 · Condition Checkers (all_of, any_of, none_of)
bool allEven = all_of (v.begin(), v.end(),
[](int x){ return x % 2 == 0; });
bool anyZero = any_of (v.begin(), v.end(),
[](int x){ return x == 0; });
bool noneNeg = none_of(v.begin(), v.end(),
[](int x){ return x < 0; });
All three short-circuit (return as soon as the answer is decided). They replace ugly hand-written loops with self-documenting one-liners.
Part 6.7 · Partition Algorithms
partition rearranges a range so that elements satisfying a predicate come first. Returns the iterator to the first element NOT satisfying the predicate.
vector<int> v = {1, 2, 3, 4, 5, 6};
auto mid = partition(v.begin(), v.end(),
[](int x){ return x % 2 == 0; });
// v: even elements, then odd elements (order WITHIN groups not preserved)
// mid points to the first odd element
stable_partition— likepartitionbut preserves relative order within each group.partition_point— for an already-partitioned range, returns the boundary in O(log n) (binary search by predicate).is_partitioned(first, last, pred)— check whether range is already partitioned bypred.
Part 7 · string — A Sequence Container in Disguise
string is a vector<char> with extra string-specific methods. Anything you can do to a vector, you can do to a string.String-specific operations
| Operation | What it does | Complexity |
|---|---|---|
s.length() / s.size() | Number of characters. | O(1) |
s.substr(pos, len) | Substring of len chars starting at pos (len defaults to end). | O(len) |
s.find(t) | First index where substring t appears, or string::npos. | O(n·m) |
s.rfind(t) | Last index where substring t appears. | O(n·m) |
s.find_first_of(set) | First position of any character in set. | O(n) |
s.replace(pos, len, t) | Replace len chars at pos with string t. | O(n) |
s.erase(pos, len) | Remove len chars at pos. | O(n) |
s.insert(pos, t) | Insert t before position pos. | O(n) |
s + t or s += t | Concatenation. | O(|s|+|t|) |
s.compare(t) | Negative if s < t, 0 if equal, positive if s > t. | O(min(|s|,|t|)) |
string s = "hello world";
size_t p = s.find("world"); // 6
if (p != string::npos) cout << "found at " << p;
string sub = s.substr(6, 5); // "world"
string rest = s.substr(6); // "world" (to end)
s.replace(0, 5, "HELLO"); // "HELLO world"
s.insert(5, ", "); // "HELLO, world"
s.erase(5, 2); // back to "HELLO world"
Strings + STL algorithms
string s = "banana";
sort(s.begin(), s.end()); // "aaabnn"
reverse(s.begin(), s.end()); // reverses in place
int as = count(s.begin(), s.end(), 'a'); // 3
// Are they anagrams?
bool anagram(string a, string b) {
sort(a.begin(), a.end());
sort(b.begin(), b.end());
return a == b;
}
String parsing with stringstream
#include <sstream>
string line = "42 7 12 99";
stringstream ss(line);
int x;
vector<int> nums;
while (ss >> x) nums.push_back(x); // {42, 7, 12, 99}
// Split a CSV line
string csv = "alice,bob,carol";
stringstream ss2(csv);
string token;
vector<string> tokens;
while (getline(ss2, token, ',')) tokens.push_back(token);
Number ↔ string conversion
string s = to_string(3.14); // "3.140000"
int n = stoi("42"); // 42
long long ll = stoll("99999999999");
double d = stod("3.14"); // 3.14
// With base (binary, hex, …)
int hex = stoi("ff", nullptr, 16); // 255
Part 8 · Functors, Lambdas & Custom Comparators
Three ways to write a comparator
// 1) Free function
bool cmp(int a, int b) { return a > b; }
sort(v.begin(), v.end(), cmp);
// 2) Functor — a struct with operator()
struct Cmp {
bool operator()(int a, int b) const { return a > b; }
};
sort(v.begin(), v.end(), Cmp());
// 3) Lambda — modern, clean, inline
sort(v.begin(), v.end(), [](int a, int b){ return a > b; });
Lambda anatomy
[capture](parameters) -> ReturnType { body }
// Examples
[](int x){ return x*2; } // no capture, returns int (deduced)
[&](int x){ count += x; } // capture all by reference
[=](int x){ return x + offset; } // capture all by value
[&count](int x){ count += x; } // capture only `count` by reference
[=, &count](int x){ ... } // most by value, count by reference
Built-in functor objects (<functional>)
| Functor | Returns true when | Use |
|---|---|---|
less<T> | a < b | default for sort, set, map |
greater<T> | a > b | descending sort, min-heap |
less_equal<T> | a ≤ b | (don't use as sort comparator!) |
plus<T> / minus<T> / multiplies<T> | a+b / a−b / a·b | accumulate, transform |
Sorting pairs and structs
// Default pair sort: by .first asc, then .second asc
vector<pair<int,int>> v;
sort(v.begin(), v.end());
// Custom: by .second ascending, ties broken by .first descending
sort(v.begin(), v.end(), [](auto& a, auto& b){
if (a.second != b.second) return a.second < b.second;
return a.first > b.first;
});
// Custom struct
struct Person { string name; int age; };
vector<Person> people;
sort(people.begin(), people.end(), [](const Person& a, const Person& b){
return a.age < b.age;
});
Custom comparator for set / map
// Set ordered by string length, then lexicographically
struct ByLen {
bool operator()(const string& a, const string& b) const {
if (a.size() != b.size()) return a.size() < b.size();
return a < b;
}
};
set<string, ByLen> s;
Part 9.1 · bitset — Compact Bit Array
A bitset<N> is a fixed-size array of N bits, packed tight (8 bits per byte). Bitwise operations on bitsets run 64 bits at a time on a 64-bit CPU — that's a free 64× speedup for problems like subset DP.
bitset<100> b; // 100 bits, all 0
b.set(5); // b[5] = 1
b.reset(5); // b[5] = 0
b.flip(); // flip all bits
b.flip(3); // flip just bit 3
bool v = b.test(3); // or just b[3]
int c = b.count(); // number of set bits (popcount)
bool any = b.any(); // at least one bit set?
bool none = b.none(); // no bits set?
string s = b.to_string();
unsigned long u = b.to_ulong();
Bitwise operations
bitset<8> a("11001100"), b("10101010");
auto _and = a & b; // 10001000
auto _or = a | b; // 11101110
auto _xor = a ^ b; // 01100110
auto _shl = a << 2; // 00110000
auto _not = ~a; // 00110011
The 64× speedup
Suppose you have an N×N adjacency matrix and want to count triangles. Naive: O(N³). With bitsets:
const int N = 2000;
bitset<N> row[N];
long long tri = 0;
for (int i = 0; i < N; ++i)
for (int j = i+1; j < N; ++j)
if (row[i][j])
tri += (row[i] & row[j]).count(); // 64-wide AND in one CPU op
tri /= 3;
// Effective: O(N³ / 64)
Part 9.2 · pair & tuple
pair — two values bundled together
pair<int, string> p = {42, "hello"};
auto p2 = make_pair(10, "world");
cout << p.first << ' ' << p.second;
// C++17 structured bindings — cleaner
auto [num, txt] = p;
cout << num << ' ' << txt;
// Pairs are comparable lexicographically (.first first, then .second)
vector<pair<int,int>> v;
sort(v.begin(), v.end());
tuple — N values bundled together
tuple<int, string, double> t = {1, "hi", 3.14};
cout << get<0>(t); // 1
cout << get<1>(t); // "hi"
// Unpack with structured binding
auto [a, b, c] = t;
// Or with tie() — older syntax
int x; string y; double z;
tie(x, y, z) = t;
Tuples are great when a function needs to return multiple values:
tuple<int, int, int> findMinMaxSum(vector<int>& v) {
int mn = *min_element(v.begin(), v.end());
int mx = *max_element(v.begin(), v.end());
int sm = accumulate(v.begin(), v.end(), 0);
return {mn, mx, sm};
}
auto [mn, mx, sm] = findMinMaxSum(v);
Part 9.3 · numeric_limits — Type Boundaries
Instead of remembering magic constants like 2147483647, ask the type itself:
#include <limits>
int iMax = numeric_limits<int>::max(); // 2147483647
int iMin = numeric_limits<int>::min(); // -2147483648
long long llMax = numeric_limits<long long>::max();
double dInf = numeric_limits<double>::infinity();
double eps = numeric_limits<double>::epsilon(); // smallest representable difference
// Equivalent old-school constants from <climits>
INT_MAX, INT_MIN, LLONG_MAX, LLONG_MIN, UINT_MAX
Part 9.4 · Custom Hash for unordered_map
Two cases where you need a custom hash:
- You're storing a custom type (struct, pair) — the default hash doesn't know what to do.
- You're storing built-in types but on Codeforces, where anti-hash test cases can blow up the default hash.
Hashing pairs
struct PairHash {
size_t operator()(const pair<int, int>& p) const {
return hash<long long>()(((long long)p.first << 32) | (unsigned)p.second);
}
};
unordered_map<pair<int, int>, int, PairHash> m;
The bulletproof contest hash (splitmix64 + random seed)
struct SafeHash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15ULL;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t SEED =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + SEED);
}
};
unordered_map<long long, int, SafeHash> safe;
Why it works: splitmix64 spreads bits chaotically, and the per-run random SEED means the contest setter can't precompute a colliding input set.
Part 10 · The Contest-Ready Master Template
Full starter (paste at top of every solution)
#include <bits/stdc++.h>
using namespace std;
// ── shorthand types ───────────────────────────────────────
using ll = long long;
using ull = unsigned long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using vi = vector<int>;
using vll = vector<ll>;
using vpii = vector<pii>;
using vvi = vector<vi>;
// ── shorthand macros ──────────────────────────────────────
#define all(x) (x).begin(), (x).end()
#define rall(x) (x).rbegin(), (x).rend()
#define sz(x) (int)(x).size()
#define pb push_back
#define eb emplace_back
const int INF = 1e9 + 7;
const ll LINF = 1e18;
int main() {
ios_base::sync_with_stdio(false); // fast I/O
cin.tie(nullptr);
// solve here
return 0;
}
The 20 most-used one-liners
| Goal | One-liner |
|---|---|
| Sort ascending | sort(all(v)); |
| Sort descending | sort(rall(v)); |
| Sort by custom key | sort(all(v), [](auto& a, auto& b){ return a.x < b.x; }); |
| Unique sorted vector | sort(all(v)); v.erase(unique(all(v)), v.end()); |
| Remove element | v.erase(remove(all(v), x), v.end()); |
| Sum | ll s = accumulate(all(v), 0LL); |
| Max element | int mx = *max_element(all(v)); |
| Index of value | int i = find(all(v), x) - v.begin(); |
| Binary search exists? | bool ok = binary_search(all(v), x); |
| Lower bound index | int i = lower_bound(all(v), x) - v.begin(); |
| Reverse | reverse(all(v)); |
| Fill with value | fill(all(v), 0); |
| Iota (0,1,2,…) | iota(all(v), 0); |
| Random shuffle | shuffle(all(v), mt19937(random_device{}())); |
| Min-heap of int | priority_queue<int, vector<int>, greater<int>> pq; |
| Frequency map | map<T,int> f; for (auto& x : v) ++f[x]; |
| Max gap | int g = *max_element(all(v)) - *min_element(all(v)); |
| Are all positive? | all_of(all(v), [](int x){ return x > 0; }); |
| Substring | string sub = s.substr(i, len); |
| Build prefix sums | partial_sum(all(v), v.begin()); |
"I need X → use Y" decision table
| If you need… | Reach for… |
|---|---|
| A growing array | vector |
| Push/pop both ends | deque |
| Sorted unique elements | set |
| Sorted with duplicates / random delete a min/max | multiset |
| Key → value lookup, sorted | map |
| Key → value lookup, blazing fast | unordered_map + custom hash |
| LIFO operations | stack |
| FIFO operations / BFS | queue |
| Always-pop the largest (Dijkstra/Prim) | priority_queue |
| Sliding window min/max | deque of indices |
| Compact bitmap / subset DP | bitset |
| Two values together (state, distance) | pair |
| K-th smallest in O(n) | nth_element |
| Any-of / all-of / count | any_of / all_of / count_if |
Part 11 · 15 Worked Problems — The Hero Path
Each problem shows the thinking, the STL choice, the full solution, and the complexity. Read in order — they get progressively harder.
P1 · Count distinct elements
STL: sort + unique or set.
int distinct1(vector<int> v) {
sort(v.begin(), v.end());
return unique(v.begin(), v.end()) - v.begin(); // O(n log n)
}
int distinct2(vector<int>& v) {
return set<int>(v.begin(), v.end()).size(); // O(n log n)
}
P2 · Frequency map
Given a list of strings, print each unique word and its count.
unordered_map<string, int> freq;
for (string& w : words) ++freq[w];
for (auto& [w, c] : freq) cout << w << ' ' << c << '\n';
// Time: O(n) average. Use map if you need sorted output.
P3 · Two Sum
Given a[] and target T, find indices i, j with a[i]+a[j]=T.
unordered_map<int, int> seen; // value → index
for (int i = 0; i < (int)a.size(); ++i) {
int need = T - a[i];
if (seen.count(need)) return {seen[need], i};
seen[a[i]] = i;
}
// Time: O(n) average
P4 · Sliding window maximum
Maximum of every k-sized window. Classic deque application — see Part 1.2.
P5 · Balanced brackets
Already solved in Part 4.1 with stack.
P6 · Next greater element
For each element, find the index of the next strictly greater element on its right.
vector<int> nextGreater(vector<int>& a) {
int n = a.size();
vector<int> ans(n, -1);
stack<int> st; // indices, decreasing values
for (int i = 0; i < n; ++i) {
while (!st.empty() && a[st.top()] < a[i]) {
ans[st.top()] = i;
st.pop();
}
st.push(i);
}
return ans;
}
// Time: O(n)
P7 · K-th largest element
// Approach 1: nth_element — O(n) average
int kthLargest1(vector<int>& v, int k) {
nth_element(v.begin(), v.begin() + (k-1), v.end(), greater<int>());
return v[k-1];
}
// Approach 2: min-heap of size k — O(n log k), uses O(k) memory
int kthLargest2(vector<int>& v, int k) {
priority_queue<int, vector<int>, greater<int>> pq;
for (int x : v) {
pq.push(x);
if ((int)pq.size() > k) pq.pop();
}
return pq.top();
}
P8 · Meeting rooms — minimum rooms needed
Given start/end times of meetings, find the maximum number of overlapping meetings.
int minRooms(vector<pair<int,int>>& mtgs) {
vector<pair<int,int>> ev; // (time, +1 for start, -1 for end)
for (auto& [s, e] : mtgs) {
ev.push_back({s, +1});
ev.push_back({e, -1}); // end-events sort first (–1 < +1) → no overcount
}
sort(ev.begin(), ev.end());
int cur = 0, best = 0;
for (auto& [t, d] : ev) {
cur += d;
best = max(best, cur);
}
return best;
}
// Time: O(n log n)
P9 · LRU cache concept
Constant-time get/put with eviction of least-recently-used: combine list (for ordering) with unordered_map (for lookup).
class LRUCache {
int cap;
list<pair<int,int>> ord; // (key, val), MRU at front
unordered_map<int, list<pair<int,int>>::iterator> mp;
public:
LRUCache(int c) : cap(c) {}
int get(int k) {
if (!mp.count(k)) return -1;
ord.splice(ord.begin(), ord, mp[k]); // O(1) move to front
return mp[k]->second;
}
void put(int k, int v) {
if (mp.count(k)) ord.erase(mp[k]);
ord.push_front({k, v});
mp[k] = ord.begin();
if ((int)ord.size() > cap) {
mp.erase(ord.back().first);
ord.pop_back();
}
}
};
P10 · Anagram groups
vector<vector<string>> anagrams(vector<string>& v) {
map<string, vector<string>> g;
for (string& w : v) {
string key = w;
sort(key.begin(), key.end());
g[key].push_back(w);
}
vector<vector<string>> ans;
for (auto& [k, group] : g) ans.push_back(group);
return ans;
}
P11 · Merge K sorted arrays
vector<int> mergeK(vector<vector<int>>& arrs) {
// (value, which array, index within that array)
using T = tuple<int, int, int>;
priority_queue<T, vector<T>, greater<T>> pq;
for (int i = 0; i < (int)arrs.size(); ++i)
if (!arrs[i].empty()) pq.push({arrs[i][0], i, 0});
vector<int> ans;
while (!pq.empty()) {
auto [val, ai, idx] = pq.top(); pq.pop();
ans.push_back(val);
if (idx + 1 < (int)arrs[ai].size())
pq.push({arrs[ai][idx+1], ai, idx+1});
}
return ans;
}
// Time: O(N log K) where N = total elements, K = number of arrays
P12 · Count inversions (merge-sort based)
long long inversions(vector<int>& a, int l, int r) {
if (r - l <= 1) return 0;
int m = (l + r) / 2;
long long ans = inversions(a, l, m) + inversions(a, m, r);
vector<int> tmp;
int i = l, j = m;
while (i < m && j < r) {
if (a[i] <= a[j]) tmp.push_back(a[i++]);
else { tmp.push_back(a[j++]); ans += m - i; }
}
while (i < m) tmp.push_back(a[i++]);
while (j < r) tmp.push_back(a[j++]);
copy(tmp.begin(), tmp.end(), a.begin() + l);
return ans;
}
// Time: O(n log n)
P13 · Range frequency queries (offline-style)
For each value, store the sorted positions where it occurs. Query "how many v in [L,R]?" with two binary searches.
unordered_map<int, vector<int>> pos;
for (int i = 0; i < n; ++i) pos[a[i]].push_back(i);
int freqInRange(int v, int L, int R) {
auto& p = pos[v];
return upper_bound(p.begin(), p.end(), R)
- lower_bound(p.begin(), p.end(), L);
}
// Per query: O(log n)
P14 · Subset enumeration with bitset
// Iterate all subsets of {0..n-1}
for (int mask = 0; mask < (1 << n); ++mask) {
bitset<32> b(mask);
cout << b << " popcount=" << b.count() << '\n';
}
// Iterate only the proper subsets of a given mask m (Gosper-style)
for (int sub = m; sub; sub = (sub - 1) & m) { ... }
P15 · BFS with visited (queue + unordered_set)
int shortestPath(unordered_map<int, vector<int>>& g, int src, int tgt) {
queue<pair<int, int>> q; // (node, distance)
unordered_set<int> vis;
q.push({src, 0});
vis.insert(src);
while (!q.empty()) {
auto [u, d] = q.front(); q.pop();
if (u == tgt) return d;
for (int v : g[u]) if (!vis.count(v)) {
vis.insert(v);
q.push({v, d+1});
}
}
return -1;
}
Part 12 · Complexity Reference Tables
Sequence containers
| Operation | vector | deque | list | array |
|---|---|---|---|---|
| operator[] | O(1) | O(1) | — | O(1) |
| push_back | O(1) amort. | O(1) amort. | O(1) | — |
| push_front | O(n) | O(1) amort. | O(1) | — |
| insert anywhere | O(n) | O(n) | O(1) with iter | — |
| erase anywhere | O(n) | O(n) | O(1) with iter | — |
| find by value | O(n) | O(n) | O(n) | O(n) |
| splice | — | — | O(1) | — |
| memory layout | contiguous | chunked | linked | contiguous (stack) |
Associative containers
| Operation | set / map | multiset / multimap | unordered_set / map |
|---|---|---|---|
| insert | O(log n) | O(log n) | O(1) avg · O(n) worst |
| erase by value | O(log n) | O(log n + k) | O(1) avg |
| erase by iterator | O(1) amort. | O(1) amort. | O(1) avg |
| find / count | O(log n) | O(log n + k) | O(1) avg |
| lower_bound / upper_bound | O(log n) | O(log n) | — |
| iterate sorted | O(n) | O(n) | not sorted |
Adapters
| Adapter | push | pop | top / front / back |
|---|---|---|---|
| stack | O(1) | O(1) | O(1) top |
| queue | O(1) | O(1) | O(1) front/back |
| priority_queue | O(log n) | O(log n) | O(1) top |
Algorithms
| Algorithm | Complexity | Notes |
|---|---|---|
| sort | O(n log n) | introsort, in place |
| stable_sort | O(n log n) | uses extra memory |
| nth_element | O(n) avg | partial selection |
| partial_sort | O(n log k) | top k |
| binary_search / lower_bound / upper_bound | O(log n) | random-access only |
| find / count / accumulate | O(n) | linear sweep |
| unique | O(n) | removes consecutive dups |
| reverse / rotate / fill | O(n) | in place |
| set_union / intersection / difference | O(n + m) | both ranges sorted |
| make_heap | O(n) | linear, not n log n! |
Part 12.2 · Top 15 Beginner Mistakes
- Comparing signed loop var to
v.size().size()is unsigned —i < v.size() - 1wraps around when v is empty. Fix: cast to(int). - Using
std::lower_boundon a set or map. O(n)! Fix: use the member functions.lower_bound(x)for O(log n). - Forgetting
removedoesn't actually remove. Fix: the erase–remove idiom:v.erase(remove(...), v.end()); map[k]just to check if k exists. Inserts a default value! Fix: usem.count(k)orm.find(k).multiset.erase(value)removes all copies. Fix:ms.erase(ms.find(value))for one.accumulate(v.begin(), v.end(), 0)overflowing on long longs. Fix: use0LL(or(long long)0).- Iterating and erasing simultaneously. Iterator gets invalidated. Fix: use the return value of
erase()or the erase–remove idiom. - Storing iterators/references after
push_back. Reallocation invalidates them. Fix: reserve up front, or store indices. - Using
list"because O(1) insert sounds good". Cache misses kill it. Fix: usevectorby default. stack.pop()returning the top. It returns void. Fix: read top first, then pop.- Using
unordered_map<long long,…>on Codeforces without custom hash. Anti-hash test cases blow up to O(n²). Fix: Part 9.4. - Comparator returning
truefor equal elements. Violates strict weak ordering — UB. Fix: use<, never<=. - Forgetting
cin.tie(nullptr)+sync_with_stdio(false). Slow I/O turns AC into TLE. Fix: always at the top ofmain. - Returning
autofrom a function with multiple return paths of different types. Compiler error or surprising deduction. Fix: declare an explicit return type. - Treating
nth_elementas a sort. It only places ONE element in its sorted spot. The rest is unordered.
🏁 Congratulations — you're a hero now
You've gone from "what's a vector?" to writing Dijkstra with custom hashes and rolling LRU caches. The next step is practice: solve at least 5 problems for each container/algorithm in this guide. STL fluency is reflex memory — you only get it from the seat.
Recommended next reads in this curriculum:
- 26-stl-valarray.html — numeric heavy-lifting with
std::valarray - 27-pbds.html — Policy-Based Data Structures (order statistics in O(log n))
