Skip to content

⚙️ Алгоритми в STL

Стандартната библиотека от шаблони (STL) е една от най-мощните функционалности на C++, предоставяща обширна колекция от алгоритми, които всеки състезател по програмиране трябва да овладее. Хедърът <algorithm> съдържа над 100 добре тествани, високо оптимизирани функции за работа с контейнери, правещи вашия код по-чист, по-бърз и по-малко податлив на грешки.

Разбирането и ефективното използване на STL алгоритмите е от решаващо значение за състезателното програмиране, защото: - Производителност: STL алгоритмите са оптимизирани от експерти и често превъзхождат ръчно написания код - Надеждност: Те са основно тествани и по-малко податливи на грешки - Четимост: Кодът, използващ STL алгоритми, е по-изразителен и по-лесен за разбиране - Спестяване на време: Писането на по-малко код означава повече време за решаване на проблеми

🔢 Алгоритми за сортиране и търсене

Функции за сортиране

std::sort(begin, end, [comparator])

Основният алгоритъм за сортиране със сложност \(O(N \log N)\) и обикновено използва оптимизирана имплементация на quicksort, heapsort или introsort.

#include <algorithm>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {5, 2, 8, 1, 9};

    // Сортиране във възходящ ред
    sort(v.begin(), v.end());
    // Резултат: {1, 2, 5, 8, 9}

    // Сортиране в низходящ ред с компаратор greater
    sort(v.begin(), v.end(), greater<int>());
    // Резултат: {9, 8, 5, 2, 1}

    // Персонализиран компаратор за двойки - сортиране по втори елемент
    vector<pair<int, int>> pairs = {{1, 5}, {2, 3}, {3, 7}};
    sort(pairs.begin(), pairs.end(), 
         [](const pair<int,int>& a, const pair<int,int>& b) {
             return a.second < b.second;
         });
    // Резултат: {{2,3}, {1,5}, {3,7}}

    return 0;
}

Важни съображения: - Не е стабилна (относителният ред на равните елементи не е гарантиран) - Работи с всеки контейнер с произволен достъп (vector, array, deque) - Изисква елементите да са сравними с оператор < или персонализиран компаратор

std::stable_sort(begin, end, [comparator])

Подобна на sort(), но гарантира стабилност - елементите, които са равни, запазват относителния си ред.

struct Student {
    string name;
    int grade;
};

vector<Student> students = {
    {"Alice", 85}, {"Bob", 90}, {"Charlie", 85}, {"David", 90}
};

// Първо сортиране по име
sort(students.begin(), students.end(), 
     [](const Student& a, const Student& b) { return a.name < b.name; });
// Alice, Bob, Charlie, David

// След това stable_sort по оценка - студенти с еднаква оценка запазват реда по име
stable_sort(students.begin(), students.end(),
            [](const Student& a, const Student& b) { return a.grade < b.grade; });
// Резултат: Alice(85), Charlie(85), Bob(90), David(90)
// Забележете Alice преди Charlie, Bob преди David

Времева сложност: \(O(N \log N)\) но може да използва повече памет от обикновеното sort.

std::partial_sort(begin, middle, end)

Когато ви трябват само най-малките (или най-големите) K елемента сортирани:

vector<int> v = {9, 5, 7, 1, 3, 8, 2, 6, 4};

// Вземи 3-те най-малки елемента в сортиран ред
partial_sort(v.begin(), v.begin() + 3, v.end());
// Първите 3 елемента: {1, 2, 3}, останалите: неопределен ред

// Времева сложност: O(N log K) където K = 3

Приложение: Намирането на топ K елементи е по-бързо от сортирането на всичко.

std::nth_element(begin, nth, end)

Частично сортира така че елементът на позиция nth да е на сортираната си позиция, всички елементи преди него да са по-малки или равни, а всички след него - по-големи или равни.

vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

// Намери медиана
nth_element(v.begin(), v.begin() + v.size()/2, v.end());
int median = v[v.size()/2];

// Времева сложност: O(N) средно - много по-бързо от пълно сортиране!

Приложения: - Намиране на медиана в \(O(N)\) - Намиране на k-ти най-малък елемент - Разделяне на данни около пивот

Функции за двоично търсене

Всички функции за двоично търсене изискват диапазонът да е сортиран първо!

std::binary_search(begin, end, value)

Връща true ако стойността съществува в диапазона, false в противен случай.

vector<int> v = {1, 2, 3, 5, 8, 13, 21};

