Chapter 3.1: STL Essentials
π Prerequisites: Chapters 2.1β2.3 (variables, loops, functions, vectors)
The Standard Template Library (STL) is C++'s built-in collection of ready-made data structures and algorithms. Instead of writing your own linked lists, hash tables, or sorting algorithms from scratch, you use the STL β it's fast, reliable, and tested by millions of programmers.
Learning to pick the right STL container for a problem is one of the most important skills in competitive programming.
What you'll learn in this chapter:
sortβ sort any sequence in one line, with custom rulespairβ bundle two values together cleanlymap/setβ ordered key-value store and unique-element collectionstack/queueβ LIFO and FIFO containers for classic algorithmspriority_queueβ always get the max (or min) in O(log N)unordered_map/unordered_setβ hash-based O(1) lookupautoand range-basedforβ write cleaner, shorter code- Useful STL algorithms:
binary_search,lower_bound,accumulate, and more
3.1.0 The STL Toolbox
Think of the STL as a toolbox. Each tool is designed for a specific job:
Quick reference β which container to reach for:
| Need | Use |
|---|---|
| Ordered list, random access | vector |
| Two values bundled together | pair |
| Key β value mapping (sorted) | map |
| Unique elements (sorted) | set |
| Key β value mapping (fast, unsorted) | unordered_map |
| Unique elements (fast, unsorted) | unordered_set |
| LIFO (last in, first out) | stack |
| FIFO (first in, first out) | queue |
| Always get the max/min quickly | priority_queue |
Picking the right tool = half the solution in competitive programming!
"Which Container Should I Use?" Decision Tree
Visual: STL Containers Overview
3.1.1 sort β The Only Sort You Need
We start with sort because you'll use it in almost every problem.
What it is and why it matters
Sorting rearranges a sequence of elements into order. Rather than implementing your own sorting algorithm (which is error-prone and takes time), C++'s sort is:
- Fast: O(N log N) β the theoretical best for comparison-based sorting
- Easy to use: one line of code
- Flexible: sorts by any rule you define
β οΈ Important:
sortrequires#include <algorithm>(included automatically via#include <bits/stdc++.h>).
#include <bits/stdc++.h>
using namespace std;
int main() {
// Sorting a vector β ascending (default)
vector<int> v = {5, 2, 8, 1, 9, 3};
sort(v.begin(), v.end());
// v is now: {1, 2, 3, 5, 8, 9}
for (int x : v) cout << x << " ";
cout << "\n";
// Sorting descending
sort(v.begin(), v.end(), greater<int>());
// v is now: {9, 8, 5, 3, 2, 1}
// Sorting an array
int arr[] = {4, 2, 7, 1, 5};
int n = 5;
sort(arr, arr + n); // sorts arr[0..n-1]
// arr is now: {1, 2, 4, 5, 7}
return 0;
}
Custom Sort (Lambda Functions)
What if you want to sort by something other than the natural order? Use a lambda β a small inline function:
vector<int> v = {5, -3, 2, -8, 1};
// Sort by absolute value
sort(v.begin(), v.end(), [](int a, int b) {
return abs(a) < abs(b); // return true if a should come BEFORE b
});
// v is now: {1, 2, -3, 5, -8} (ordered by |value|)
The lambda [](int a, int b) { return ...; } is the comparison rule. It must return true if a should come before b in the sorted result.
π Common mistake: Never return
truewhena == bβ this violates the "strict weak ordering" rule and causes undefined behavior (crash or wrong answer). Always use<or>, never<=or>=.
// Sort a vector of pairs: by second element (descending), then first element (ascending)
vector<pair<int,int>> pts = {{3,5},{1,7},{2,5},{4,3}};
sort(pts.begin(), pts.end(), [](pair<int,int> a, pair<int,int> b) {
if (a.second != b.second) return a.second > b.second; // bigger second first
return a.first < b.first; // tie-break: smaller first first
});
// Result: {1,7}, {2,5}, {3,5}, {4,3}
3.1.2 pair β Storing Two Values Together
A pair bundles two values into one object. Think of it as a "mini-struct" for two values.
Why use pair? Often you need to keep two related values together β like (value, index), (x-coordinate, y-coordinate), or (score, name). pair does this cleanly.
#include <bits/stdc++.h>
using namespace std;
int main() {
// Create a pair
pair<int, int> point = {3, 5}; // (x, y)
pair<string, int> student = {"Alice", 95}; // (name, score)
// Access elements: .first and .second
cout << point.first << " " << point.second << "\n"; // 3 5
cout << student.first << ": " << student.second << "\n"; // Alice: 95
// Pairs support comparison: compares .first first, then .second
pair<int,int> a = {1, 3};
pair<int,int> b = {1, 5};
cout << (a < b) << "\n"; // 1 (true) β same .first, so compare .second: 3 < 5
// Very common pattern: sort by second element using pairs
vector<pair<int,int>> v = {{3,9},{1,2},{4,1},{1,5}};
sort(v.begin(), v.end()); // sorts by .first first, then .second
// Result: {1,2}, {1,5}, {3,9}, {4,1}
return 0;
}
β‘ Pro Tip: When you need to sort items by one value but keep track of their original index, store them as
pair<value, index>and sort. After sorting,.secondgives you the original index.
π‘
make_pairvs brace initialization: Bothmake_pair(3, 5)and{3, 5}create a pair. The brace syntax{3, 5}is shorter and preferred in modern C++ (C++11 and later).
3.1.3 map β The Dictionary
A map stores key-value pairs, like a real dictionary: given a word (key), look up its definition (value). Each key appears at most once.
When to use map
- Counting frequencies: word β count, score β frequency
- Mapping IDs to names: student_id β name
- Storing properties: cow_name β milk_production
#include <bits/stdc++.h>
using namespace std;
int main() {
map<string, int> phoneBook;
// Insert key-value pairs
phoneBook["Alice"] = 555001;
phoneBook["Bob"] = 555002;
phoneBook["Charlie"] = 555003;
// Lookup by key
cout << phoneBook["Alice"] << "\n"; // 555001
// Iterate (always in SORTED KEY order!)
for (auto& entry : phoneBook) {
cout << entry.first << " -> " << entry.second << "\n";
}
// Prints:
// Alice -> 555001
// Bob -> 555002
// Charlie -> 555003
// Erase a key
phoneBook.erase("Charlie");
cout << phoneBook.size() << "\n"; // 2
return 0;
}
Common Operations with Time Complexity
| Operation | Code | Time |
|---|---|---|
| Insert/update | m[key] = value | O(log n) |
| Lookup | m[key] or m.at(key) | O(log n) |
| Check existence | m.count(key) or m.find(key) | O(log n) |
| Delete | m.erase(key) | O(log n) |
| Size | m.size() | O(1) |
| Iterate all | range-for | O(n) |
π The map Access Gotcha
This is one of the most common map bugs:
map<string, int> freq;
// DANGER: accessing a non-existent key CREATES IT with value 0!
cout << freq["apple"] << "\n"; // prints 0, but now "apple" is IN the map!
cout << freq.size() << "\n"; // 1 β even though we only "looked", not "inserted"!
// SAFE way 1: check count first
if (freq.count("apple") > 0) {
cout << freq["apple"] << "\n"; // safe β key exists
}
// SAFE way 2: use .find()
auto it = freq.find("apple");
if (it != freq.end()) { // .end() means "not found"
cout << it->second << "\n"; // it->second is the value
}
Frequency Counting β The Most Common map Pattern
vector<string> words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
map<string, int> freq;
for (const string& w : words) {
freq[w]++; // if "w" doesn't exist, it's created with 0, then incremented to 1
}
// freq: appleβ3, bananaβ2, cherryβ1
for (auto& p : freq) {
cout << p.first << " appears " << p.second << " times\n";
}
π‘ Why
freq[w]++works even for new keys:mapdefault-initializes missing values. Forint, the default is0. So accessing a new key creates it with value0, and++makes it1. This is intentional and widely used for counting.
3.1.4 set β Unique Sorted Collection
A set stores unique elements in sorted order. Insert a duplicate β it's silently ignored.
When to use set
- Removing duplicates from a list
- Checking membership quickly: "have I seen this value before?"
- Getting the minimum/maximum of a dynamic collection
#include <bits/stdc++.h>
using namespace std;
int main() {
set<int> s;
s.insert(5);
s.insert(2);
s.insert(8);
s.insert(2); // duplicate β ignored! set stays {2, 5, 8}
s.insert(1);
// s is now: {1, 2, 5, 8} (automatically sorted, no duplicates)
// Check membership
cout << s.count(2) << "\n"; // 1 (exists)
cout << s.count(7) << "\n"; // 0 (not there)
// Erase
s.erase(2); // s = {1, 5, 8}
// Iterate (always sorted)
for (int x : s) cout << x << " ";
cout << "\n"; // 1 5 8
// Min and max
cout << *s.begin() << "\n"; // 1 (smallest; * dereferences the iterator)
cout << *s.rbegin() << "\n"; // 8 (largest; r = reverse)
cout << s.size() << "\n"; // 3
return 0;
}
Common Operations with Time Complexity
| Operation | Code | Time |
|---|---|---|
| Insert | s.insert(x) | O(log n) |
| Check existence | s.count(x) or s.find(x) | O(log n) |
| Delete | s.erase(x) | O(log n) |
| Min | *s.begin() | O(1) |
| Max | *s.rbegin() | O(1) |
| Size | s.size() | O(1) |
Deduplication with set
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
set<int> unique_set(v.begin(), v.end()); // construct set from vector
// unique_set = {1, 2, 3, 4, 5, 6, 9}
cout << "Unique count: " << unique_set.size() << "\n"; // 7
// Convert back to sorted vector if needed:
vector<int> deduped(unique_set.begin(), unique_set.end());
π‘
setvsmultiset:setstores each value at most once. If you need to store duplicates but still want sorted order, usemultiset<int>β it allows repeated elements.
3.1.5 stack β Last In, First Out
A stack works like a pile of plates: you can only add to the top or remove from the top. The last item added is the first one removed (LIFO: Last In, First Out).
When to use stack
- Matching brackets/parentheses
- Undo/redo history
- Depth-first search (DFS) β covered in later chapters
- Reversing a sequence
#include <bits/stdc++.h>
using namespace std;
int main() {
stack<int> st;
st.push(1); // [1] (top is on the right)
st.push(2); // [1, 2]
st.push(3); // [1, 2, 3]
cout << st.top() << "\n"; // 3 (peek at top without removing)
st.pop(); // remove top β [1, 2]
cout << st.top() << "\n"; // 2
cout << st.size() << "\n"; // 2
cout << st.empty() << "\n"; // 0 (not empty)
return 0;
}
Common Operations with Time Complexity
| Operation | Code | Time |
|---|---|---|
| Push to top | st.push(x) | O(1) |
| Remove from top | st.pop() | O(1) |
| Peek at top | st.top() | O(1) |
| Check if empty | st.empty() | O(1) |
| Size | st.size() | O(1) |
π Common Stack Mistake: Pop Without Checking
stack<int> st;
// st.top(); // CRASH! Can't peek at top of empty stack
// st.pop(); // CRASH! Can't pop from empty stack
// Always check before accessing:
if (!st.empty()) {
cout << st.top() << "\n";
st.pop();
}
Classic Stack Problem: Balanced Parentheses
string expr = "((a+b)*(c-d))";
stack<char> parens;
bool balanced = true;
for (char ch : expr) {
if (ch == '(') {
parens.push(ch); // opening: push onto stack
} else if (ch == ')') {
if (parens.empty()) { // closing with no matching opening
balanced = false;
break;
}
parens.pop(); // match found: pop the opening
}
}
if (!parens.empty()) balanced = false; // unmatched opening parens remain
cout << (balanced ? "Balanced" : "Not balanced") << "\n";
3.1.6 queue β First In, First Out
A queue works like a line at a store: you join at the back and leave from the front. The first person who joined is the first to be served (FIFO: First In, First Out).
When to use queue
- Simulating a line of customers, processes, tasks
- Breadth-first search (BFS) β one of the most important algorithms in competitive programming (Chapter 5.2)
- Processing items in the order they arrived
#include <bits/stdc++.h>
using namespace std;
int main() {
queue<int> q;
q.push(10); // [10]
q.push(20); // [10, 20]
q.push(30); // [10, 20, 30]
cout << q.front() << "\n"; // 10 (first in line β will leave first)
cout << q.back() << "\n"; // 30 (last in line β will leave last)
q.pop(); // remove from front β [20, 30]
cout << q.front() << "\n"; // 20
cout << q.size() << "\n"; // 2
return 0;
}
Common Operations with Time Complexity
| Operation | Code | Time |
|---|---|---|
| Add to back | q.push(x) | O(1) |
| Remove from front | q.pop() | O(1) |
| Peek front | q.front() | O(1) |
| Peek back | q.back() | O(1) |
| Check if empty | q.empty() | O(1) |
Note: You'll use
queueextensively in Chapter 5.2 for BFS β one of the most important graph algorithms for USACO.
π Common mistake:
queuehas notop()method (that'sstack). Usefront()to peek at the front element andback()to peek at the rear.
3.1.7 priority_queue β The Heap
A priority_queue is like a magic queue: no matter what order you insert things, it always gives you the largest element first (max-heap by default).
When to use priority_queue
- Always need the largest (or smallest) element quickly
- Finding the K largest numbers
- Dijkstra's shortest path algorithm (Chapter 5.4)
- Greedy algorithms where you always process the "best" item next
#include <bits/stdc++.h>
using namespace std;
int main() {
// Max-heap: always gives you the MAXIMUM
priority_queue<int> maxPQ;
maxPQ.push(5);
maxPQ.push(1);
maxPQ.push(8);
maxPQ.push(3);
// Pop in decreasing order
while (!maxPQ.empty()) {
cout << maxPQ.top() << " "; // always the current max
maxPQ.pop();
}
cout << "\n"; // prints: 8 5 3 1
return 0;
}
π The Min-Heap Gotcha
By default, priority_queue is a max-heap (gives largest first). For a min-heap (gives smallest first), you need a special syntax:
// MAX-heap (default) β gives LARGEST first
priority_queue<int> maxPQ;
// MIN-heap β gives SMALLEST first (note the extra template arguments!)
priority_queue<int, vector<int>, greater<int>> minPQ;
minPQ.push(5);
minPQ.push(1);
minPQ.push(8);
minPQ.push(3);
while (!minPQ.empty()) {
cout << minPQ.top() << " ";
minPQ.pop();
}
// prints: 1 3 5 8 (smallest first)
Common Operations with Time Complexity
| Operation | Code | Time |
|---|---|---|
| Insert | pq.push(x) | O(log n) |
| Get max/min | pq.top() | O(1) |
| Remove max/min | pq.pop() | O(log n) |
| Check if empty | pq.empty() | O(1) |
π‘ Priority queue with pairs: You can store
pair<int, int>in a priority queue. It compares by.firstfirst, then.secondβ useful for Dijkstra's algorithm where you store{distance, node}.priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> minPQ; // min-heap of (distance, node) pairs β used in Dijkstra
3.1.8 unordered_map and unordered_set β Hash-Based Speed
The regular map and set are sorted (using a balanced binary search tree internally), giving O(log N) operations. The unordered_ variants use a hash table for O(1) average time β faster, but no guaranteed ordering.
π‘ Underlying Principle: Why is map O(log N) while unordered_map is O(1)?
map/setinternally use a red-black tree (a self-balancing binary search tree). Each insert, find, or delete traverses from root to leaf, with tree height β logβN, hence O(log N). Advantage: elements are always sorted, supportinglower_boundand range queries.unordered_map/unordered_setinternally use a hash table. The hash function directly computes the storage location, averaging O(1). But element order is not guaranteed, and worst case can degrade to O(N) (with severe hash collisions).- Contest experience: If you only need lookup/insert without ordered traversal, prefer
unordered_map. But if you get TLE from worst-caseunordered_mapbehavior (being hacked), switching back tomapis the safest choice.
unordered_map<string, int> freq;
freq["apple"]++;
freq["banana"]++;
freq["apple"]++;
cout << freq["apple"] << "\n"; // 2 (same interface as map)
unordered_set<int> seen;
seen.insert(5);
seen.insert(10);
cout << seen.count(5) << "\n"; // 1 (found)
cout << seen.count(7) << "\n"; // 0 (not found)
Hack-Resistant unordered_map
In competitive programming, adversaries can craft inputs that cause many hash collisions, degrading unordered_map to O(N) per operation. A simple defense is to use a custom hash:
// Add this before main() to make unordered_map harder to hack
struct custom_hash {
size_t operator()(long long x) const {
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9LL;
x = (x ^ (x >> 27)) * 0x94d049bb133111ebLL;
return x ^ (x >> 31);
}
};
unordered_map<long long, int, custom_hash> safe_map;
For most problems, the default unordered_map is fine. Use the custom hash only when you suspect anti-hash tests.
When to use which
| Container | When to use |
|---|---|
map / set | Need sorted order; need lower_bound; small N; safety first |
unordered_map / unordered_set | Only need lookup; N is large (> 10β΅); keys are strings or ints |
β‘ Pro Tip:
unordered_mapcan be 5-10Γ faster thanmapfor large inputs with string keys. But it has rare "worst case" behavior that can be exploited in competitive programming β usemapif you're getting TLE withunordered_mapand suspect a hack.
3.1.9 The auto Keyword and Range-Based For
C++ can often figure out the type of a variable automatically. The auto keyword tells the compiler: "you figure out the type."
auto x = 42; // x is int
auto y = 3.14; // y is double
auto v = vector<int>{1, 2, 3}; // v is vector<int>
map<string, int> freq;
auto it = freq.find("cat"); // type would be map<string,int>::iterator β very long!
// auto saves you from writing that
β οΈ
autopitfall:autodeduces the type at compile time β it doesn't make variables dynamically typed. Also,auto x = 1000000000 * 2;deducesintand may overflow; writeauto x = 1000000000LL * 2;to getlong long.
Range-Based For
Clean iteration over any container:
vector<int> nums = {10, 20, 30, 40, 50};
// Read-only iteration (copies each element β fine for int, wasteful for string)
for (int x : nums) {
cout << x << " ";
}
// Reference: no copy, can modify elements
for (int& x : nums) {
x *= 2; // doubles each element in-place
}
// Const reference: no copy, read-only (best for large types like string)
for (const auto& x : nums) {
cout << x << " ";
}
Rule of thumb for range-based for:
- Small types (
int,char):for (int x : v)β copy is fine - Large types (
string,pair, structs):for (const auto& x : v)β avoid copying - Need to modify:
for (auto& x : v)
3.1.10 Useful STL Algorithms
These functions from <algorithm> and <numeric> work on any sequence:
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// Sort ascending
sort(v.begin(), v.end());
// v = {1, 1, 2, 3, 4, 5, 6, 9}
// Binary search (on SORTED sequence only!)
cout << binary_search(v.begin(), v.end(), 5) << "\n"; // 1 (found)
cout << binary_search(v.begin(), v.end(), 7) << "\n"; // 0 (not found)
// lower_bound: first position where value >= target
auto it = lower_bound(v.begin(), v.end(), 4);
cout << *it << "\n"; // 4
cout << (it - v.begin()) << "\n"; // index: 3
// upper_bound: first position where value > target
auto it2 = upper_bound(v.begin(), v.end(), 4);
cout << (it2 - v.begin()) << "\n"; // index: 4 (first element > 4)
// Elements in range [lo, hi]: upper_bound(hi+1) - lower_bound(lo)
// Min and max
cout << *min_element(v.begin(), v.end()) << "\n"; // 1
cout << *max_element(v.begin(), v.end()) << "\n"; // 9
// Sum of all elements
long long total = accumulate(v.begin(), v.end(), 0LL);
cout << total << "\n"; // 31
// Count occurrences
cout << count(v.begin(), v.end(), 1) << "\n"; // 2
// Reverse
reverse(v.begin(), v.end());
// Fill with a value
fill(v.begin(), v.end(), 0); // all zeros
return 0;
}
3.1.11 Putting It All Together: Word Frequency Counter
Let's build a complete mini-program that uses map, vector, and sort together.
Problem: Read N words, count how many times each word appears, then print:
- All words and their counts in alphabetical order
- The most frequently appearing word
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
// Step 1: Count word frequencies using a map
map<string, int> freq;
for (int i = 0; i < n; i++) {
string word;
cin >> word;
freq[word]++; // if word is new, creates entry with 0, then increments
}
// Step 2: Print all words in alphabetical order (map iterates in sorted key order)
cout << "All words:\n";
for (auto& entry : freq) {
// entry.first = word, entry.second = count
cout << entry.first << ": " << entry.second << "\n";
}
// Step 3: Find the most frequent word
// max_element on the map: compare by value (.second)
auto best = max_element(
freq.begin(),
freq.end(),
[](const pair<string,int>& a, const pair<string,int>& b) {
return a.second < b.second; // compare by count
}
);
cout << "\nMost frequent: \"" << best->first << "\" (appears "
<< best->second << " times)\n";
return 0;
}
Sample Input:
10
the cat sat on the mat the cat sat on
Sample Output:
All words:
cat: 2
mat: 1
on: 2
sat: 2
the: 3
Most frequent: "the" (appears 3 times)
Complexity Analysis:
- Time: O(N log N) β each
freq[word]++is O(log N), N times total;max_elementtraverses the map in O(M), where M is the number of distinct words- Space: O(M) β map stores M distinct words
π€ Why does iterating over
mapgive alphabetical order?mapinternally uses a balanced BST (binary search tree), which keeps keys sorted. So when you iterate, you always get keys in sorted order automatically β no extra sorting needed!
π‘ Alternative for finding max: Instead of
max_elementon the map, you could also transfer entries to avector<pair<string,int>>, sort by.seconddescending, and take the first element. Both approaches are O(M) after counting.
Chapter Summary
π Key Takeaways
| Container | Description | Key Operations | Time | Why It Matters |
|---|---|---|---|---|
vector<T> | Dynamic array | push_back, [], size | O(1) amortized | Most commonly used container, default choice |
pair<A,B> | Store two values | .first, .second | O(1) | Graph edges, coordinates, etc. |
map<K,V> | Ordered key-value pairs | [], find, count | O(log n) | Frequency counting, ordered mapping |
set<T> | Ordered unique set | insert, count, erase | O(log n) | Deduplication, range queries |
stack<T> | Last-in first-out | push, pop, top | O(1) | Bracket matching, DFS |
queue<T> | First-in first-out | push, pop, front | O(1) | BFS, simulation |
priority_queue<T> | Max-heap | push, pop, top | O(log n) | Greedy max/min, Dijkstra |
unordered_map<K,V> | Hash map (unsorted) | [], find, count | O(1) avg | Fast lookup for large data |
unordered_set<T> | Hash set (unsorted) | insert, count, erase | O(1) avg | Fast membership check, dedup |
β FAQ
Q1: What is the difference between vector and a plain array? When to use which?
A:
vectorcan grow dynamically (push_back), knows its own size (.size()), and can be safely passed to functions. Plain arrays have fixed size but are slightly faster (global arrays auto-initialize to 0). In contests, usevectorfor most cases; global large arrays (likeint dp[100001]) are sometimes more convenient as C arrays.
Q2: When to choose map vs unordered_map?
A: If you only need lookup/insert/delete, use
unordered_map(O(1)) for speed. If you need ordered traversal orlower_bound/upper_bound, usemap(O(log N)). In contests without special requirements,mapis safer (cannot be hacked).
Q3: Is priority_queue a max-heap or min-heap by default?
A: Max-heap.
pq.top()returns the maximum element. For a min-heap, declare aspriority_queue<int, vector<int>, greater<int>>.
Q4: When is a custom struct better than pair?
A:
pair's.first/.secondhave poor readability β three months later you may forget what.firstmeans.structlets you give members meaningful names (like.weight,.value). When data has 3 or more fields,structis necessary.
Q5: Why does sort on a vector<pair<int,int>> sort by .first first?
A:
pairhas a built-inoperator<that compares.firstfirst; if equal, it compares.second. This is called lexicographic order β the same way words are sorted in a dictionary. You can rely on this behavior without writing a custom comparator.
Q6: What's the difference between s.count(x) and s.find(x) != s.end() for a set?
A: For
setandmap, both are O(log N) and functionally equivalent for checking existence.countreturns 0 or 1 (sets have no duplicates), whilefindreturns an iterator you can use to access the element directly. Preferfindwhen you also need to read the value; prefercountfor a simple yes/no check.
π Connections to Later Chapters
- Chapter 3.4 (Monotonic Stack & Queue): monotonic stack for next-greater-element problems; monotonic deque for sliding window max/min
- Chapter 3.6 (Stacks & Queues): deep dive into
stackandqueuealgorithm applications β bracket matching, BFS - Chapter 3.8 (Maps & Sets): advanced usage of
map/setβ frequency counting,multiset - Chapter 3.3 (Sorting):
sortwith custom comparators used withvectorandpair priority_queueappears frequently in Chapter 4.1 (Greedy) and Chapter 5.3 (Kruskal MST)- The STL containers in this chapter are the foundational tools for all subsequent chapters in this book
Practice Problems
π‘οΈ Warm-Up Problems
Warm-up 3.1.1 β Set Membership
Read N integers, then read Q queries. For each query, read one integer and print YES if it appeared in the original N integers, NO otherwise.
π Sample Input/Output (6 lines, click to expand)
Sample Input:
5
10 20 30 40 50
3
20
35
50
Sample Output:
YES
NO
YES
π‘ Solution (click to reveal)
Approach: Store the N integers in a set, then for each query check s.count(x).
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
set<int> s;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
s.insert(x);
}
int q;
cin >> q;
while (q--) {
int x;
cin >> x;
cout << (s.count(x) ? "YES" : "NO") << "\n";
}
return 0;
}
Key points:
s.count(x)returns 1 if x is in the set, 0 if notwhile (q--)is a common idiom: runs the loop q times (q decrements each time)- Using a set gives O(log N) per query vs O(N) for a linear search
Warm-up 3.1.2 β Character Frequency Read a string (no spaces). Print each character that appears in it along with its count, in alphabetical order.
Sample Input: hello
Sample Output:
e: 1
h: 1
l: 2
o: 1
π‘ Solution (click to reveal)
Approach: Use a map<char, int> to count character frequencies. Iterating over the map gives alphabetical order automatically.
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
map<char, int> freq;
for (char c : s) {
freq[c]++;
}
for (auto& entry : freq) {
cout << entry.first << ": " << entry.second << "\n";
}
return 0;
}
Key points:
for (char c : s)iterates over each character in the stringfreq[c]++creates the entry with value 0 on first access, then increments it- Map iteration is always in sorted key order β so characters come out alphabetically
Warm-up 3.1.3 β Reverse with Stack
Read a string (no spaces). Use a stack to print the string in reverse.
Sample Input: hello β Sample Output: olleh
π‘ Solution (click to reveal)
Approach: Push each character onto a stack. Then pop them all β LIFO order gives reverse order.
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
stack<char> st;
for (char c : s) {
st.push(c);
}
while (!st.empty()) {
cout << st.top();
st.pop();
}
cout << "\n";
return 0;
}
Key points:
- Stack's LIFO property: last character pushed is first popped = reversal
- Always check
!st.empty()before accessingst.top()or callingst.pop() - Note:
reverse(s.begin(), s.end()); cout << s;is simpler β but using a stack teaches the concept
Warm-up 3.1.4 β Queue Simulation Simulate a line of exactly 5 people. Their names are: Alice, Bob, Charlie, Dave, Eve. They join in that order. Serve them one at a time (pop from front) and print each name as they're served.
Expected Output:
Serving: Alice
Serving: Bob
Serving: Charlie
Serving: Dave
Serving: Eve
π‘ Solution (click to reveal)
Approach: Push all names into a queue, then pop and print until empty.
#include <bits/stdc++.h>
using namespace std;
int main() {
queue<string> line;
line.push("Alice");
line.push("Bob");
line.push("Charlie");
line.push("Dave");
line.push("Eve");
while (!line.empty()) {
cout << "Serving: " << line.front() << "\n";
line.pop();
}
return 0;
}
Key points:
queue.front()accesses the first element WITHOUT removing itqueue.pop()removes the front element (no return value β usefront()beforepop()if you need the value)- Queue maintains insertion order β first pushed is first popped
Warm-up 3.1.5 β Top 3 Largest
Read N integers. Using a priority_queue, find and print the 3 largest values (in descending order).
Sample Input:
7
5 1 9 3 7 2 8
Sample Output:
9
8
7
π‘ Solution (click to reveal)
Approach: Push all into a max-heap priority_queue. Pop 3 times to get the 3 largest.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
priority_queue<int> pq;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
pq.push(x);
}
for (int i = 0; i < 3 && !pq.empty(); i++) {
cout << pq.top() << "\n";
pq.pop();
}
return 0;
}
Key points:
priority_queue<int>is a max-heap βtop()always gives the largest- We pop 3 times to get the 3 largest in order
- The
&& !pq.empty()guard handles the edge case where N < 3
ποΈ Core Practice Problems
Problem 3.1.6 β Unique Elements Read N integers. Print only the unique values, in the order they first appeared (not sorted). If a value appears more than once, print it only on its first occurrence.
π Sample Input/Output (113 lines, click to expand)
Sample Input:
8
3 1 4 1 5 9 2 6
Sample Output: 3 1 4 5 9 2 6
(Note: 1 appears twice but is printed only once, at its first position.)
π‘ Solution (click to reveal)
Approach: Use an unordered_set to track which values we've seen. For each element, print it only if we haven't seen it yet.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
unordered_set<int> seen;
bool first = true;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
if (seen.count(x) == 0) { // haven't seen x yet
seen.insert(x);
if (!first) cout << " ";
cout << x;
first = false;
}
}
cout << "\n";
return 0;
}
Key points:
- We can't use a regular
setbecause set would sort the output β we want original order unordered_setgives O(1) average lookup: much faster than searching a vector- The
firstflag handles spacing (no leading/trailing space)
Problem 3.1.7 β Most Frequent Word Read N words. Print the word that appears most often. If there's a tie, print the alphabetically smallest word.
Sample Input:
7
apple banana apple cherry banana apple cherry
Sample Output: apple
π‘ Solution (click to reveal)
Approach: Count with map, then find the maximum count. Among all words with that count, pick the alphabetically smallest (which map iteration naturally gives us).
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
map<string, int> freq;
for (int i = 0; i < n; i++) {
string w;
cin >> w;
freq[w]++;
}
string bestWord;
int bestCount = 0;
// Map iterates in sorted (alphabetical) order β so first word we see with max count wins
for (auto& entry : freq) {
if (entry.second > bestCount) {
bestCount = entry.second;
bestWord = entry.first;
}
}
cout << bestWord << "\n";
return 0;
}
Key points:
- Using
>(strictly greater) means we keep the FIRST word that achieves the max count - Since
mapiterates alphabetically, the first max-count word seen is the alphabetically smallest one - Tie-breaking is handled automatically because of map's sorted property
Problem 3.1.8 β Pair Sum Read N integers and a target T. For each pair of values (a, b) where a appears before b in the input and a + b = T, print the pair. Use a set for an O(N) solution.
Sample Input:
6 9
1 8 3 6 4 5
Sample Output:
1 8
3 6
4 5
π‘ Solution (click to reveal)
Approach: For each element x, check if T - x is already in a set of previously seen elements. If yes, we found a pair.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, t;
cin >> n >> t;
set<int> seen;
vector<int> arr(n);
for (int i = 0; i < n; i++) cin >> arr[i];
for (int i = 0; i < n; i++) {
int complement = t - arr[i]; // we need arr[i] + complement = t
if (seen.count(complement)) {
// complement came before arr[i], print in order: complement arr[i]
cout << complement << " " << arr[i] << "\n";
}
seen.insert(arr[i]);
}
return 0;
}
Key points:
- For each element x, the "complement" needed is
T - x - If the complement is already in our "seen" set, it came earlier in the array β valid pair
- This is O(N log N) with set (or O(N) with unordered_set) vs O(NΒ²) for the brute force
- We print
complementfirst (it appeared earlier in input) thenarr[i]
Problem 3.1.9 β Bracket Matching
Read a string containing only (, ), [, ], {, }. Print YES if all brackets are properly matched and nested, NO otherwise.
Sample Input 1: {[()]} β Output: YES
Sample Input 2: ([)] β Output: NO
Sample Input 3: ((() β Output: NO
π‘ Solution (click to reveal)
Approach: Use a stack. Push opening brackets. When we see a closing bracket, check if the top of the stack is the matching opening bracket.
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
stack<char> st;
bool ok = true;
for (char ch : s) {
if (ch == '(' || ch == '[' || ch == '{') {
st.push(ch); // opening: push
} else {
// closing bracket
if (st.empty()) {
ok = false; // no matching opening
break;
}
char top = st.top();
st.pop();
// Check if it matches
if ((ch == ')' && top != '(') ||
(ch == ']' && top != '[') ||
(ch == '}' && top != '{')) {
ok = false;
break;
}
}
}
if (!st.empty()) ok = false; // leftover unmatched opening brackets
cout << (ok ? "YES" : "NO") << "\n";
return 0;
}
Key points:
- The key insight: the most recently opened bracket must be the next one to close
- Stack's LIFO property perfectly models this "most recent opening" requirement
- Three failure conditions: (1) closing bracket with empty stack, (2) mismatched bracket type, (3) leftover unclosed brackets at end
Problem 3.1.10 β Top K Elements Read N integers and K. Print the K largest values in descending order.
Sample Input:
8 3
4 9 1 7 3 5 2 8
Sample Output:
9
8
7
π‘ Solution (click to reveal)
Approach: Push all into a max-heap, then pop K times.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, k;
cin >> n >> k;
priority_queue<int> pq;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
pq.push(x);
}
for (int i = 0; i < k && !pq.empty(); i++) {
cout << pq.top() << "\n";
pq.pop();
}
return 0;
}
Key points:
- Priority queue automatically keeps the maximum at the top
- Each
pop()removes the current maximum, revealing the next largest - Alternative: sort in descending order and take first K β same result
π Challenge Problems
Challenge 3.1.11 β Inventory System Process M commands on a store inventory. Each command is one of:
ADD name quantityβ addquantityunits of productnameREMOVE name quantityβ removequantityunits (if removing more than available, set to 0)QUERY nameβ print current quantity ofname(0 if never added)
π Sample Input/Output (7 lines, click to expand)
Sample Input:
6
ADD apple 10
ADD banana 5
QUERY apple
REMOVE apple 3
QUERY apple
QUERY grape
Sample Output:
10
7
0
π‘ Solution (click to reveal)
Approach: Use a map<string, long long> as the inventory. Process each command by parsing the type and updating accordingly.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int m;
cin >> m;
map<string, long long> inventory;
while (m--) {
string cmd;
cin >> cmd;
if (cmd == "ADD") {
string name;
long long qty;
cin >> name >> qty;
inventory[name] += qty;
} else if (cmd == "REMOVE") {
string name;
long long qty;
cin >> name >> qty;
inventory[name] -= qty;
if (inventory[name] < 0) inventory[name] = 0;
} else { // QUERY
string name;
cin >> name;
// Use count to avoid creating an entry for missing items
if (inventory.count(name)) {
cout << inventory[name] << "\n";
} else {
cout << 0 << "\n";
}
}
}
return 0;
}
Key points:
inventory[name] += qtyβ ifnamedoesn't exist, it's created with 0 then has qty added (correct!)- For QUERY, use
inventory.count(name)to check existence before accessing β avoids silently creating a 0 entry long longfor quantities in case they're large
Challenge 3.1.12 β Sliding Window Maximum Read N integers and a window size K. Print the maximum value in each window of K consecutive elements.
Sample Input:
8 3
1 3 -1 -3 5 3 6 7
Sample Output:
3
3
5
5
6
7
(Windows: [1,3,-1]β3, [3,-1,-3]β3, [-1,-3,5]β5, [-3,5,3]β5, [5,3,6]β6, [3,6,7]β7)
π‘ Solution (click to reveal)
Approach: Use a deque (double-ended queue) to maintain a window of useful indices. The deque stores indices in decreasing order of their values β so deque.front() is always the index of the current window's maximum.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, k;
cin >> n >> k;
vector<int> arr(n);
for (int i = 0; i < n; i++) cin >> arr[i];
deque<int> dq; // stores indices, front = index of current maximum
for (int i = 0; i < n; i++) {
// Remove indices that are out of the current window
while (!dq.empty() && dq.front() < i - k + 1) {
dq.pop_front();
}
// Remove indices of elements smaller than arr[i] from the back
// (they can never be the maximum while arr[i] is in the window)
while (!dq.empty() && arr[dq.back()] <= arr[i]) {
dq.pop_back();
}
dq.push_back(i);
// Print maximum for windows that are full (starting from index k-1)
if (i >= k - 1) {
cout << arr[dq.front()] << "\n";
}
}
return 0;
}
Key points:
- The deque maintains a "decreasing monotone queue" of indices
- Front of deque = index of the maximum in current window
- When we add new element
arr[i]: remove from back all elements β€arr[i](they're useless β arr[i] is larger and will stay in window longer) - Remove from front when that index is no longer in the window (index < i - k + 1)
- This gives O(N) total β each index is pushed and popped at most once
Challenge 3.1.13 β Haybales Range Count (USACO Bronze style)
N haybales are placed at various positions on a number line. Process Q queries: for each query (L, R), print how many haybales have positions in the range [L, R] inclusive.
π Sample Input/Output (6 lines, click to expand)
Sample Input:
5 4
3 1 7 5 2
1 3
2 6
4 8
1 10
Sample Output:
3
3
2
5
π‘ Solution (click to reveal)
Approach: Sort the positions. For each query, use lower_bound and upper_bound to find the count in range [L, R] in O(log N).
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, q;
cin >> n >> q;
vector<int> pos(n);
for (int i = 0; i < n; i++) cin >> pos[i];
sort(pos.begin(), pos.end()); // must sort for binary search to work
while (q--) {
int l, r;
cin >> l >> r;
// lower_bound(l): first position >= l
// upper_bound(r): first position > r
// Elements in [l, r] = count between these two iterators
auto lo = lower_bound(pos.begin(), pos.end(), l);
auto hi = upper_bound(pos.begin(), pos.end(), r);
cout << (hi - lo) << "\n"; // distance between iterators = count
}
return 0;
}
Key points:
- Sort the positions first β required for binary search
lower_bound(pos.begin(), pos.end(), l)returns an iterator to the first element β₯ lupper_bound(pos.begin(), pos.end(), r)returns an iterator to the first element > r- The count of elements in [l, r] = distance between these two iterators =
hi - lo - This is O(N log N) for sorting + O(Q log N) for queries β much better than O(NΓQ) brute force
Looking Ahead: Beyond Basic STL
This chapter covered the core STL containers you'll use in 90% of problems. As you progress, you'll encounter more specialized structures:
deque<T>β double-ended queue; supports O(1) push/pop at both front and back. Used in the sliding window maximum problem (Challenge 3.1.12) and monotonic deque (Chapter 3.4).multiset<T>β likesetbut allows duplicate elements. Useful when you need sorted order with repeats.bitset<N>β fixed-size sequence of bits; extremely fast for subset/membership problems.- Trie (prefix tree) β stores strings by sharing common prefixes, enabling O(L) lookup where L is string length.
Visual: Trie Data Structure
A trie (prefix tree) stores strings by sharing common prefixes. Words "bat", "car", "card", "care", "cat" share prefixes efficiently: "ca" is stored once, branching to "r" and "t". Double-ringed nodes mark word endings. Tries are used for autocomplete, spell checking, and string matching. For string hashing alternatives, see Chapter 3.7 (Hashing Techniques).