Learn C++ STL

Lesson 1 of 9 · The STL in Competitive Programming: Which Container for Which Problem

Module 16 · The STL in Competitive Programming: Which Container for Which Problem

Which Container Answers Your Next Question?

FreeReading

In this lesson

  • Choose a container by the four questions from Module 2.
  • Explain why the next required operation matters more than a familiar name.
  • Route three small problems through one decision tree before writing code.

Alice has three jobs before the library opens. Returns must leave in arrival order. A shelf report needs alphabetical totals. A repair desk always takes the most urgent job. All three jobs store records, but their next questions differ.

Ask the same four questions

A container holds values and promises costs for certain operations. A pattern combines operations with a rule that keeps them correct. This module joins the containers you already know. It starts with questions, because choosing a name first hides the actual work.

Module 2 asked four questions. First, is the data a sequence, or do you look it up by key? Second, do you read by position, or search by value? Third, where does it grow and shrink? Fourth, how large is the collection, and how large is one element?

Keep all four answers on paper. One answer can outweigh another. A sorted sequence still needs expensive shifts when insertions happen throughout it. A linked list supports cheap insertion only after you already hold the place. Looking for that place still takes work.

The word invariant means a fact your program keeps true after each change. For a queue, arrival order remains intact. For a sorted vector, values remain in order. When choosing a container, name both the invariant and the operation you need next.

Sequence, key, or a restricted exit

A sequence keeps positions meaningful. Start with vector when you append, index, and scan small values. Choose array when its size belongs to the type. Choose string for a sequence of characters. Its byte positions do not automatically become human language character positions.

Deque supports both ends and indexing. It suits a line that gains customers at the back and loses them at the front. List suits changes around known iterators and moving whole linked ranges. It does not offer fast access to the middle by index.

A container adaptor exposes a restricted set of operations over another container. Stack exposes the newest item. Queue exposes the oldest. Priority_queue exposes an extreme according to its comparison rule. That restriction can express your algorithm's promise more clearly than a larger interface.

Do not choose priority_queue when you need an arbitrary key next. Its top is cheap, but searching inside is not its job. Do not choose stack just because the story has a pile. The departure rule, rather than the picture in the story, decides the useful interface.

Ordered keys, duplicates, and stored values

Set stores unique keys in sorted order. Multiset keeps repeated keys too. Map associates each unique key with a value. Multimap permits several entries with equivalent keys. All four support ordered searching, so asking for the next key makes sense.

The unordered forms use hashing. Unordered_set and unordered_multiset store keys; unordered_map and unordered_multimap also store mapped values. Their normal lookup cost is average constant time. Their worst case is linear, and their iteration order is not a sorted report.

The decision about duplicates comes from the statement. Two tickets with the same price remain two tickets. Two sightings of the same book may describe one distinct book. Losing a duplicate can change the answer even when the program still prints a plausible number.

When all values arrive before the questions, a sorted vector deserves another look. Sorting once can buy binary search and cheap indexing afterward. When values change between questions, set or map may fit better. Sorted storage is useful, but sorted storage alone does not provide fast rank queries.

Size and representation finish the choice

Bitset stores a fixed number of binary answers. A plain integer mask can represent a small fixed collection. Vector<bool> offers a runtime length of packed flags, with special reference behaviour. None replaces a map when the keys are sparse labels with associated records.

Pair and tuple group fields into one value. They are not replacements for the outer container. A priority_queue of pairs can choose the smallest distance, then the smallest node number. A vector of pairs can hold roads. The two layers answer different questions.

Large records deserve care. Moving a small index can be cheaper than moving a long record repeatedly. Keep records in stable storage when their identities must survive reordering. Then sort a vector of indexes, provided later changes do not invalidate the storage those indexes describe.

Also estimate the total memory, including overhead. A million linked nodes cost more than their payload alone. A vector may reserve spare capacity. An adjacency list contains many inner vectors. These costs are reasons to inspect constraints, not reasons to reject a container without measuring its workload.

Route three small jobs

