STL
Ввод и вывод
Ускорение стандартного ввода-вывода
В начале main, до первого ввода или вывода:
ios::sync_with_stdio(false);
cin.tie(nullptr);
ios::sync_with_stdio(false)отключает синхронизацию потоков C++ (cin,cout) с потоками C (scanf,printf). После этого не стоит смешивать эти способы ввода-вывода.cin.tie(nullptr)отключает автоматический сброс буфераcoutперед чтением изcin.
endl и '\n'
cout << answer << '\n'; // Перевод строки.
cout << answer << endl; // Перевод строки и сброс буфера.
В обычных задачах используйте '\n': сбрасывать буфер после каждой строки не нужно.
В интерактивных задачах запрос должен попасть в систему проверки до ожидания ответа. Используйте endl или cout.flush(). Настройки ускорения при этом допустимы, если вывод сбрасывается явно.
Файловый ввод-вывод
#include <fstream>
using namespace std;
int main() {
ifstream fin("input.txt");
ofstream fout("output.txt");
int x;
fin >> x;
fout << x * 2 << '\n';
}
fin и fout используются так же, как cin и cout. Имена файлов берите из условия задачи.
getline
cin >> s читает строку до первого пробельного символа. getline(cin, s) читает всю строку, включая пробелы, до перевода строки. Сам перевод строки в s не попадает.
string s;
getline(cin, s);
cout << s << '\n';
Если количество строк заранее неизвестно, читаем до конца файла:
string s;
while (getline(cin, s)) {
// Обрабатываем строку s.
}
Для файлового ввода вместо cin используем fin. Не пишите while (!cin.eof()): проверять нужно результат чтения.
setprecision
#include <iomanip>
// Внутри main:
double x = 3.1415926535;
cout << fixed << setprecision(2) << x << '\n'; // 3.14
С fixed задаётся число знаков после точки, без fixed — число значащих цифр при стандартном формате вывода. Форматирование меняет вывод, а не значение переменной.
Итераторы
Итератор указывает на элемент контейнера. Через него можно получить значение и перейти к другому элементу.
vector<int> v = {10, 20, 30};
auto it = v.begin();
cout << *it << '\n'; // 10
++it;
cout << *it << '\n'; // 20
auto просит компилятор определить тип переменной по её начальному значению. Например, тип it здесь — vector<int>::iterator.
begin() и end()
begin()указывает на первый элемент.end()указывает на позицию после последнего элемента.- У пустого контейнера
begin() == end().
Разыменовывать end() нельзя. Поэтому результат поиска сначала проверяют:
if (it != v.end()) {
cout << *it << '\n';
}
Передвижение и сравнение
Для vector доступны:
++it;
--it;
it += 2;
Все переходы должны оставаться в пределах от begin() до end(). Уменьшать begin() и увеличивать end() нельзя.
Индекс элемента в vector:
auto index = it - v.begin();
Для итераторов одного vector можно использовать <, >, <=, >=: сравнивается положение в контейнере.
У set и map нет арифметики итераторов: нельзя написать it + 2, вычесть итераторы или сравнить их через <. Можно использовать ++, --, ==, !=.
Обход контейнера
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << ' ';
}
Если нужен только обход всех значений, удобнее цикл по диапазону:
for (auto x : v) {
cout << x << ' ';
}
auto x— копия элемента.auto& x— ссылка: можно изменять элемент контейнера.const auto& x— ссылка без копирования и без возможности изменить элемент через неё.
Пример ввода массива:
vector<int> a(n);
for (auto& x : a) {
cin >> x;
}
Без & ввод изменит только локальную копию x, а элементы массива останутся прежними.
Когда итератор перестаёт быть действительным
- Если
vectorпри добавлении элементов перевыделил память, все прежние итераторы на него недействительны. vector::eraseделает недействительными итераторы на удалённый элемент и все позиции после него, включая прежнийend().- Вставка в
setиmapне делает прежние итераторы недействительными. При удалении недействителен только итератор на удалённый элемент.
Сортировка
std::sort из <algorithm> сортирует элементы за O(nlog n) сравнений. Для чисел по умолчанию используется порядок по возрастанию.
vector<int> v = {5, 1, 4, 2, 3};
sort(v.begin(), v.end()); // 1 2 3 4 5
Алгоритм работает на полуинтервале [first, last): левая граница включена, правая — нет.
Сортировка части вектора
sort(v.begin() + 1, v.begin() + 4);
Сортируются элементы с индексами 1, 2, 3.
Обратный порядок
sort(v.rbegin(), v.rend());
rbegin() и rend() задают обход в обратном направлении. После этой сортировки числа в самом векторе стоят по убыванию.
Удаление повторов: sort + unique
vector<int> v = {3, 1, 3, 2, 1};
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
// v = {1, 2, 3}
unique оставляет по одному элементу из каждой группы соседних равных элементов и возвращает итератор за концом полученной последовательности.
Сам unique не уменьшает размер вектора. Ненужный хвост удаляет erase. Предварительная сортировка нужна, чтобы все равные элементы оказались рядом.
Встроенный бинарный поиск
Функции из <algorithm>:
| Функция | Результат |
|---|---|
binary_search(first, last, x) |
true, если x найден, иначе false |
lower_bound(first, last, x) |
Итератор на первый элемент ≥ x |
upper_bound(first, last, x) |
Итератор на первый элемент > x |
Для обычного вызова массив должен быть отсортирован по возрастанию. Если подходящего элемента нет, lower_bound и upper_bound возвращают last.
vector<int> v = {1, 2, 2, 4};
bool found = binary_search(v.begin(), v.end(), 2); // true
auto first = lower_bound(v.begin(), v.end(), 2); // Индекс 1.
auto last = upper_bound(v.begin(), v.end(), 2); // Индекс 3.
cout << last - first << '\n'; // 2 вхождения числа 2.
Для vector каждый такой поиск занимает O(log n) времени.
auto it = lower_bound(v.begin(), v.end(), 3);
if (it != v.end()) {
cout << *it << '\n'; // 4
}
lower_bound(x) может вернуть элемент, не равный x. Он ищет первый элемент, который не меньше x.
Компараторы
Компаратор принимает два элемента и возвращает true, если первый должен стоять раньше второго.
Функция как компаратор
bool cmp(int a, int b) {
return a > b;
}
// Внутри main:
sort(v.begin(), v.end(), cmp);
Вектор будет отсортирован по убыванию.
Используйте строгие сравнения: < или >, а не <= или >=. Для равных по выбранным критериям элементов компаратор должен возвращать false в обе стороны.
Порядок должен быть согласованным: если a раньше b, а b раньше c, то a должно быть раньше c. Во время сортировки компаратор не должен менять элементы или правило сравнения.
Передача по ссылке
bool cmp_vec(const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
}
// Внутри main:
vector<vector<int>> vv = {{3, 4}, {1, 5}, {2, 6}};
sort(vv.begin(), vv.end(), cmp_vec);
Здесь все внутренние векторы непустые, поэтому можно обращаться к a[0] и b[0].
Строки, векторы и большие структуры передавайте в компаратор через const ...&. Это позволяет читать аргументы без копирования. Если копировать векторы длины m при каждом сравнении, время сортировки может вырасти до O(nlog n ⋅ m).
Лямбда-функция
Лямбда позволяет записать компаратор прямо в месте вызова:
sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
Её можно сохранить в переменную:
auto cmp = [](int a, int b) {
return a > b;
};
sort(v.begin(), v.end(), cmp);
Доступ к внешним переменным
vector<int> a = {30, 10, 20};
vector<int> indices = {0, 1, 2};
sort(indices.begin(), indices.end(), [&](int i, int j) {
return a[i] < a[j];
});
// indices = {1, 2, 0}; сам a не изменился.
[&] даёт доступ к используемым внешним переменным по ссылке. Здесь сортируются индексы по значениям соответствующих элементов массива.
Сортировка по нескольким полям
vector<pair<int, int>> p = {{2, 5}, {1, 4}, {2, 3}};
sort(p.begin(), p.end(), [](const auto& a, const auto& b) {
if (a.first != b.first) {
return a.first < b.first;
}
return a.second < b.second;
});
// {1, 4}, {2, 3}, {2, 5}
Пары уже сравниваются так по умолчанию: сначала first, при равенстве — second. В этом примере достаточно sort(p.begin(), p.end()).
Строки сравниваются лексикографически. Если одно слово — начало другого, более короткое стоит раньше. Строковое сравнение чисел не совпадает с числовым: строка "10A" меньше "2A". Составные поля вроде класса нужно сравнивать по номеру, затем по букве.
operator< внутри структуры
struct Date {
int day, month, year;
bool operator<(const Date& other) const {
if (year != other.year) {
return year < other.year;
}
if (month != other.month) {
return month < other.month;
}
return day < other.day;
}
};
// Внутри main:
vector<Date> dates = {
{5, 3, 2000},
{12, 7, 1999},
{5, 3, 1999}
};
sort(dates.begin(), dates.end());
Даты сравниваются в порядке год → месяц → день, хотя в структуре поля записаны иначе.
const Date& other— аргумент передаётся без копирования и не изменяется через эту ссылку.constпосле скобок — метод не изменяет сам объект, для которого вызван.
Лимиты чисел
В обычной олимпиадной среде int занимает 32 бита, а long long — 64 бита:
| Тип | Диапазон |
|---|---|
int |
От −231 до 231 − 1, примерно ±2.1 ⋅ 109 |
long long |
От −263 до 263 − 1, примерно ±9.2 ⋅ 1018 |
Размеры зависят от реализации; точные границы можно получить через numeric_limits<T> из <limits>.
Примеры констант для обозначения большого значения:
const int INF_INT = 2'000'000'000;
const long long INF_LL = 2'000'000'000'000'000'000LL;
INF выбирают по ограничениям задачи. Он должен быть больше допустимых ответов, а арифметика с ним не должна приводить к переполнению.
int a = 100000;
int b = 100000;
long long product = 1LL * a * b;
Одного long long у результата недостаточно: в записи long long product = a * b умножение сначала выполняется в int. 1LL переводит вычисление в long long до умножения.
set
set из <set> хранит уникальные элементы в отсортированном порядке.
set<int> st = {3, 1, 4}; // 1 3 4
st.insert(2); // 1 2 3 4
st.insert(2); // Ничего не изменится.
st.erase(3); // 1 2 4
Вставка, поиск и удаление по значению работают за O(log n).
Поиск и обход
if (st.count(2)) {
cout << "Found\n";
}
auto it = st.find(2);
if (it != st.end()) {
cout << *it << '\n';
}
for (auto x : st) {
cout << x << ' ';
}
count(x) возвращает 0 или 1, find(x) — итератор на элемент либо end().
Элементы при обходе идут по возрастанию. Отдельно сортировать set не нужно; std::sort к его итераторам неприменим.
Минимум и максимум
Для обычного порядка по возрастанию:
if (!st.empty()) {
cout << *st.begin() << '\n'; // Минимум.
cout << *st.rbegin() << '\n'; // Максимум.
}
Сначала проверьте, что контейнер непустой. Получение минимума и максимума занимает O(1).
lower_bound и upper_bound
auto it1 = st.lower_bound(2);
auto it2 = st.upper_bound(2);
lower_bound(x)— первый элемент ≥ x.upper_bound(x)— первый элемент > x.- Если подходящего элемента нет, возвращается
end().
Для set используйте методы через точку. st.lower_bound(x) работает за O(log n). Свободная функция lower_bound(st.begin(), st.end(), x) делает O(log n) сравнений, но до O(n) перемещений итератора, поэтому работает за O(n) времени.
Поиск предыдущего элемента
auto it = st.lower_bound(x);
if (it != st.begin()) {
--it;
cout << *it << '\n'; // Наибольший элемент, строго меньший x.
}
Если lower_bound вернул end(), уменьшить его можно только у непустого контейнера. Проверка it != st.begin() учитывает и пустое множество.
Свой компаратор
auto cmp = [](int a, int b) {
return a > b;
};
set<int, decltype(cmp)> st(cmp);
st.insert(1);
st.insert(3);
st.insert(2);
// При обходе: 3 2 1.
decltype(cmp) обозначает тип лямбды. Сам объект cmp передаётся конструктору множества.
begin(), lower_bound и upper_bound учитывают выбранный компаратор. При порядке по убыванию begin() указывает на максимум; привычные формулировки «первый ≥ x» и «первый > x» для границ уже не подходят.
Уникальность тоже определяется компаратором: если cmp(a, b) и cmp(b, a) оба равны false, элементы считаются одинаковыми. Если разные объекты могут иметь одинаковый приоритет, добавьте в ключ их номер, например pair<приоритет, номер>.
Удаление при обходе
auto it = st.begin();
while (it != st.end()) {
if (*it % 2 == 0) {
it = st.erase(it);
} else {
++it;
}
}
erase(it) возвращает итератор на следующий элемент. После удаления нельзя увеличивать или разыменовывать старый итератор.
multiset
multiset из <set> хранит отсортированные элементы и допускает повторения.
multiset<int> ms = {1, 1, 2};
ms.insert(1); // 1 1 1 2
Удаление одного или всех вхождений
auto it = ms.find(1);
if (it != ms.end()) {
ms.erase(it); // Удалили одну единицу: 1 1 2.
}
ms.erase(1); // Удалили все оставшиеся единицы: 2.
erase(x) удаляет все вхождения, erase(it) — только одно. Запись ms.erase(ms.find(x)) допустима, только если наличие x гарантировано: передавать end() в erase нельзя.
Ещё один вариант в C++17 — ms.extract(x): извлекает один элемент, если он есть, и возвращает узел с ним. Этот узел можно вставить в другой совместимый контейнер; если его не сохранить, извлечённый элемент будет уничтожен.
begin(), rbegin(), find, lower_bound, upper_bound используются так же, как у set.
count(x) в multiset работает за O(log n + k), где k — число вхождений x. Если нужно только проверить наличие, используйте find(x) != end().
map
map из <map> хранит пары ключ → значение. Ключи уникальны и отсортированы.
map<string, int> score;
score["Alice"] = 3;
score["Bob"] = 5;
Ключ и значение могут иметь разные типы. Например, map<string, string> хранит соответствие между словами, а map<string, vector<string>> — список строк для каждого ключа.
Обращение через []
Если ключа нет, map[key] создаёт его со значением по умолчанию. Для int это 0, для строки и вектора — пустое значение.
На этом удобно строить словарь частот:
vector<int> a = {2, 5, 2};
map<int, int> frequency;
for (int x : a) {
++frequency[x];
}
// frequency[2] = 2, frequency[5] = 1.
Для проверки наличия без вставки используйте find или count:
auto it = score.find("Carol");
if (it != score.end()) {
cout << it->second << '\n';
}
У итератора map поле first — ключ, second — значение. Запись it->second равнозначна (*it).second.
Обход
for (const auto& [name, points] : score) {
cout << name << ": " << points << '\n';
}
Элементы выводятся по возрастанию ключа. Запись [name, points] — структурированная привязка из C++17.
Чтобы изменять значения при обходе, используйте auto& [name, points]. Ключи всё равно останутся неизменяемыми.
Поиск границ и удаление
auto it = score.lower_bound("Bob");
if (it != score.end()) {
cout << it->first << ' ' << it->second << '\n';
}
score.erase("Alice");
lower_bound и upper_bound ищут по ключу и работают за O(log n). Удаление при обходе записывается так же, как для set: it = score.erase(it).
Ключи в set и map
Тип ключа должен поддерживать сравнение через < либо заданный компаратор.
Элементы set и ключи map нельзя менять на месте: это нарушило бы порядок внутри контейнера. Значения map менять можно.
Для изменения ключа удалите старую запись и вставьте новую:
set<pair<int, int>> tasks; // Приоритет, номер задачи.
tasks.insert({10, 7});
tasks.erase({10, 7});
tasks.insert({20, 7});
Не меняйте внешние данные, от которых зависит компаратор, пока соответствующие элементы находятся в set или map.
unordered_set и unordered_map
Эти контейнеры подключаются из <unordered_set> и <unordered_map>. Основные операции похожи на set и map, но порядок элементов не гарантируется.
unordered_set<int> st = {3, 1, 4};
unordered_map<string, int> frequency;
st.insert(2);
++frequency["apple"];
Внутри используется хеш-таблица: хеш-функция помогает определить, в какую ячейку поместить ключ. Если разные ключи попали в одну ячейку, возникает коллизия. Контейнер различает такие ключи проверкой равенства.
Поиск, вставка и удаление одного ключа занимают в среднем O(1), но в худшем случае — O(n). Средняя сложность не гарантирует быстрого выполнения на любых входных данных.
У unordered_set и unordered_map нет lower_bound и upper_bound. Для поиска соседей, минимума или упорядоченного вывода подходят set и map.
Сложность основных операций
Здесь n — число элементов, k — число вхождений искомого значения. Считаем сравнение и хеширование ключа операциями за O(1); для длинных строк их стоимость нужно учитывать отдельно.
| Операция | set / map |
multiset |
unordered_set / unordered_map |
|---|---|---|---|
Поиск через find |
O(log n) | O(log n) | В среднем O(1), в худшем O(n) |
| Вставка одного элемента | O(log n) | O(log n) | В среднем O(1), в худшем O(n) |
| Удаление по ключу | O(log n) | O(log n + k) | В среднем O(1), в худшем O(n) |
count |
O(log n) | O(log n + k) | В среднем O(1), в худшем O(n) |
Метод lower_bound / upper_bound |
O(log n) | O(log n) | Нет |
| Полный обход | O(n) | O(n) | O(n) |
Отладка STL-кода
Для локальной отладки с libstdc++ можно включить дополнительные проверки контейнеров и итераторов:
#define _GLIBCXX_DEBUG
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {10, 20, 30};
cout << v[3] << '\n'; // Намеренная ошибка для демонстрации.
}
В _GLIBCXX_DEBUG одно подчёркивание в начале. Директива должна стоять перед подключением заголовков стандартной библиотеки.
Можно включить тот же режим флагом компилятора:
g++ -std=c++17 -D_GLIBCXX_DEBUG main.cpp -o main
В этом примере проверка обнаружит выход за границы vector. В сообщении об ошибке найдите операцию, индекс и размер контейнера.
Режим также помогает находить разыменование end() и использование недействительных итераторов. Он замедляет программу: используйте его при локальной отладке.
Макрос относится к реализации стандартной библиотеки libstdc++, обычно используемой с g++. Он также работает с Clang, если тот использует libstdc++; для MSVC и libc++ нужны их собственные средства отладки.