Module 16 · The STL in Competitive Programming: Which Container for Which Problem
Which Container Answers Your Next Question?
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.
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 CompilerMaria 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 CompilerDavid 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 CompilerFollow the labelled branch for the operation you actually require. The final row revisits size and payload, rather than treating them as optional questions.
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.
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.
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
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