bool exists = binary_search(v.begin(), v.end(), 5);    // true
bool missing = binary_search(v.begin(), v.end(), 7);   // false

// Времева сложност: O(log N)

Ограничение: Само казва дали елементът съществува, но не и позицията му.

std::lower_bound(begin, end, value)

Връща итератор към първия елемент, който НЕ Е по-малък от стойността (т.е. \(\ge\) value).

vector<int> v = {1, 2, 2, 2, 5, 8, 8, 13};

auto it = lower_bound(v.begin(), v.end(), 2);
// Сочи към първата 2 на индекс 1

int index = it - v.begin();  // Вземи индекс: 1

auto it2 = lower_bound(v.begin(), v.end(), 7);
// Сочи към 8 (първи елемент >= 7)

Често използвани случаи:

// Брой срещания на стойност в сортиран масив
auto lb = lower_bound(v.begin(), v.end(), value);
auto ub = upper_bound(v.begin(), v.end(), value);
int count = ub - lb;

// Вмъкни стойност, запазвайки сортирания ред
v.insert(lower_bound(v.begin(), v.end(), value), value);

// Провери дали стойността съществува и вземи позиция
auto it = lower_bound(v.begin(), v.end(), value);
if (it != v.end() && *it == value) {
    // Намерена на позиция (it - v.begin())
}

std::upper_bound(begin, end, value)

Връща итератор към първия елемент, който е по-голям от стойността (т.е. \(>\) value).

vector<int> v = {1, 2, 2, 2, 5, 8, 8, 13};

auto it = upper_bound(v.begin(), v.end(), 2);
// Сочи към 5 (първи елемент > 2)

// Намери диапазон на равни елементи
auto lb = lower_bound(v.begin(), v.end(), 2);
auto ub = upper_bound(v.begin(), v.end(), 2);
// Диапазонът [lb, ub) съдържа всички 2-ки

std::equal_range(begin, end, value)

Връща двойка итератори [lower_bound, upper_bound) за дадената стойност.

vector<int> v = {1, 2, 2, 2, 5, 8};

auto range = equal_range(v.begin(), v.end(), 2);
// range.first = lower_bound, range.second = upper_bound
int count = range.second - range.first;  // 3 срещания

🛠️ Алгоритми за манипулация на данни

Модифициращи последователности

std::reverse(begin, end)

Обръща реда на елементите в диапазона.

vector<int> v = {1, 2, 3, 4, 5};
reverse(v.begin(), v.end());
// Резултат: {5, 4, 3, 2, 1}

string s = "hello";
reverse(s.begin(), s.end());
// Резултат: "olleh"

// Времева сложност: O(N)

std::rotate(begin, middle, end)

Ротира елементите така че middle да стане първи елемент.

vector<int> v = {1, 2, 3, 4, 5};
rotate(v.begin(), v.begin() + 2, v.end());
// Резултат: {3, 4, 5, 1, 2}

// Полезно за циклични измествания
// Ляво изместване с k позиции:
rotate(v.begin(), v.begin() + k, v.end());

// Дясно изместване с k позиции:
rotate(v.rbegin(), v.rbegin() + k, v.rend());

std::unique(begin, end)

Премахва последователни дублиращи се елементи. Връща итератор към нов край.

Критично: Трябва първо да сортирате, за да премахнете ВСИЧКИ дубликати!

vector<int> v = {1, 2, 2, 3, 2, 4, 4, 5};

// ГРЕШНО: Без сортиране
auto new_end = unique(v.begin(), v.end());
// Премахва само последователни дубликати: {1, 2, 3, 2, 4, 5}

// ПРАВИЛНО: Със сортиране
sort(v.begin(), v.end());
// {1, 2, 2, 2, 3, 4, 4, 5}
new_end = unique(v.begin(), v.end());
v.erase(new_end, v.end());  // Действително премахни елементите
// Резултат: {1, 2, 3, 4, 5}

// Идиома на един ред:
v.erase(unique(v.begin(), v.end()), v.end());

std::fill(begin, end, value) и std::fill_n(begin, count, value)

Присвоява стойност на всички елементи в диапазона.

vector<int> v(5);
fill(v.begin(), v.end(), 42);
// Резултат: {42, 42, 42, 42, 42}

int arr[10];
fill_n(arr, 10, -1);
// Всички елементи зададени на -1

std::copy(begin, end, dest_begin)

Копира елементи от един диапазон в друг.

vector<int> src = {1, 2, 3, 4, 5};
vector<int> dest(5);

