Стеки, деки, очереди - 2
Оглавление
Запись занятия
Минимальная строка
На день рождения Пете подарили строку s длиной до 105 символов. Он взял еще две пустые строки t и u и решил сыграть в игру. По правилам в игре допускается два варианта ходов:
1. Изъять символ из начала строки s и приписать его в конец строки t.
2. Изъять символ из конца t и приписать его в конец строки u.
В результате Петя хочет, чтобы строка u была лексикографически минимальна, а s и t — пусты.
Решение
Решение за O(n)
Жадный алгоритм: каждый раз ищем лексикографически минимальный возможный символ, который мы можем написать в строку u. Этот символ может быть либо последним символом из строки t, либо любым из строки s. А далее лишь дело реализации: как поддерживать лексикографически минимальный символ в строке s? Над этим советую подумать, прежде чем смотреть в раздел c кодом.
Код
Код
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <deque>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
int cnt[26];
int main() {
string s;
cin >> s;
string t;
for (char c : s)
cnt[c - 'a']++;
int n = s.size();
reverse(s.begin(), s.end());
for (int i = 0; i < n; ++i) {
int lexmin = 27;
for (int i = 0; i < 26; ++i)
if (cnt[i])
lexmin = min(lexmin, i);
if (!t.empty() && lexmin >= t.back() - 'a') {
cout << t.back();
t.pop_back();
}
else {
while (s.back() - 'a' != lexmin) {
cnt[s.back() - 'a']--;
t.push_back(s.back());
s.pop_back();
}
cnt[s.back() - 'a']--;
s.pop_back();
cout << (char)(lexmin + 'a');
}
}
}
Гистограмма
Задача
Во дворе есть забор, состоящий из секций со стороной 1 метр по горизонтали и h[i] метров по вертикали. Костя хочет выбрать одну прямоугольную область на заборе и покрасить ее в красный цвет. Помогите Косте найти максимальную площадь, которую он сможет закрасить, чтобы он рассчитал необходимое количество краски.
Решение
Решение за O(n)
Заметим, что нижняя граница наибольшего прямоугольника - это всегда y=0. Наибольший прямоугольник должно быть невозможно расширить ни в одну из сторон. Давайте просто переберем все такие прямоугольники и выберем из них максимальный по площади.
Давайте идти слева направо по вертикальным прямоугольникам гистограммы и хранить такую “лесенку” - в стеке будут лежать пройденные столбики (индекс и высота), но только те, которые нужны, чтобы столбцы строго возрастали, и при этом лесенка заканчивалась последним рассмотренным столбцом.
Строить её нужно так: давайте рассмотрим новый столбец. Если его высота больше, чем у последнего в стеке (предыдущего столбца), то просто кладём его в стек, и на этом всё. Если его высота меньше или равна, чем у последнего, то нужно вынимать столбцы с конца стека, пока высота нового столбца не будет наконец больше, чем у последнего в стеке. В конце нужно просто вынуть все столбцы из стека (для этого удобно просто в конец положить фиктивный столбец высоты ноль).
При вынимании столбца из стека нужно посчитать площадь максимального прямоугольника, который включает этот столбец. Высоту мы уже знаем, надо определить его площадь. Заметим, что его левая координата - это индекс столбца, который лежит перед этим столбцом в стеке (это самый правый столбец, который левее удаляемого и при этом ниже по высоте) плюс один. А правая координата - это та, которую мы сейчас рассматриваем (раз нам нужно удалить этот столбец).
Как это работает? Наибольший прямоугольник упирается верхом хотя бы в один столбец, а значит когда мы его будем удалять, мы учтем этот прямоугольник.
Так можно за O(N) найти площадь максимального прямоугольника в такой гистограмме
Площадь максимального белого прямоугольника
Задача
Дан прямоугольник размера n x m, состоящий из 0 и 1. Необходимо найти максимальную площадь прямоугольника, состоящего только из единиц.
Решение
Решение за O(nm)
После осознания задачи о гистограмме решение задачи должно быть очевидным. Для каждой строки вычислим h[i,j] - количество подряд идущих единиц вверх от клетки i,j. Это можно вычислить за O(nm) проходом сверху вниз по массиву. А далее для каждой строки независимо решать задачу о гистограмме. Такое решение работает за O(nm).
K-рюкзак
Задача
Дано n типов предметов, i-й из них имеет стоимость ci, вес wi и таких предметов всего ai штук. Вам необходимо набрать максимальный по стоимости рюкзак, выдерживающий предметы весом не больше W.
а) O(nW^2)
b) O(nW)
Решение
Пункт (a)
Здесь мы можем написать абсолютно стандартный рюкзак, а именно: пусть dp[i][j] - это максимальный по стоимости рюкзак, в котором мы использовали предметы первых i типов и при этом набрали вес j. Переходы тоже будут очень простые: мы можем либо не брать i-й предмет (в dp[i-1][j]), либо взять 1 раз предмет i (в dp[i-1][j - w[i]]), либо 2 раза предмет i (в dp[i-1][j - 2w[i]]) и так далее до a[i] раз взять предмет i (в dp[i][j - a[i]w[i]]). То есть dp[i][j] = max(dp[i-1][j-kw[i]] + c[i] * k), где k=0..a[i]
Не сложно заметить, что если w[i] = 1, а a[i] - очень большое, то мы сделаем переходы из каждой клетки i-й строки в каждую клетку (i-1)-й строки, то есть такую динамику мы посчитаем за O(nW^2).
Пункт (b)
А теперь время оптимизаций. Нам потребуется пара идей:
Идея 1
Идея 1: заметим, что для фиксированного w[i] и j мы будем смотреть только на часть клеток k, имеющих остаток от деления k % w[i] = j % w[i]. То есть задача для всех остатков от деления на w[i] независима! Тогда выпишем по-отдельности клетки
0, w[i], 2w[i], …
1, w[i] + 1, 2w[i] + 1, …
…
w[i] - 1, 2w[i] - 1, 3w[i] - 1, …
И решим задачу для них отдельно. Собственно, какую задачу осталось решить то?
Идея 2
В каждом отдельном массиве нам нужно найти максимум среди a[i] предыдущих динамик с некоторой добавкой (вида k*c[i]). То есть мы решаем задачу максимума в окне размера a[i] с небольшим изменением: при движении окна значения в окне меняются нехитрым образом. Исследуем то, как оно меняется:
Пусть мы считаем значения динамики для первого из w[i] независимых подзадач и оно лежат в массиве t. Тогда переходы для j-го элемента имеют вид: <p align="center"> t[j], t[j-1] + c[i], …, t[j-a[i]] + a[i] * c[i] </p>
Переходы для (j+1)-го элемента будут очень похожи: <p align="center">t[j+1], t[j] + c[i], …, t[j-a[i] + 1] + a[i] * c[i] </p>
То есть разница для каждого перехода составляет ровно c[i]. Это очень важный факт! Он означает, что отсортированный порядок массива t с нашими добавлениями не меняется при переходе от j к j+1! А это в свою очередь означает, что мы можем писать обычный алгоритм для поиска максимума в окне, ведь нам важно было поддерживать убывающую последовательность элементов, и от j к j+1 основная ее часть не будет меняться (кроме некоторого суффикса и иногда первого элемента, как и в обычном алгоритме).
Устройство стека и очереди
В прошлый раз, когда мы обсуждали стек и очередь, мы абсолютно не задумывались, как они устроены. Пришло время поговорить о том, как же это реализовано так, чтобы все операции занимали О(1) (иначе говоря, константу) времени.
Стек
В чём вообще проблема? Предположим, стек использует обычный массив. Тогда на каждое добавление нового элемента нам придется увеличивать массив на один элемент. К сожалению, такой операции над массивами не существует. Поэтому чтобы еёё реализовать, нам придется:
- Создать новый массив размера n+1, это занимает О(1) по времени (да, создание массива любого размера с помощью оператора new занимает константу времени в C++)
- Скопировать n элементов из строго массива в новый, это занимает О(n)
Таким образом, если мы добавим в стек последовательно n элементов, то это займет во времени примерно 1 + 2 + 3 + … + n = О(n2) операций. Беда.
Как обычно в алгоритмах, нужно постараться найти “узкое место”. В нашем случае - это копирование n элементов из одного массива в другой каждый раз. Значит это место нужно исправить.
Вместо того, чтобы на каждый push создавать новый массив размера n+1, будем создавать массива размера 2n, скопировав всего n элементов. Это позволит следующие n пушей не копировать старый массив. Оценим временную сложность: пусть мы добавили n элементов в стек, тогда мы потратили 1 + 2 + 0 + 4 + 0 + 0 + 0 + 8 + 0 + 0 + … ≤ 2n = О(n) действий.
И ещё небольшое замечание, при действии pop мы не будем менять размер массива. Вместо этого будем поддерживать указатель на первый свободный элемент в стеке. Таким образом, любой pop занимает О(1) времени.
Очередь
Очередь, похожая на стек
Можно реализовать очередь абсолютно аналогично предыдущему пункту: тоже увеличивать её размер в два раза. При этом нужно поддерживать два указателя: левый указатель будет поддерживать последний элемент в очереди и двигаться при операции pop. Правый указатель - первый свободный элемент в массиве и двигаться при операции push.
Замечание: при копировании массива стоит копировать, все элементы, начиная с левого указателя и заканчивая правым.
Кольцевая очередь
Иногда задача на очередь весьма специфичная, а именно: мы заранее знаем, что в очереди в любой момент времени будет не больше k элементов. Тогда удобнее всего реализовать очередь на кольцевом массиве:
Создадим массив размера k+1. Всё так же будем поддерживать два указателя: на первый элемент очереди и первый свободный элемент в массиве. Однако, создавать новые массивы нам не придётся. Как только любой из указателей дойдёт до конца массива, его нужно передвинуть в начало, “по кольцу”. Таким образом реализация становится очень простой и удобной.