For library returns, Alice answers: sequence, oldest first, back arrivals, front removals. Queue expresses that exit rule. If staff must also inspect both ends, deque becomes useful. The record count changes at runtime, so array is not the natural starting choice.

For alphabetical shelf totals, Maria answers: look up by shelf name, update a count, print keys in order. Map<string,int> fits those requirements. If the report is rare, an unordered_map plus a sorted key list is another design. Its extra sorting must appear in the cost.

For the repair desk, David answers: repeatedly take the smallest promised completion time. A min-priority_queue fits when only the next job matters. Cancellation adds another requirement. Then keep current records by id and discard stale heap entries when they surface, as Module 7 introduced.

These routes are not proofs of every future algorithm. A new requirement can change the answer. Sliding windows need expiry as well as an extreme. Dijkstra needs a current distance table as well as a heap. Later lessons add those invariants without changing the four starting questions.

Write a workload before choosing a type

queue<int> returns;             // Oldest first.
map<string, int> shelfCount;     // Key lookup and sorted reporting.
priority_queue<int> urgency;     // Largest currently waiting value.

The type argument says what one stored value is. The interface says which next question is cheap. Include each container's own header in a complete program.

Example 1: returns leave oldest first

Alice's arrivals happen before service in this tiny example. Read the front before popping it. Pop changes the collection and returns no item.

#include <iostream>
#include <queue>
using namespace std;
int main() {
    queue<int> returns;
    for (int id : {8, 3, 8}) returns.push(id);
    while (!returns.empty()) {
        cout << returns.front() << '\n'; returns.pop();
    }
}
8
3
8

This is a complete departure trace. Repeated labels still occupy separate queue entries.

Run in Compiler
Example 2: count and report by key

Maria needs counts as well as order. A set would preserve the names but lose those counts.

#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
    map<string,int> count;
    for (string shelf : {"Z", "A", "Z"}) ++count[shelf];
    for (const auto& [shelf, n] : count) cout << shelf << ' ' << n << '\n';
}
A 1
Z 2

Here operator[] deliberately creates a missing counter. A lookup that must not insert would use find instead.

Run in Compiler
Example 3: take the most urgent repair

David keeps duplicates because two repairs can share an urgency. He needs the largest value next, rather than the earliest arrival.

#include <iostream>
#include <queue>
using namespace std;
int main() {
    priority_queue<int> jobs;
    for (int x : {4, 9, 4}) jobs.push(x);
    while (!jobs.empty()) { cout << jobs.top() << '\n'; jobs.pop(); }
}
9
4
4

The program prints every departure. The heap does not preserve the original order.

Run in Compiler
Container decision tree Sequence or key lookup? Sequence: position or restricted exit? Index: array / vector / string; both ends: deque Known middle iterator: list; newest: stack; oldest: queue Extreme next: priority_queue; packed flags: bitset / vector<bool> Keys: sorted report or next key needed? Yes: set / multiset / map / multimap No: unordered_set / unordered_multiset / unordered_map / unordered_multimap Finish: duplicates? associated value? fixed size? large elements? sequence key index / ends other exit extreme / flags yes no size
Four questions lead to an interface, then a representation

Follow the labelled branch for the operation you actually require. The final row revisits size and payload, rather than treating them as optional questions.

The same steps as a table, one row per step.

StepOperationValuesWindowDequeMaxWhat happened
1advance2 5 1 40 00The first window is not full
2advance2 5 1 40 115The front names the largest active value
3advance2 5 1 41 21 25The front names the largest active value
4advance2 5 1 42 334The front names the largest active value

The deque stores indexes. A partial window has no reported maximum. Lesson 02 explains why some active indexes can disappear before they expire.

Where this is used

  • Ada Practice 124-circular-queue: its bounded FIFO requirement makes capacity part of the container choice.
  • Ada Practice 128-sliding-window-maximum: expiry and maximum queries require more than a plain queue.

Common mistakes

1. Index a linked collection

#include <list>
#include <iostream>
int main() { std::list<int> a{7,8}; std::cout << a[1]; }