copy(src.begin(), src.end(), dest.begin());
// dest = {1, 2, 3, 4, 5}

// Копирай на изход
copy(src.begin(), src.end(), ostream_iterator<int>(cout, " "));
// Отпечатва: 1 2 3 4 5

Алгоритми за пермутации

std::next_permutation(begin, end)

Трансформира диапазона в следващата лексикографски по-голяма пермутация. Връща true ако е успешно, false ако е вече на последната пермутация.

vector<int> v = {1, 2, 3};

do {
    for(int x : v) cout << x << " ";
    cout << "\n";
} while(next_permutation(v.begin(), v.end()));

// Изход:
// 1 2 3
// 1 3 2
// 2 1 3
// 2 3 1
// 3 1 2
// 3 2 1

Важно за състезателно програмиране:

// Генерирай всички пермутации на низ
string s = "ABC";
sort(s.begin(), s.end());  // Трябва да започнете сортирани!
do {
    cout << s << "\n";
} while(next_permutation(s.begin(), s.end()));

// Брой пермутации до изпълнение на условие
int count = 0;
do {
    count++;
    // Провери някакво условие...
} while(next_permutation(v.begin(), v.end()));

std::prev_permutation(begin, end)

Подобна на next_permutation, но генерира предишна пермутация.

vector<int> v = {3, 2, 1};  // Започни от най-голямата

do {
    for(int x : v) cout << x << " ";
    cout << "\n";
} while(prev_permutation(v.begin(), v.end()));

📊 Математически и числови алгоритми

Намиране на екстремуми

std::max_element(begin, end) / std::min_element(begin, end)

Връща итератор към максималния/минималния елемент.

vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

auto max_it = max_element(v.begin(), v.end());
cout << "Максимум: " << *max_it << "\n";           // 9
cout << "Позиция: " << (max_it - v.begin()) << "\n";  // 5

auto min_it = min_element(v.begin(), v.end());
cout << "Минимум: " << *min_it << "\n";           // 1

// Персонализиран компаратор за сложни типове
struct Point { int x, y; };
vector<Point> points = {{1, 2}, {3, 1}, {2, 5}};

auto furthest = max_element(points.begin(), points.end(),
    [](const Point& a, const Point& b) {
        return (a.x*a.x + a.y*a.y) < (b.x*b.x + b.y*b.y);
    });

std::minmax_element(begin, end)

Връща и минимум и максимум в един проход (по-ефективно).

vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

auto [min_it, max_it] = minmax_element(v.begin(), v.end());
cout << "Мин: " << *min_it << ", Макс: " << *max_it << "\n";
// Мин: 1, Макс: 9

Числови операции (от <numeric>)

std::accumulate(begin, end, init, [binary_op])

Изчислява сума или персонализирана операция на свиване.

#include <numeric>

vector<int> v = {1, 2, 3, 4, 5};

// Сумирай всички елементи
int sum = accumulate(v.begin(), v.end(), 0);
// Резултат: 15

// Произведение на всички елементи
int product = accumulate(v.begin(), v.end(), 1, multiplies<int>());
// Резултат: 120

// Персонализирана операция - конкатенирай низове
vector<string> words = {"Hello", " ", "World"};
string sentence = accumulate(words.begin(), words.end(), string(""));
// Резултат: "Hello World"

// Сума на квадрати
int sum_of_squares = accumulate(v.begin(), v.end(), 0,
    [](int acc, int x) { return acc + x * x; });
// Резултат: 55

std::partial_sum(begin, end, dest_begin)

Изчислява префиксни суми (кумулативна сума).

vector<int> v = {1, 2, 3, 4, 5};
vector<int> prefix(5);

partial_sum(v.begin(), v.end(), prefix.begin());
// prefix = {1, 3, 6, 10, 15}

// Може и на място
partial_sum(v.begin(), v.end(), v.begin());

std::iota(begin, end, start_value)

Запълва диапазон с нарастващи стойности, започвайки от start_value.

vector<int> v(10);
iota(v.begin(), v.end(), 1);
// Резултат: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// Създай масив от индекси
vector<int> indices(n);
iota(indices.begin(), indices.end(), 0);
// indices = {0, 1, 2, ..., n-1}

🔍 Алгоритми за търсене и заявки

std::find(begin, end, value)

Линейно търсене за стойност. Връща итератор към първото срещане или end ако не е намерена.

vector<int> v = {1, 2, 3, 4, 5};

