Т-Банк

Алгоритмы.

STL

STL

Ввод и вывод

Ускорение стандартного ввода-вывода

В начале main, до первого ввода или вывода:

ios::sync_with_stdio(false);
cin.tie(nullptr);

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()

Разыменовывать 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 << ' ';
}

Пример ввода массива:

vector<int> a(n);
for (auto& x : a) {
    cin >> x;
}

Без & ввод изменит только локальную копию x, а элементы массива останутся прежними.

Когда итератор перестаёт быть действительным

Сортировка

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());

Даты сравниваются в порядке год → месяц → день, хотя в структуре поля записаны иначе.

Лимиты чисел

В обычной олимпиадной среде 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);

Для 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++ нужны их собственные средства отладки.