πŸ“– Chapter 3.1 ⏱️ ~70 min read 🎯 Beginner

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 rules
  • pair β€” bundle two values together cleanly
  • map / set β€” ordered key-value store and unique-element collection
  • stack / queue β€” LIFO and FIFO containers for classic algorithms
  • priority_queue β€” always get the max (or min) in O(log N)
  • unordered_map / unordered_set β€” hash-based O(1) lookup
  • auto and range-based for β€” 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:

STL Toolbox

Quick reference β€” which container to reach for:

NeedUse
Ordered list, random accessvector
Two values bundled togetherpair
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 quicklypriority_queue

Picking the right tool = half the solution in competitive programming!

"Which Container Should I Use?" Decision Tree

STL Container Decision Tree

Visual: STL Containers Overview

STL Containers


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: sort requires #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 true when a == 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, .second gives you the original index.

πŸ’‘ make_pair vs brace initialization: Both make_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

OperationCodeTime
Insert/updatem[key] = valueO(log n)
Lookupm[key] or m.at(key)O(log n)
Check existencem.count(key) or m.find(key)O(log n)
Deletem.erase(key)O(log n)
Sizem.size()O(1)
Iterate allrange-forO(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: map default-initializes missing values. For int, the default is 0. So accessing a new key creates it with value 0, and ++ makes it 1. 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

OperationCodeTime
Inserts.insert(x)O(log n)
Check existences.count(x) or s.find(x)O(log n)
Deletes.erase(x)O(log n)
Min*s.begin()O(1)
Max*s.rbegin()O(1)
Sizes.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());

πŸ’‘ set vs multiset: set stores each value at most once. If you need to store duplicates but still want sorted order, use multiset<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

OperationCodeTime
Push to topst.push(x)O(1)
Remove from topst.pop()O(1)
Peek at topst.top()O(1)
Check if emptyst.empty()O(1)
Sizest.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

OperationCodeTime
Add to backq.push(x)O(1)
Remove from frontq.pop()O(1)
Peek frontq.front()O(1)
Peek backq.back()O(1)
Check if emptyq.empty()O(1)

Note: You'll use queue extensively in Chapter 5.2 for BFS β€” one of the most important graph algorithms for USACO.

πŸ› Common mistake: queue has no top() method (that's stack). Use front() to peek at the front element and back() 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

OperationCodeTime
Insertpq.push(x)O(log n)
Get max/minpq.top()O(1)
Remove max/minpq.pop()O(log n)
Check if emptypq.empty()O(1)

πŸ’‘ Priority queue with pairs: You can store pair<int, int> in a priority queue. It compares by .first first, 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 / set internally 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, supporting lower_bound and range queries.
  • unordered_map / unordered_set internally 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-case unordered_map behavior (being hacked), switching back to map is 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

ContainerWhen to use
map / setNeed sorted order; need lower_bound; small N; safety first
unordered_map / unordered_setOnly need lookup; N is large (> 10⁡); keys are strings or ints

⚑ Pro Tip: unordered_map can be 5-10Γ— faster than map for large inputs with string keys. But it has rare "worst case" behavior that can be exploited in competitive programming β€” use map if you're getting TLE with unordered_map and 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

⚠️ auto pitfall: auto deduces the type at compile time β€” it doesn't make variables dynamically typed. Also, auto x = 1000000000 * 2; deduces int and may overflow; write auto x = 1000000000LL * 2; to get long 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:

  1. All words and their counts in alphabetical order
  2. 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_element traverses 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 map give alphabetical order? map internally 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_element on the map, you could also transfer entries to a vector<pair<string,int>>, sort by .second descending, and take the first element. Both approaches are O(M) after counting.


Chapter Summary

πŸ“Œ Key Takeaways

ContainerDescriptionKey OperationsTimeWhy It Matters
vector<T>Dynamic arraypush_back, [], sizeO(1) amortizedMost commonly used container, default choice
pair<A,B>Store two values.first, .secondO(1)Graph edges, coordinates, etc.
map<K,V>Ordered key-value pairs[], find, countO(log n)Frequency counting, ordered mapping
set<T>Ordered unique setinsert, count, eraseO(log n)Deduplication, range queries
stack<T>Last-in first-outpush, pop, topO(1)Bracket matching, DFS
queue<T>First-in first-outpush, pop, frontO(1)BFS, simulation
priority_queue<T>Max-heappush, pop, topO(log n)Greedy max/min, Dijkstra
unordered_map<K,V>Hash map (unsorted)[], find, countO(1) avgFast lookup for large data
unordered_set<T>Hash set (unsorted)insert, count, eraseO(1) avgFast membership check, dedup

❓ FAQ

Q1: What is the difference between vector and a plain array? When to use which?

A: vector can 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, use vector for most cases; global large arrays (like int 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 or lower_bound/upper_bound, use map (O(log N)). In contests without special requirements, map is 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 as priority_queue<int, vector<int>, greater<int>>.

Q4: When is a custom struct better than pair?

A: pair's .first/.second have poor readability β€” three months later you may forget what .first means. struct lets you give members meaningful names (like .weight, .value). When data has 3 or more fields, struct is necessary.

Q5: Why does sort on a vector<pair<int,int>> sort by .first first?

A: pair has a built-in operator< that compares .first first; 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 set and map, both are O(log N) and functionally equivalent for checking existence. count returns 0 or 1 (sets have no duplicates), while find returns an iterator you can use to access the element directly. Prefer find when you also need to read the value; prefer count for 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 stack and queue algorithm applications β€” bracket matching, BFS
  • Chapter 3.8 (Maps & Sets): advanced usage of map/set β€” frequency counting, multiset
  • Chapter 3.3 (Sorting): sort with custom comparators used with vector and pair
  • priority_queue appears 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 not
  • while (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 string
  • freq[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 accessing st.top() or calling st.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 it
  • queue.pop() removes the front element (no return value β€” use front() before pop() 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 set because set would sort the output β€” we want original order
  • unordered_set gives O(1) average lookup: much faster than searching a vector
  • The first flag 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 map iterates 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 complement first (it appeared earlier in input) then arr[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 β€” add quantity units of product name
  • REMOVE name quantity β€” remove quantity units (if removing more than available, set to 0)
  • QUERY name β€” print current quantity of name (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 β€” if name doesn'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 long for 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 β‰₯ l
  • upper_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> β€” like set but 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

Trie 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).