auto it = find(v.begin(), v.end(), 3);
if (it != v.end()) {
    cout << "Намерена на позиция: " << (it - v.begin()) << "\n";
}

// Времева сложност: O(N) - използвайте binary_search за сортирани масиви!

std::find_if(begin, end, predicate)

Намира първия елемент, удовлетворяващ предиката.

vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};

// Намери първо четно число
auto it = find_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// Сочи към 2

// Намери първо число > 5
auto it2 = find_if(v.begin(), v.end(), [](int x) { return x > 5; });
// Сочи към 6

std::count(begin, end, value) / std::count_if(begin, end, predicate)

Брои срещанията на стойност или елементи, удовлетворяващи предиката.

vector<int> v = {1, 2, 2, 3, 2, 4, 5};

int count = count(v.begin(), v.end(), 2);
// Резултат: 3

int even_count = count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// Резултат: 4 (елементи: 2, 2, 2, 4)

std::all_of, std::any_of, std::none_of

Проверява дали предикатът е истина за всички/някои/никой от елементите.

vector<int> v = {2, 4, 6, 8, 10};

bool all_even = all_of(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// true

bool has_negative = any_of(v.begin(), v.end(), [](int x) { return x < 0; });
// false

bool no_odds = none_of(v.begin(), v.end(), [](int x) { return x % 2 == 1; });
// true

🎯 Операции с множества (върху сортирани диапазони)

Всички операции с множества изискват сортирани входни диапазони!

std::set_union, std::set_intersection, std::set_difference

vector<int> a = {1, 2, 3, 4, 5};
vector<int> b = {3, 4, 5, 6, 7};
vector<int> result;

// Обединение: всички елементи от двете (без дубликати)
set_union(a.begin(), a.end(), b.begin(), b.end(), back_inserter(result));
// result = {1, 2, 3, 4, 5, 6, 7}

result.clear();

// Сечение: общи елементи
set_intersection(a.begin(), a.end(), b.begin(), b.end(), back_inserter(result));
// result = {3, 4, 5}

result.clear();

// Разлика: елементи в a, но не в b
set_difference(a.begin(), a.end(), b.begin(), b.end(), back_inserter(result));
// result = {1, 2}

🚀 Разширени алгоритми

std::partition(begin, end, predicate)

Пренарежда елементите така че тези, удовлетворяващи предиката, да дойдат първи. Връща итератор към първия елемент, неудовлетворяващ предиката.

vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};

auto pivot = partition(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// Възможен резултат: {2, 4, 6, 8, 1, 3, 5, 7}
// pivot сочи към 1

// Времева сложност: O(N)
// Не е стабилна - използвайте stable_partition за запазване на относителния ред

std::transform(begin, end, dest_begin, unary_op)

Прилага операция към всеки елемент и съхранява резултата.

vector<int> v = {1, 2, 3, 4, 5};
vector<int> squares(5);

transform(v.begin(), v.end(), squares.begin(), 
          [](int x) { return x * x; });
// squares = {1, 4, 9, 16, 25}

// Трансформация на място
transform(v.begin(), v.end(), v.begin(),
          [](int x) { return x * 2; });
// v = {2, 4, 6, 8, 10}

std::remove(begin, end, value) и std::remove_if(begin, end, predicate)

"Премахва" елементи като ги премества към края. Трябва да извикате erase() за действително изтриване!

vector<int> v = {1, 2, 3, 2, 4, 2, 5};

// Премахни всички 2-ки
auto new_end = remove(v.begin(), v.end(), 2);
v.erase(new_end, v.end());
// v = {1, 3, 4, 5}

// Идиома erase-remove (един ред):
v.erase(remove(v.begin(), v.end(), value), v.end());

// Премахни четни числа
v.erase(remove_if(v.begin(), v.end(), 
                  [](int x) { return x % 2 == 0; }), 
        v.end());

💡 Съвети за производителност и добри практики

1. Изберете правилния алгоритъм

// ЛОШО: O(N) линейно търсене в сортиран масив
vector<int> v = {1, 2, 3, ..., 1000000};  // сортиран
auto it = find(v.begin(), v.end(), target);  // O(N)

// ДОБРО: O(log N) двоично търсене
bool found = binary_search(v.begin(), v.end(), target);  // O(log N)

2. Избягвайте ненужни копия

// ЛОШО: Множество проходи
sort(v.begin(), v.end());
auto new_end = unique(v.begin(), v.end());
v.erase(new_end, v.end());

// ДОБРО: Комбинирани операции
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());