GCC 12 stops with error: no match for 'operator[]' (operand types are 'std::__cxx11::list<int>' and 'int')

A list iterator walks; it does not provide indexing. Use vector when direct positions are the repeated operation.

2. Expect a queue pop to return a value

#include <queue>
int main() { std::queue<int> q; q.push(3); int x=q.pop(); return x; }

GCC 12 stops with error: void value not ignored as it ought to be

Read front first, behind an empty check. Pop then removes the entry. The two operations have separate jobs.

Brain teaser

Can three exit rules all fit?

All parcels arrive before any leave. Can FIFO, LIFO, and largest-first explain the same nonempty batch? Give a family of inputs.

Hint: consider whether all labels must differ.

Exercise 1: identify compatible policiesEasy

Alice finds two complete logs for a closed parcel batch. The first records all arrivals, before any parcel leaves. The second records all departures. She wants to know which rules could explain the whole departure log: oldest first, newest first, or largest label first. Several rules may fit. Equal labels are indistinguishable.

Input. Read n, n arrival labels, then n departure labels. The two lists contain the same labels with the same counts.

Output. Print three 0-or-1 flags, in order: FIFO, LIFO, largest-first. A flag is 1 exactly when that policy explains every departure.

Constraints. 1 <= n <= 2000; 0 <= label <= 1000000000.

Sample.

3
2 1 2
2 2 1
0 0 1
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main() {
    int n; cin >> n;
    vector<int> a(n), b(n);
    for (int& x : a) cin >> x;
    for (int& x : b) cin >> x;
    // Compare all three complete departure orders.
}
Run in Compiler

Graded as exit-policy-witness, a free problem in this module's problem set.

Exercise 2: order is evidenceMedium

Zara checks a shelf log. Count neighbouring entries with the same label. This is an ungraded sequence exercise.

Input. n, followed by n labels.

Output. The number of equal neighbouring pairs.

Constraints. 1 <= n <= 100; 0 <= label <= 100.

Sample

5
4 4 1 4 4
#include <iostream>
#include <vector>
using namespace std;
int main() { int n; cin>>n; vector<int> a(n); for(int& x:a) cin>>x;
    // Count adjacent equal pairs without reordering the sequence.
}

Sample output.

2
Run in Compiler
Exercise 3: choose what to retainHard

Maria wants the number of distinct labels appearing at least twice. Explain why a set alone discards needed evidence. This exercise is ungraded.

Input. n, then n labels.

Output. The number of distinct repeated labels.

Constraints. 1 <= n <= 100; 0 <= label <= 100.

Sample

6
4 1 4 2 2 4
#include <iostream>
#include <vector>
using namespace std;
int main() { int n; cin>>n; vector<int> a(n); for(int& x:a) cin>>x;
    // Report how many different labels repeat at least twice.
}

Sample output.

2
Run in Compiler

Common doubts

  • Is vector always the default?

    It is a useful starting point for sequences. It cannot make front erasures or repeated key searches cheap. Write the workload before deciding.

  • Why not keep every job in a map?

    A map orders keys and charges logarithmic costs for lookup. A queue can express arrival order directly. A map also stores links and keys that some jobs never need.

  • Are arrays obsolete when vectors exist?

    No. A fixed-size array expresses a size known in the type. It avoids runtime resizing, and still offers indexed access. Choose it when that fixed shape is part of the problem.

  • Does one container solve the whole algorithm?

    Often several cooperate. Dijkstra uses an adjacency list, distances, and a heap. Each keeps a different fact, and the algorithm joins those facts.

Key takeaways

  • Ask about sequence or keys, access, changes, and size.
  • Choose an exit rule from the required behaviour.
  • Preserve duplicates when the statement counts separate objects.
  • Separate the outer container from pair or tuple payloads.
  • Revisit costs when a new operation appears.
  • Go deeper: Changing Ranks With GCC pb_ds, and What a Plain Set Cannot Promise (Pro).

Next, maintain a moving window without rescanning its contents.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Sliding Windows and Two Pointers, With Their Proofs