3. Използвайте подходящи контейнери

// Ако имате нужда от чести търсения: използвайте set/unordered_set вместо vector+sort
set<int> s;  // Автоматично сортиране, O(log N) търсене
unordered_set<int> us;  // O(1) средно търсене

4. Ламбда изрази за персонализирана логика

// Чисто и четимо
sort(students.begin(), students.end(),
     [](const Student& a, const Student& b) {
         return a.grade > b.grade;  // Сортирай по оценка низходящо
     });

🎓 Често срещани шаблони в състезателно програмиране

Шаблон 1: Сортиране + Двоично търсене

// Задача: Брой елементи в диапазон [L, R]
sort(v.begin(), v.end());
auto left = lower_bound(v.begin(), v.end(), L);
auto right = upper_bound(v.begin(), v.end(), R);
int count = right - left;

Шаблон 2: Сортиране + Уникални (Премахване на дубликати)

// Задача: Брой различни елементи
sort(v.begin(), v.end());
int distinct = unique(v.begin(), v.end()) - v.begin();

Шаблон 3: Сортиране + Два показалеца

// Задача: Намери двойка със сума равна на цел
sort(v.begin(), v.end());
int left = 0, right = n - 1;
while (left < right) {
    int sum = v[left] + v[right];
    if (sum == target) {
        // Намерена двойка
        break;
    } else if (sum < target) {
        left++;
    } else {
        right--;
    }
}

Шаблон 4: Префиксни суми

// Задача: Заявки за сума на диапазон
vector<int> prefix(n + 1, 0);
partial_sum(v.begin(), v.end(), prefix.begin() + 1);

// Сума на диапазон [L, R]
int range_sum = prefix[R + 1] - prefix[L];

Шаблон 5: Генериране и тестване на пермутации

// Задача: Намери пермутация, удовлетворяваща условие
sort(v.begin(), v.end());
do {
    if (check_condition(v)) {
        return true;
    }
} while(next_permutation(v.begin(), v.end()));

⚠️ Чести грешки

1. Забравяне на сортиране преди двоично търсене

vector<int> v = {5, 2, 8, 1, 9};
binary_search(v.begin(), v.end(), 5);  // ГРЕШНО! Недефинирано поведение!

sort(v.begin(), v.end());
binary_search(v.begin(), v.end(), 5);  // ПРАВИЛНО

2. Неизползване на erase след remove

remove(v.begin(), v.end(), value);  // Елементите все още са във вектора!
v.erase(remove(v.begin(), v.end(), value), v.end());  // ПРАВИЛНО

3. Модифициране на контейнер при итериране

// ГРЕШНО
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == value) v.erase(it);  // Итераторът е невалиден!
}

// ПРАВИЛНО
v.erase(remove(v.begin(), v.end(), value), v.end());

🏁 Заключение

STL алгоритмите са основен инструмент в арсенала на всеки състезател по програмиране. Ключови изводи:

  1. Не преоткривайте колелото: STL алгоритмите са тествани, оптимизирани и по-малко податливи на грешки от персонализирани имплементации
  2. Знайте сложностите си: Разбирането на временната сложност помага да изберете правилния алгоритъм
  3. Сортирайте първо: Много алгоритми изискват сортирани данни (двоично търсене, unique, операции с множества)
  4. Използвайте ламбди: Те правят кода по-четим и позволяват персонализирана логика
  5. Практикувайте често срещани шаблони: Сортиране+двоично търсене, идиома erase-remove, префиксни суми и др.

Овладейте тези алгоритми и ще пишете по-чист, по-бърз и по-надежден код в състезания. Спестеното време от избягване на грешки и имплементиране на оптимизирани решения може да бъде разликата между решаването на още една задача или не!

Бърза справочна таблица

Алгоритъм Времева сложност Изисква сортиране Приложение
sort \(O(N \log N)\) Не Общо сортиране
binary_search \(O(\log N)\) Да Проверка за съществуване
lower_bound \(O(\log N)\) Да Намиране на позиция
unique \(O(N)\) Да Премахване на дубликати
reverse \(O(N)\) Не Обръщане на реда
next_permutation \(O(N)\) Не Генериране на пермутации
max_element \(O(N)\) Не Намиране на максимум
accumulate \(O(N)\) Не Сума/свиване
partial_sum \(O(N)\) Не Префиксни суми

Не забравяйте: STL алгоритмите спестяват време, намаляват грешките и правят кода ви по-професионален!