← Параллель CВсе параллели⬇ Скачать конспект

Занятие 4. Параллель C — Разбор контестов

Разбор задач трёх контестов: контейнеры (стек, очередь, дек), линейные алгоритмы (префиксные суммы, два указателя) и бинарный поиск (по ответу, вещественный, вложенный).
Разбор трёх контестов≈ 3 ч 15 минПсевдокод
Лекции из Т-поколения

1. Контест «Контейнеры» 0:00

Контейнеры — это структуры, которые хранят набор элементов и позволяют быстро добавлять и забирать их в определённом порядке: стек (кладём и забираем с одного конца), очередь (кладём в конец, забираем из начала), дек (добавляем и убираем с обоих концов). Почти все задачи контеста сводятся к тому, чтобы выбрать нужный контейнер и правильно хранить в нём вспомогательную информацию.

1.1. Задача D. Стек с минимумом 0:00

0:00 Условие. Нужно поддерживать стек чисел с тремя операциями: положить число, убрать верхнее, сказать минимум среди всех чисел стека.

Идея. Вместе с каждым числом храним минимум среди всех, кто лежит не выше него (то есть самого числа и всех ниже). Тогда ячейка стека — это пара (число, минимум до этой ячейки).

Пример. Приходят числа 1, 2, 1, 4. Ячейки: (1, 1), (2, 1), (1, 1), (4, 1) — в каждой второе число показывает, какой минимум у стека до этого места включительно. Убираем 4 — просто удаляем верхнюю пару целиком, и в вершине снова лежит (1, 1) с правильным минимумом. Если потом после опустошения стека положить 4, 2, 3, то получится (4, 4), (2, 2), (3, 2): минимум стека всегда читается с вершины.

Инвариант
Для каждой ячейки второе число равно минимуму всех чисел от дна стека до этой ячейки. При добавлении числа $x$ новый минимум равен $\min(x,\ \text{минимум вершины})$ (для пустого стека — просто $x$). При удалении вершины все оставшиеся ячейки не меняются, поэтому инвариант сохраняется сам собой.
псевдокод
структура СтекСМинимумом:
    ячейки — список пар (число, минимум)

    положить(x):
        если стек пуст: m = x
        иначе:          m = min(x, ячейки.последняя.минимум)
        ячейки.добавить_в_конец((x, m))

    убрать():
        ячейки.удалить_последнюю()          // пара удаляется целиком

    минимум():
        вернуть ячейки.последняя.минимум

Все операции работают за $O(1)$, память — $O(n)$. На записи отмечено, что стек можно реализовать и обычным динамическим массивом (добавление и удаление с конца), как выше: отдельная структура «стек» из библиотеки не обязательна.

Типичные ошибки
Хранить один «глобальный минимум» и обновлять его только при добавлении: после удаления минимального числа он перестанет быть верным. Вызывать getMin у пустого стека.

1.2. Задача E. Очередь с минимумом 3:00

6:00 Условие. Нужна очередь (добавляем в конец, убираем из начала) и ответ на вопрос «какой сейчас минимум?». У обычной std::queue минимум получить нельзя, поэтому очередь строится из двух стеков с минимумом.

Очередь на двух стеках. Заводим входной стек (в него кладём приходящих) и выходной (из него забираем уходящих). Когда нужно убрать первого из очереди:

Очередь = два стека; в каждой ячейке пара (число, минимум под ним)вход — сюда приходят(4, 4)(2, 2)(5, 2)выход — отсюда уходят(5, 5)(2, 2)(4, 2)если выход пуст:перекладываем всё сверху внизпервым пришёл — окажется на вершинеминимум очереди = min(минимум входа, минимум выхода)
Слева — стек тех, кто пришёл (4, затем 2, затем 5). Когда нужно удалить первого, а выходной стек пуст, всё перекладывается в выходной: вершиной становится 4 — самый ранний.

6:00 Минимум. Теперь делаем каждый из двух стеков стеком с минимумом из предыдущей задачи. Все элементы очереди лежат либо во входном, либо в выходном стеке, поэтому минимум очереди — это минимум из двух минимумов. На записи этот факт объяснён так: если множество разбито на две части, то минимум всего множества равен минимуму из минимумов частей (например, $\min(1,2,3,4,5)=\min(\min(1,2,3),\min(4,5))$).

псевдокод
структура ОчередьСМинимумом:
    вход, выход — два стека с минимумом

    добавить(x):
        вход.положить(x)

    убрать():
        если выход пуст:
            пока вход не пуст:                 // перекладываем всех
                выход.положить(вход.вершина.число)
                вход.убрать()
        выход.убрать()

    минимум():
        если вход пуст:  вернуть выход.минимум()
        если выход пуст: вернуть вход.минимум()
        вернуть min(вход.минимум(), выход.минимум())

Сложность. Каждый элемент за всё время перекладывается из входного стека в выходной не более одного раза, поэтому операции работают за $O(1)$ амортизированно (одно удаление может занять $O(n)$, но в среднем по всем операциям — константа).

Типичные ошибки
Перекладывать входной стек в выходной, когда выходной ещё не пуст (порядок очереди сломается). Брать минимум только из одного стека, забыв, что другой может быть пустым.

1.3. Задача F. Шарики 9:00

9:00 Условие. Дан ряд шариков разных цветов. Каждый раз, когда образуется группа из трёх и более шариков одного цвета подряд, вся группа лопается и исчезает, а оставшиеся части ряда склеиваются (соседи после склейки могут образовать новую группу). Нужно найти, сколько шариков останется. Важное условие: в начальный момент групп из трёх и более одноцветных шариков не больше одной.

Идея. Сначала сворачиваем ряд в группы подряд идущих одинаковых шариков — стек пар (цвет, сколько). Для ряда $1,2,2,3,3,3,3,2,1,1$ получается $(1,1),(2,2),(3,4),(2,1),(1,2)$. Сворачивание делается за один проход: если цвет совпал с вершиной стека, увеличиваем счётчик, иначе кладём новую пару.

Шарики сворачиваем в группы (цвет × длина)1 × 12 × 23 × 42 × 11 × 2лопаются группы, которые вместе с вершиной стека дают не меньше 3 шариков
Десять шариков превращаются в пять групп. Дальше достаточно обрабатывать группы стеком.

14:00 Дальше идём по группам слева направо и собираем второй стек — то, что осталось:

Разбор примера: $(1,1)$ и $(2,2)$ кладём; $(3,4)$ — лопает сама (лопнуло 4); следующая $(2,1)$ совпадает цветом с вершиной $(2,2)$ — получается три, лопаем (ещё 3) и снимаем вершину; последняя $(1,2)$ совпадает с вершиной $(1,1)$ — снова три, лопаем (ещё 3). Всего лопнуло 10, осталось 0.

псевдокод
функция оставшиесяШарики(a[1..n]):
    // 1. сворачиваем в группы (цвет, длина)
    g = пустой список
    для каждого x из a:
        если g не пуст и g.последняя.цвет == x: g.последняя.длина += 1
        иначе: g.добавить((x, 1))

    // 2. обрабатываем группы слева направо
    стек = пустой; лопнуло = 0
    для каждой группы (цвет, k) из g:
        если стек не пуст и стек.вершина.цвет == цвет:   // склеиваем с вершиной
            стек.вершина.длина += k
            k = стек.вершина.длина
            если k >= 3:
                лопнуло += k
                стек.убрать()
        иначе если k >= 3:
            лопнуло += k                                 // лопнула сама
        иначе:
            стек.положить((цвет, k))
    вернуть n - лопнуло

Почему так можно. Лопнувшая группа ничем не мешает дальнейшему: после неё соседи склеиваются, а склейка рассматривается, когда мы дойдём до следующей группы. Поскольку начальная группа из трёх и более ровно одна, каскады идут цепочкой от неё, и один проход слева направо со стеком их воспроизводит. Удалений из середины нет — всё происходит на вершине стека. Время и память — $O(n)$.

17:00 Вопросы из чата. Предложение «заменять лопнувшие на $-1$ и искать группы заново» не работает так просто: после замены соседние шарики не склеиваются, а нужно, чтобы цвета по обе стороны от образовавшейся «дыры» снова считались соседями. Решение «идти по массиву с вспомогательным вектором и проверять последние три элемента» тоже можно довести до рабочего, но нельзя удалять тройку сразу: если дальше идёт четвёртый такой же шарик, нужно удалить всю четвёрку целиком — поэтому и удобнее считать длины групп.

1.4. Задача G. Соседние буквы 23:00

23:00 Условие. Дана строка из латинских букв. Пара соседних в строке символов называется подходящей, если эти буквы соседние в алфавите (например, ab, ba, cd, yz; а ac — нет). Пока в строке есть подходящие пары, берётся самая левая из них, и из двух букв оставляется меньшая (большая стирается). Нужно получить итоговую строку.

Пример. В строке cbbcbb подходящие пары — cb и снова cb; самая левая — первые две буквы, остаётся b. Процесс продолжается, пока подходящих пар не останется; итог для этой строки — bbbb.

25:00 Идея. Так как всегда обрабатывается самая левая пара, строку можно читать слева направо со стеком уже обработанных букв: как только новая буква образует подходящую пару с вершиной стека, эту пару можно обработать немедленно. Откладывать нечего — это и есть самая левая пара.

Алгоритм. Для очередной буквы $c$ пока стек не пуст и вершина — соседняя с $c$ по алфавиту буква: снимаем вершину и заменяем $c$ на меньшую из двух. Затем кладём $c$ на стек. Цикл нужен потому, что после замены новая буква может образовать подходящую пару уже с буквой под вершиной (так и получается на разборе: замена $B$ на $A$ образует новую пару с тем, что лежало ниже).

31:39 Как сравнивать буквы. Тип char хранит числовой код символа (кодировка ASCII). Коды латинских букв нижнего регистра идут подряд: 'a' = 97, 'b' = 98, …, 'z' = 122; заглавные идут подряд тоже ('A' = 65 … 'Z' = 90), цифры — тоже. Поэтому буквы можно сравнивать как числа, а «соседние в алфавите» означает, что коды отличаются ровно на 1: abs(a - b) == 1. На записи лектор сначала назвал код буквы a неверно (по памяти), а затем исправил — правильно 97.

псевдокод
функция сжатьБуквы(s):
    стек = пустой
    для каждой буквы c из s:
        пока стек не пуст и |код(стек.вершина) - код(c)| == 1:
            c = min(стек.вершина, c)         // остаётся меньшая
            стек.убрать()
        стек.положить(c)
    вернуть стек как строку
Осторожно с регистром
Сравнивать на «больше–меньше» можно буквы одного регистра. Заглавные и строчные буквы в таблице кодов лежат отдельными блоками, и заглавная буква меньше любой строчной.

1.5. Задача H. Парикмахерская 35:00

35:00 Условие. Даны моменты прихода клиентов (по возрастанию) и для каждого — его «терпимость» (сколько людей перед собой он готов терпеть). Каждого клиента стригут ровно 20 минут. Клиент, придя, считает людей в парикмахерской, включая того, кого стригут сейчас; если их больше его терпимости, он сразу уходит. Если время окончания стрижки одного клиента совпадает со временем прихода другого, считается, что стрижка закончилась раньше. Нужно для каждого клиента вывести момент, когда он уйдёт.

Идея. Все времена переводим в минуты (например, $2{:}01$ — это $121$). Поддерживаем очередь тех, кто остался стричься, и храним в ней не момент прихода, а момент, когда клиент закончит стрижку. Для очередного клиента, пришедшего в момент $t$:

Парикмахерская: каждого стригут 20 минут; ушёл сразу, если людей перед ним больше терпимости6080100120140160клиент 1клиент 2клиент 4клиент 3 уходит сразуклиент 5 уходит сразуклиент 4 приходит в 122, ждёт клиента 2 и выйдет в 140 + 20 = 160
Моменты прихода (пунктир) и стрижка. Клиент 3 в 121 видит перед собой клиента 2 (терпимость 0) и уходит; клиент 4 готов ждать троих и выходит в 160.

Пример с записи: клиенты приходят в $60, 120, 121, 122, 123$ минут. Первый стрижётся до 80 и уходит в 80. Второй (терпимость 0) приходит, когда очередь пуста, и заканчивает в 140. Третий в 121 видит перед собой второго, а его терпимость 0 — уходит сразу, в 121. Четвёртый (терпимость 3) в 122 готов ждать и заканчивает в $140+20=160$ (а не в 142: сначала нужно дождаться второго). Пятый в 123 видит двоих при терпимости 0 и уходит сразу. Ответ: 80, 140, 121, 160, 123.

псевдокод
функция парикмахерская(t[1..n], терпимость[1..n]):
    очередь = пустая                         // моменты окончания стрижки
    для i от 1 до n:
        пока очередь не пуста и очередь.первый <= t[i]:
            очередь.убрать_первый()          // уже подстриглись и ушли
        если размер(очередь) > терпимость[i]:
            ответ[i] = t[i]                  // ушёл сразу
        иначе:
            если очередь пуста: начало = t[i]
            иначе:              начало = очередь.последний
            конец = начало + 20
            очередь.добавить_в_конец(конец)
            ответ[i] = конец
    вернуть ответ

Работает за $O(n)$: каждый клиент попадает в очередь и покидает её не более одного раза. Как пояснено на записи, терпимость 0 означает «не готов терпеть никого», а не «ждёт бесконечно»: клиент встанет в очередь только если перед ним в этот момент никого нет.

1.6. Задача I. Очередь из блоков одинаковых чисел 48:23

48:23 Условие. Есть две операции: «добавить в конец $c$ копий числа $x$» и «удалить из начала $c$ чисел и вывести их сумму». Числа $c$ могут достигать миллиардов.

Идея. Раскладывать миллиард копий по одной в очередь невозможно — не хватит ни времени, ни памяти. Поэтому храним очередь пар (сколько, какое число). Добавление — одна пара в конец. Удаление $c$ чисел: пока нужно удалять, смотрим первую пару $(k, x)$:

Пример. Добавили две двойки — пара $(2, 2)$. Удаляем одно число: сумма 2, остаётся $(1, 2)$. Добавили пять четвёрок: очередь $(1,2),(5,4)$. Удаляем три числа: первая пара уходит целиком (сумма 2, осталось удалить 2), из $(5,4)$ убираем две четвёрки (ещё 8, остаётся $(3,4)$) — ответ 10.

псевдокод
структура ОчередьБлоков:
    очередь пар (сколько, значение)

    добавить(c, x):
        очередь.добавить_в_конец((c, x))

    удалитьИВывестиСумму(c):
        сумма = 0
        пока c > 0:                          // в очереди заведомо достаточно чисел
            (k, x) = очередь.первая
            d = min(c, k)                    // сколько берём из первой пары
            сумма += d * x
            первая.k -= d;  c -= d
            если первая.k == 0: очередь.убрать_первую()
        вернуть сумма                        // сумма — в 64-битном типе

Сложность. Каждая пара добавляется один раз и целиком удаляется не более одного раза; кроме того, за один запрос удаления может остаться не более одной пары, из которой убрали лишь часть. Поэтому суммарное время — $O(q)$ по числу операций. Сумму храните в long long: произведения $c\cdot x$ легко выходят за int.

Вопрос из чата о том, как удалять, если в паре меньше чисел, чем нужно удалить: удаляем её целиком, уменьшаем оставшееся количество и берём следующую пару — именно для этого нужен цикл while. Хранить пары нужно именно в очереди (queue или deque), потому что добавляем в конец, а удаляем из начала.

1.7. Задача J. Железнодорожный тупик 56:00

56:00 Условие. Поезда заезжают по первому пути; их можно загнать в тупик (он работает как стек) и затем выпускать на второй путь. Напрямую с первого пути на второй проехать нельзя. Нужно, чтобы на втором пути вагоны выстроились по порядку $1,2,\dots,n$. Если это невозможно — вывести 0.

Идея. Поддерживаем число $\text{need}$ — какой вагон должен ехать на второй путь следующим (сначала 1). Каждый приехавший вагон приходится положить в тупик — другого выхода нет. После этого, пока на вершине тупика лежит именно нужный вагон, отправляем его на второй путь и увеличиваем $\text{need}$. Если на вершине лежит не он — выпускать нельзя (порядок нарушится), ждём следующих вагонов.

Прогон для $4,1,3,2$: 4 в тупик (ждём 1); приехал 1 — это нужный, уезжает; приехал 3 — в тупик (ждём 2); приехал 2 — нужный, уезжает; теперь на вершине 3 — нужный, уезжает; затем 4. Получили $1,2,3,4$. Для $3,2,1$ тоже получается сортировка. Для $2,3,1$: 2 и 3 ложатся в тупик, 1 уезжает, теперь нужна двойка, но на вершине лежит 3, а двойка под ней — и новых вагонов нет. Отсортировать нельзя — ответ 0.

псевдокод
функция можноОтсортировать(a[1..n]):
    стек = пустой
    нужен = 1
    для каждого x из a:
        стек.положить(x)
        пока стек не пуст и стек.вершина == нужен:
            стек.убрать()                    // вагон уезжает на второй путь
            нужен += 1
    вернуть (стек пуст)                      // иначе — вывести 0

Почему жадность верна. Если нужный вагон лежит на вершине, его выгодно отправить сразу: пока он лежит, любой следующий вагон ляжет сверху и закроет его, а убирать другие вагоны со второго пути нельзя. А если на вершине не нужный вагон, отправить его на второй путь — значит нарушить порядок, так что делать нечего. Если нужно вывести и сами операции, достаточно записывать «въезд в тупик» и «выезд на второй путь» в те же места кода.

1.8. Задача K. Невероятная очередь 1:04:00

1:05:21 Условие. У очереди три операции: человек уходит из начала; человек встаёт в конец; человек вклинивается в середину — встаёт сразу за центром очереди (при нечётном числе людей — сразу за центральным; например, в очереди $1,2,3,4,5$ новичок встаёт между 3 и 4, а в очереди $1,2,3,4$ — между 2 и 3).

Решение «в лоб» — хранить массив и вставлять в середину — слишком медленное: вставка в середину сдвигает половину элементов.

Идея. Храним две половины очереди в двух деках L (левая) и R (правая) с условием $|L| = |R|$ или $|L| = |R|+1$. Тогда «середина» — это граница между ними:

Две половины-дека: левая (|L| = |R| или |R|+1) и правая123L — левая половина45R — правая половинасередина6вклиниться = положить в НАЧАЛО правой половины (push_front), потом выровнять размерыуйти из начала — pop_front у L; прийти в конец — push_back у R
Очередь 1 2 3 | 4 5. Новый участник 6 встаёт сразу за серединой — это начало правой половины.
псевдокод
структура НевероятнаяОчередь:
    L, R — два дека; всегда |L| == |R| или |L| == |R| + 1

    выровнять():
        пока |L| < |R|:      L.добавить_в_конец(R.первый);  R.убрать_первый()
        пока |L| > |R| + 1:  R.добавить_в_начало(L.последний);  L.убрать_последний()

    встатьВКонец(x):    R.добавить_в_конец(x);   выровнять()
    вклиниться(x):      R.добавить_в_начало(x);  выровнять()      // сразу за серединой
    уйти():             x = L.первый;  L.убрать_первый();  выровнять();  вернуть x

Разбор примера с записи: очередь $1,2,3\mid 4,5$. Уходит 1 — из начала L, размеры $2$ и $2$ нормальны. Уходит 2 — остаётся $|L|=1$, $|R|=2$, поэтому 4 переезжает из начала R в конец L: $3,4\mid 5$. Вклинивается 6 — встаёт в начало правой половины: $3,4\mid 6,5$. Приходит 7 в конец — в конец правой: $3,4\mid 6,5,7$, размеры $2$ и $3$, поэтому 6 переезжает в левую: $3,4,6\mid 5,7$.

Каждая операция переносит не больше одного элемента между половинами, поэтому всё работает за $O(1)$ на операцию.

1.9. Задача L. Постфиксная запись и параллельные вычисления 1:13:00

1:16:45 Условие. Задано выражение в постфиксной записи: последовательность чисел и операций, где операция применяется к двум последним числам стека. Независимые операции можно выполнять одновременно — за один «шаг». Нужно найти значение выражения и минимальное число шагов.

Идея. Вместе с каждым числом в стеке храним, за сколько шагов оно вычисляется. Число из входа вычисляется за 0 шагов. Операция берёт два верхних числа с их шагами $a$ и $b$, и, так как слагаемые можно считать параллельно, результат готов через $\max(a,b)+1$ шагов.

Пример: $(1+2)\cdot(3+4)-6$ в постфиксной записи — 1 2 + 3 4 + * 6 -. Числа 1, 2, 3, 4 — по 0 шагов. $1+2=3$ за $\max(0,0)+1=1$ шаг; $3+4=7$ тоже за 1 шаг (параллельно с первым). Произведение $3\cdot7=21$ — за $\max(1,1)+1=2$ шага. Шестёрка — 0 шагов, вычитание $21-6=15$ — за $\max(2,0)+1=3$ шага. Ответ: значение 15, шагов 3. Если бы одно из слагаемых считалось за 3 шага, а другое за 1, то для следующей операции получилось бы $\max(3,1)+1=4$: короткое вычисление скрывается внутри длинного.

псевдокод
функция вычислить(токены):
    стек = пустой                            // пары (значение, шаги)
    для каждого t из токенов:
        если t — операция:
            (b, sb) = стек.снять()           // правый операнд лежит выше
            (a, sa) = стек.снять()
            v = применить(t, a, b)
            стек.положить((v, max(sa, sb) + 1))
        иначе:
            стек.положить((число(t), 0))     // число из входа — 0 шагов
    вернуть стек.вершина

1.10. Задача M. Жемчужины 1:20:03

1:20:03 Условие. Дан массив жемчужин $k$ цветов. Нужно разбить как можно больше жемчужин на непересекающиеся группы так, чтобы в каждой группе были жемчужины всех $k$ цветов (по одной каждого), причём любые две позиции в группе отличаются не больше чем на $m$.

Пример: $1,2,2,1,3,3$, $k=3$, $m=2$. Группа из позиций 0, 1, 4 запрещена: позиции 0 и 4 отстоят на 4. Допустима только группа из позиций 2, 3, 4 с цветами 2, 1, 3. Больше групп не набрать.

Жемчужины: k = 3 цвета, m = 2 (в группе разница позиций ≤ 2)1поз. 02поз. 12поз. 21поз. 33поз. 43поз. 5группа: позиции 2, 3, 4 — цвета 2, 1, 3для каждого цвета — своя очередь позиций; позиции, отстоящие больше чем на m от текущей, из очередей удаляемкогда все k очередей непусты — берём из каждой первую позицию и закрываем группу
В группу попадает 2, 1, 3 (позиции 2–4). Группа 1, 2, 3 из позиций 0, 1, 4 запрещена: позиции 0 и 4 отстоят на 4 > 2.

Идея. Для каждого цвета ведём свою очередь позиций жемчужин этого цвета (в порядке появления), плюс счётчик числа непустых очередей. Идём по массиву слева направо; на позиции $i$:

  1. позицию $i-m-1$ (если она ещё стоит в начале очереди своего цвета) удаляем: она отстоит от $i$ больше, чем на $m$, и ни в какую группу, содержащую $i$ или дальше, попасть уже не может;
  2. добавляем $i$ в очередь её цвета;
  3. если все $k$ очередей непусты — берём первую позицию из каждой, записываем группу и удаляем эти позиции из очередей.
псевдокод
функция жемчужины(a[0..n-1], k, m):          // цвета 1..k
    q[1..k] — k пустых очередей позиций
    непустых = 0;  групп = 0
    для i от 0 до n-1:
        p = i - m - 1                            // слишком далеко от i
        если p >= 0 и q[a[p]] не пуста и q[a[p]].первая == p:
            q[a[p]].убрать_первую()
            если q[a[p]] стала пуста: непустых -= 1
        если q[a[i]] пуста: непустых += 1
        q[a[i]].добавить_в_конец(i)
        если непустых == k:                      // можно закрыть группу
            групп += 1
            для каждого цвета c от 1 до k:
                q[c].убрать_первую()
                если q[c] стала пуста: непустых -= 1
    вернуть групп

Группа, которую мы закрываем, состоит из самых левых позиций каждого цвета, находящихся не дальше $m$ от текущей, поэтому размах группы не превышает $m$. Время работы — $O(n)$: каждая позиция один раз добавляется и один раз удаляется.

Важное дополнение
Почему выгодно закрывать группу сразу же, как только все очереди непусты, и брать самые левые позиции: более левые жемчужины раньше других выходят из допустимого окна и пропадают, поэтому использовать их в группе сейчас — самый «дешёвый» способ; оставлять их на потом — рисковать потерять. Это жадный выбор; на мелких случайных тестах он совпал с полным перебором.

2. Контест «Линейные алгоритмы» 1:29:00

Линейные алгоритмы работают за время, пропорциональное длине входа. Здесь два главных инструмента: префиксные суммы (и их обратная операция — разностный массив), а также два указателя, которые двигаются только в одну сторону.

2.1. Задачи A–D. Префиксные и разностные массивы 1:29:00

1:29:00 Первые задачи контеста были на лекции, поэтому на записи разбираются кратко.

A. Сумма на отрезке (RSQ). Префиксные суммы: $p_0=0$, $p_{i+1}=p_i+a_i$. Тогда сумма $a_l+\dots+a_r$ равна $p_{r+1}-p_l$ — ответ на запрос за $O(1)$ после предварительного подсчёта за $O(n)$.

B. Прибавление на отрезке (RUQ). Разностный массив $d$: прибавление $x$ ко всем элементам отрезка $[l,r]$ — это $d_l \mathrel{+}= x$ и $d_{r+1} \mathrel{-}= x$. После всех операций исходный массив восстанавливается префиксными суммами $d$. Каждая операция — $O(1)$.

C. Сумма на прямоугольнике (RSQ 2D). Двумерные префиксные суммы: $p[i][j]$ — сумма на прямоугольнике от угла $(1,1)$ до $(i,j)$:

$$p[i][j]=a[i][j]+p[i-1][j]+p[i][j-1]-p[i-1][j-1].$$

Сумма на прямоугольнике $(x_1,y_1)\ldots(x_2,y_2)$ вычисляется включением–исключением: $p[x_2][y_2]-p[x_1-1][y_2]-p[x_2][y_1-1]+p[x_1-1][y_1-1]$.

1:30:00 D. Сумма на параллелепипеде (RSQ 3D). В лекции эта часть не разбиралась, но идея та же: трёхмерные префиксные суммы и включение–исключение по восьми «углам». Подробная формула есть в шпаргалке параллели в разделе «Многомерные префиксные суммы».

псевдокод
// одномерные: p[0] = 0;  p[i+1] = p[i] + a[i];   сумма a[l..r] = p[r+1] - p[l]

// трёхмерные: p[x][y][z] — сумма на параллелепипеде от (1,1,1) до (x,y,z), p[0][*][*] = 0
для x, y, z по порядку возрастания:
    p[x][y][z] = a[x][y][z]
               + p[x-1][y][z] + p[x][y-1][z] + p[x][y][z-1]
               - p[x-1][y-1][z] - p[x-1][y][z-1] - p[x][y-1][z-1]
               + p[x-1][y-1][z-1]

функция сумма(x1,y1,z1, x2,y2,z2):               // границы включительно
    вернуть  p[x2][y2][z2]
           - p[x1-1][y2][z2] - p[x2][y1-1][z2] - p[x2][y2][z1-1]
           + p[x1-1][y1-1][z2] + p[x1-1][y2][z1-1] + p[x2][y1-1][z1-1]
           - p[x1-1][y1-1][z1-1]
Важное дополнение
Знаки в формуле параллелепипеда чередуются по числу «срезанных» измерений: ноль срезов — плюс (полный объём), один — минус, два — плюс, три — минус. Для суммы по параллелепипеду на 1D, 2D и 3D работает один и тот же принцип включения–исключения; формулы проверены случайными тестами против прямого суммирования.

2.2. Задача E. Отрезок с максимальной суммой 1:31:00

1:31:00 Условие. Найти подотрезок с максимальной суммой (числа могут быть отрицательными).

Идея. Сумма подотрезка $[l,r]$ равна $p_{r+1}-p_l$. Зафиксируем правую границу: чтобы сумма была максимальной, нужно вычесть наименьший из префиксов $p_0,\dots,p_r$. Поэтому идём по массиву слева направо, поддерживая текущую префиксную сумму и минимум из префиксных сумм, которые уже встречались (включая $p_0=0$).

псевдокод
функция максимальныйПодотрезок(a[1..n]):       // непустой
    pre = 0;  minPre = 0;  ответ = -∞
    для каждого x из a:
        pre += x                                 // p[r+1]
        ответ = max(ответ, pre - minPre)         // лучшая левая граница — с минимальным p[l]
        minPre = min(minPre, pre)                // p[r+1] станет левой границей для следующих
    вернуть ответ

Время $O(n)$, память $O(1)$. Порядок строк внутри цикла важен: сначала используем минимум, потом обновляем его, иначе получим пустой подотрезок. Если массив состоит только из отрицательных чисел, ответ — наибольший (наименее отрицательный) элемент, а не 0 — поэтому ans нельзя инициализировать нулём.

2.3. Задача F. Объединение квадратов и кубов 1:32:00

1:32:00 Условие. Есть последовательности квадратов $1,4,9,16,\dots$ и кубов $1,8,27,64,\dots$. Их сливают в одну неубывающую последовательность $1,1,4,8,9,16,25,27,\dots$. Нужно вывести элемент на позиции $x$, где $x$ до $10^7$.

Идея. Это та же операция, что слияние двух отсортированных массивов в сортировке слиянием, только массивы заданы формулами. Два указателя $i$ и $j$ (какой квадрат и какой куб рассматриваем), и $x$ раз выбираем меньшее из $i^2$ и $j^3$ и сдвигаем соответствующий указатель.

псевдокод
функция кМерджа(x):                            // 64-битные числа: i*i может переполнить int
    i = 1;  j = 1;  последнее = 0
    повторить x раз:
        если i*i <= j*j*j:  последнее = i*i;      i += 1
        иначе:              последнее = j*j*j;    j += 1
    вернуть последнее

1:36:00 Вопрос о повторяющихся числах. В таком виде число, которое является одновременно квадратом и кубом (1, 64, 729, …), входит в последовательность дважды. Если по условию совпадающие числа нужно учитывать один раз, добавляют отдельный случай: при $i^2=j^3$ берём число и сдвигаем оба указателя, а в остальных ветках сравнение делается строго:

псевдокод
если i*i == j*j*j:    последнее = i*i;   i += 1;  j += 1      // общее число берём один раз
иначе если i*i < j*j*j: последнее = i*i;   i += 1
иначе:                  последнее = j*j*j; j += 1

2.4. Задача G. Стильная одежда 1:37:00

1:37:00 Условие. Есть четыре вида одежды (например, шапка, куртка, брюки, обувь), у каждого — набор вариантов с числовыми характеристиками. Нужно выбрать по одному варианту каждого вида так, чтобы разность $\max-\min$ среди выбранных чисел была как можно меньше.

Пример: наборы $\{1,2,3\}$, $\{1,3\}$, $\{3,4\}$, $\{2,3\}$. Выгодно выбрать $3,3,3,3$ — разность 0.

Идея. Сортируем каждый набор. Ставим по указателю на начало каждого набора — получаем текущий выбор из четырёх чисел. Дальше как в задачах на два указателя нужно решить, какой указатель двигать. Рассуждение:

После каждого сдвига обновляем ответ $\max-\min$. Останавливаемся, когда указатель, которому надо шагнуть, выходит за границу своего набора: меньшего минимума в таком случае уже не получить.

псевдокод
функция стильнаяОдежда(g[1..4]):                // g[i] — варианты i-го вида
    отсортировать каждый g[i]
    p[i] = 0 для всех i                           // указатели
    ответ = ∞
    повторять:
        значения v[i] = g[i][p[i]]
        mn = минимум v;  mx = максимум v;  who = индекс минимума
        ответ = min(ответ, mx - mn)
        p[who] += 1
        если p[who] вышел за границу g[who]: прервать
    вернуть ответ

Время — $O(S\cdot k)$ для $S$ вариантов всего и $k=4$ видов (поиск минимума среди четырёх — константа). Для большого числа видов минимум удобно поддерживать в приоритетной очереди.

2.5. Задача H. Пары точек на большом расстоянии 1:42:00

1:42:00 Условие. Дан отсортированный массив координат точек и число $d$. Нужно посчитать количество пар точек, расстояние между которыми строго больше $d$ (до $3\cdot10^5$ точек).

Пример: точки $1,3,5,8$, $d=4$. Подходят пары $(1,8)$ с расстоянием 7 и $(3,8)$ с расстоянием 5; пара $(1,5)$ имеет расстояние ровно 4 и не подходит. Ответ 2.

Уточнение
На записи условие сначала произнесено как «больше либо равно», а при разборе примера лектор исправился: «здесь не больше либо равно, а строго больше». Пример (ответ 2) согласуется именно со строгим неравенством; если бы подходили пары с расстоянием ровно $d$, ответ был бы 3.

Идея. Для каждой левой точки $i$ ищем указатель $j$ — первую точку, которая находится от неё дальше чем на $d$. Все точки правее $j$ ещё дальше, поэтому они тоже подходят — это $n-j$ пар. При переходе к следующему $i$ точка $x_i$ сдвигается вправо, значит, указатель $j$ не нужно возвращать назад — он только растёт. Это и есть два указателя.

псевдокод
функция дальниеПары(x[1..n], d):                // x отсортирован
    j = 1;  ответ = 0                             // ответ — в 64-битном типе
    для i от 1 до n:
        пока j <= n и x[j] - x[i] <= d:
            j += 1                                // пропускаем ближние точки
        ответ += n - j + 1                        // все точки с индексом >= j подходят
    вернуть ответ

Время $O(n)$. Ответ нужно хранить в 64-битном типе: при $n=3\cdot10^5$ количество пар достигает десятков миллиардов.

2.6. Задача I. Префикс и суффикс с близкими суммами 1:49:37

1:49:37 Условие. Дан массив неотрицательных чисел. Нужно выбрать позиции $l<r$ так, чтобы модуль разности суммы префикса $a_1+\dots+a_l$ и суммы суффикса $a_r+\dots+a_n$ был минимальным. Для массива $1,2,3,4$: префикс $1+2=3$ и суффикс $4$ дают разность 1.

Идея. Два указателя навстречу друг другу: $l$ — конец префикса, $r$ — начало суффикса, и две суммы $A$ (сумма префикса) и $B$ (сумма суффикса). Изначально префикс — это только $a_1$, суффикс — только $a_n$. Дальше сдвигаем тот указатель, у которого сумма меньше: расширение меньшей суммы приближает её к большей, а расширение большей только увеличит разность. После каждого шага обновляем ответ $|A-B|$ и продолжаем, пока указатели не встретятся.

псевдокод
функция баланс(x[1..n]):                        // n >= 2, числа неотрицательны
    l = 1;  r = n
    A = x[1];  B = x[n];  ответ = |A - B|
    пока l + 1 < r:
        если A < B: l += 1;  A += x[l]            // расширяем меньшую сумму
        иначе:      r -= 1;  B += x[r]
        ответ = min(ответ, |A - B|)
    вернуть ответ
Условие применимости
Жадный выбор «двигать меньшую сумму» верен только для неотрицательных чисел: именно поэтому на записи отмечено, что отрицательных чисел нет. Если числа могут быть отрицательными, расширение суммы может её и уменьшить, и тогда двигать нужно иначе.

В условии оригинальной задачи просят вывести индексы $l$ и $r$, а в коде выше считается только значение. Как лектор оставил слушателю, нужно дополнительно запоминать $l$ и $r$ в момент, когда ответ улучшается. Время $O(n)$.

2.7. Задача J. Супероперации 1:58:04

1:58:04 Условие. Дан массив и список операций, каждая из которых прибавляет число $d$ к отрезку $[l,r]$. Затем приходят запросы-«супероперации»: «выполнить операции с номерами от $a$ до $b$». Нужно получить массив после выполнения всех супероопераций. Выполнять каждую супероперацию по всем её операциям слишком долго.

Идея в два этапа с разностными массивами.

  1. Заводим массив-счётчик $c$ по номерам операций: $c_i$ — сколько раз будет выполнена операция $i$. Каждая супероперация $(a,b)$ — это «прибавить 1 на отрезке $[a,b]$ в массиве $c$». Делаем это разностным массивом и восстанавливаем $c$ префиксными суммами.
  2. Операция $i$ с числом $d$, выполненная $c_i$ раз, прибавит суммарно $d\cdot c_i$ на том же отрезке. Применяем теперь все операции один раз, но с числом $d\cdot c_i$ — снова разностным массивом по элементам.

Пример с записи: операции $(1,2,1)$, $(1,3,2)$, $(2,3,4)$ и супероперации $(1,2)$, $(1,3)$, $(2,3)$. Счётчики: первая операция входит в две супероперации (2 раза), вторая — во все три (3 раза), третья — в две (2 раза). Эффективные добавки: $1\cdot2=2$, $2\cdot3=6$, $4\cdot2=8$. К элементам массива прибавится: на позиции 1 — $2+6=8$, на позиции 2 — $2+6+8=16$, на позиции 3 — $6+8=14$.

2:01:00 Вопрос зачем умножать. Операция «прибавить 4», вызванная дважды, добавляет $4\cdot2=8$ — это то же самое, что вызвать операцию «прибавить 8» один раз. Умножение на счётчик просто собирает все её вызовы в один.

псевдокод
// шаг 1: сколько раз выполнится каждая операция
cnt[1..m] = 0
для каждой супероперации (a, b):
    cnt[a] += 1;  cnt[b+1] -= 1                   // разностный массив по номерам операций
cnt[i] += cnt[i-1] для i = 2..m                   // восстанавливаем счётчики

// шаг 2: применяем операции с d * cnt[i]
diff[1..n+1] = 0
для i от 1 до m, операция (l, r, d):
    diff[l]   += d * cnt[i]
    diff[r+1] -= d * cnt[i]
cur = 0
для i от 1 до n:
    cur += diff[i];  a[i] += cur

Время $O(n+m+q)$, где $q$ — число супероопераций.

2.8. Задача K. Прибавление на прямоугольнике 2:04:55

2:04:55 Условие. Дана таблица $n\times m$ и много запросов вида «прибавить $d$ ко всем клеткам прямоугольника $(l_x,l_y)\ldots(r_x,r_y)$». Нужно вывести таблицу после всех запросов (важно: только итог, а не состояние после каждого запроса).

Идея. Это двумерный разностный массив. Сначала решение «каждая строка — свой разностный массив»: прибавление на прямоугольнике — это по $+d$ в начале каждой строки отрезка и $-d$ сразу после его конца, то есть порядка $n$ изменений на запрос. Чтобы сократить до константы, посмотрим на эти изменения по столбцам как на разностный массив по вертикали: столбец «$+d$ в строках $l_x..r_x$» — это всего два изменения. Итого на запрос меняем 4 клетки:

$$t[l_x][l_y]\mathrel{+}=d,\quad t[l_x][r_y+1]\mathrel{-}=d,\quad t[r_x+1][l_y]\mathrel{-}=d,\quad t[r_x+1][r_y+1]\mathrel{+}=d.$$
Прибавление на прямоугольнике: меняем только 4 клетки+d−d−d+dзатем два прохода:1) по столбцам сверху вниз:t[i][j] += t[i-1][j]2) по строкам слева направо:t[i][j] += t[i][j-1]
Синий — прямоугольник запроса. Ставим +d в левый верхний угол, −d справа и снизу от прямоугольника, +d в правый нижний угол. Два прохода восстанавливают настоящие значения.

После всех запросов восстанавливаем таблицу двумя проходами: сначала по столбцам сверху вниз (t[i][j] += t[i-1][j]), затем по строкам слева направо (t[i][j] += t[i][j-1]). Проходы делаются один раз в самом конце.

псевдокод
для каждого запроса (x1, y1, x2, y2, d):
    t[x1][y1]     += d
    t[x1][y2+1]   -= d
    t[x2+1][y1]   -= d
    t[x2+1][y2+1] += d

для каждого столбца j, сверху вниз:   t[i][j] += t[i-1][j]    // первый проход
для каждой строки i, слева направо:   t[i][j] += t[i][j-1]    // второй проход
// таблица t размера (n+2) x (m+2), индексы с 1

Общее время — $O(k+nm)$ для $k$ запросов: 4 операции на запрос и два прохода по таблице в конце (на записи это названо «$2nm$ плюс константа на запрос»). Таблицу заводим размера $(n+2)\times(m+2)$, чтобы индексы $r+1$ не выходили за границы.

3. Контест «Бинарный поиск» 2:13:00

Бинарный поиск применим не только к поиску числа в массиве, но и к поиску ответа: если есть проверка «подходит ли значение $x$?», которая для всех значений до некоторого порога даёт «да», а после — «нет» (или наоборот), то границу можно найти за $O(\log)$ проверок. Во всех задачах ниже используется один шаблон: $l$ — значение, которое точно подходит, $r$ — значение, которое точно не подходит, и пока $l+1\ne r$ проверяем середину.

псевдокод
// l — точно подходит, r — точно не подходит
пока l + 1 != r:
    m = (l + r) / 2
    если подходит(m): l = m  иначе: r = m     // если подходят большие значения, ветки меняются
// ответ — l (наибольшее подходящее)

3.1. Задачи A и B. Поиск числа и ближайшего числа 2:13:00

2:13:00 A. Проверить, есть ли число $x$ в отсортированном массиве, — обычный бинарный поиск (binary_search или lower_bound и проверка найденного элемента).

2:14:00 B. Ближайшее число. Найти в массиве число, ближайшее по модулю разности к заданному $x$. Находим lower_bound — позицию pos первого элемента, не меньшего $x$. Ближайшее число — либо a[pos], либо предыдущее a[pos-1]: предыдущее тоже может оказаться ближе. Например, для массива $2, 5$ и $x=3$ lower_bound указывает на 5, но ближайшее число — 2.

псевдокод
функция ближайшее(a[1..n], x):                 // a отсортирован
    pos = первая позиция, где a[pos] >= x         // lower_bound
    лучший = ничего
    если pos <= n:  кандидат a[pos]
    если pos > 1:   кандидат a[pos-1]              // предыдущее тоже может быть ближе
    вернуть кандидата с наименьшим |a - x|

2:14:11 Вопрос из чата: почему именно lower_bound, а не upper_bound? lower_bound — первый элемент, не меньший $x$ (больше или равный), upper_bound — первый строго больший. Если число $x$ есть в массиве, lower_bound попадает ровно на него, и расстояние равно 0 — это сразу правильный ответ.

Граничные случаи
Позиция pos может оказаться равной $n$ (все элементы меньше $x$) или 0 (все элементы не меньше $x$) — обе проверки в коде выше защищены условиями на границы.

3.2. Задача C. Копии на двух принтерах 2:15:00

2:15:00 Условие. Нужно сделать $n$ копий документа. Первый аппарат делает копию за $x$ секунд, второй — за $y$. Ловушка: в начале есть только оригинал, поэтому первую копию нужно сделать на более быстром аппарате (он освободится за $m=\min(x,y)$ секунд); после этого копировать можно на обоих.

Функция проверки. Для момента $t$: если $t<m$, не успели ни одной копии — «нет». Иначе первая копия сделана за $m$ секунд, а за оставшиеся $t-m$ секунд каждый аппарат делает $\lfloor\frac{t-m}{x}\rfloor$ и $\lfloor\frac{t-m}{y}\rfloor$ копий. Затем бинарный поиск по минимальному $t$, при котором проверка выполняется.

Уточнение
На записи проверка произнесена как «$\lfloor\frac{t-m}{x}\rfloor+\lfloor\frac{t-m}{y}\rfloor\ge n$». Если первая копия, сделанная за $m$ секунд, входит в число $n$ (так в классической постановке этой задачи), то её нужно учесть: $1+\lfloor\frac{t-m}{x}\rfloor+\lfloor\frac{t-m}{y}\rfloor\ge n$, то есть остальные $n-1$ копий делаются двумя аппаратами за оставшееся время. Версия без единицы даёт другие ответы (на малых значениях они расходятся в большинстве случаев), поэтому сверьтесь с точной формулировкой условия задачи.
псевдокод
функция копии(n, x, y):
    m = min(x, y)
    подходит(t):  вернуть t >= m и 1 + (t-m)/x + (t-m)/y >= n     // деление целое
    l = m - 1;  r = m + n * max(x, y)              // l — не успели, r — точно успели
    пока l + 1 < r:
        t = (l + r) / 2
        если подходит(t): r = t  иначе: l = t
    вернуть r                                      // наименьшее подходящее время

3.3. Задача D. Вещественный бинарный поиск 2:17:00

2:17:00 Условие. Найти решение уравнения $x^2+\sqrt{x}=c$. Разбиралась на лекции: функция $f(x)=x^2+\sqrt{x}$ возрастает при $x\ge0$, поэтому корень находится бинарным поиском по вещественным числам с фиксированным числом итераций.

Развёрнутое объяснение
Левая граница 0 (там $f=0\le c$), правая — $\max(1,c)$: при $c\ge1$ имеем $f(c)=c^2+\sqrt{c}\ge c$, а при $c<1$ значение $f(1)=2>c$. Дальше 100 итераций сжимают отрезок до предела точности double. Проверка условия «подходит» здесь — $f(m)<c$ (значит, корень правее).
псевдокод
функция уравнение(c):                          // x² + √x = c
    l = 0;  r = max(1, c)
    повторить 100 раз:                             // число итераций надёжнее, чем сравнение с ε
        m = (l + r) / 2
        если m*m + √m < c: l = m  иначе: r = m
    вернуть (l + r) / 2

3.4. Задача E. Космическое поселение 2:18:00

2:18:00 Условие. Есть прямоугольное поле размером $V\times H$ и нужно разместить $n$ модулей размера $a\times b$. Вокруг каждого модуля с каждой стороны нужна защитная зона шириной $d$, то есть каждому модулю нужен прямоугольник $(a+2d)\times(b+2d)$. Нужно найти максимальное $d$, при котором все $n$ модулей помещаются; модули можно поворачивать на 90°. Гарантируется, что при $d=0$ они помещаются.

Монотонность. Если модули помещаются при каком-то $d$, то при меньшем $d$ они тем более поместятся; если не помещаются — при большем тоже не поместятся. Значит, можно бинарить $d$.

Проверка. В поле вдоль стороны $V$ помещается $\lfloor V/(a+2d)\rfloor$ модулей, вдоль $H$ — $\lfloor H/(b+2d)\rfloor$; всего их произведение. Для повёрнутого модуля — $\lfloor H/(a+2d)\rfloor\cdot\lfloor V/(b+2d)\rfloor$. Подходит, если хотя бы одно произведение не меньше $n$.

псевдокод
функция влезает(V, H, a, b, n, d):
    вернуть  (V/(a+2d)) * (H/(b+2d)) >= n
          или (H/(a+2d)) * (V/(b+2d)) >= n         // с поворотом на 90°

l = 0;  r = max(V, H) + 1                          // l подходит, r заведомо нет
пока l + 1 < r:
    m = (l + r) / 2
    если влезает(..., m): l = m  иначе: r = m
вернуть l

2:18:35 Вопрос: почему сравнивать площади нельзя. Проверка «суммарная площадь модулей не больше площади поля» неверна: при очень узком поле прямоугольники по площади «помещаются», а на деле нет. Нужно считать, сколько прямоугольников укладывается по каждой стороне, и перемножать. Пример: поле $4\times5$ и модули $2\times2$. По площади $\frac{4\cdot5}{2\cdot2}=5$ модулей, но по сторонам помещается $\lfloor4/2\rfloor\cdot\lfloor5/2\rfloor=2\cdot2=4$ — пятый уже никуда не встанет.

3.5. Задача F. Дипломы 2:24:22

2:24:22 Условие. У Пети $n$ дипломов размера $V\times H$. Найти наименьшее $c$, при котором на квадратную доску $c\times c$ можно разместить все дипломы (поворачивать нельзя).

Проверка. На доске $c\times c$ помещается $\lfloor c/H\rfloor$ рядов, в каждом из них $\lfloor c/V\rfloor$ дипломов. Подходит, если $\lfloor c/H\rfloor\cdot\lfloor c/V\rfloor\ge n$. Бинарный поиск: $l=0$ точно не подходит (дипломов нет), $r=n\cdot\max(V,H)$ точно подходит.

Переполнение
При $c$ порядка $10^{18}$ произведение двух больших чисел $\lfloor c/H\rfloor\cdot\lfloor c/V\rfloor$ выходит за long long. Чтобы этого избежать, умножение заменяют делением с округлением вверх: пусть rows = c / H. Условие «в каждом ряду достаточно места» записывается как perRow >= ceil(n / rows), то есть c / V >= (n + rows - 1) / rows. Деление переполнения не вызывает. Если rows == 0, ответ «не подходит» (иначе деление на 0).
псевдокод
функция влезают(c, V, H, n):
    рядов = c / H;  вРяду = c / V                  // целочисленное деление
    если рядов == 0 или вРяду == 0: вернуть ложь
    вернуть вРяду >= ⌈n / рядов⌉                   // без умножения — переполнения нет
                                                   // ⌈n/рядов⌉ = (n + рядов - 1) / рядов

l = 0;  r = n * max(V, H)                          // l не подходит, r подходит
пока l + 1 < r:
    m = (l + r) / 2
    если влезают(m): r = m  иначе: l = m
вернуть r

2:29:00 Вопрос про сложность задач. Задачи в контестах не отсортированы по сложности: учебные стараются ставить в начало, но общего правила нет. Поэтому стоит просматривать не только первые, но и средние и последние задачи — среди них может оказаться простая.

3.6. Задача G. Корень кубического уравнения 2:30:09

2:30:09 Условие. Найти корень уравнения $ax^3+bx^2+cx+d=0$ при $a\ne0$.

Идея. Кубическая функция непрерывна и уходит в $\pm\infty$ на концах, поэтому на достаточно широком отрезке $[l,r]$ знаки значений на концах противоположны и корень найдётся бинарным поиском по вещественным числам. Ловушка: при $a>0$ график идёт «снизу вверх» ($f(l)<0<f(r)$), а при $a<0$ — «сверху вниз» ($f(l)>0>f(r)$). Поэтому условие «слева значение отрицательно, справа положительно» верно только при $a>0$.

Простое решение: если $a<0$, умножить все четыре коэффициента на $-1$. Корни уравнения от этого не меняются, а $a$ становится положительным.

Важное дополнение
Границы поиска нужно брать достаточно широкими: любой корень по модулю не превосходит $R=1+\max\bigl(|b/a|,|c/a|,|d/a|\bigr)$ (оценка Коши). Отрезок $[-R,R]$ гарантированно содержит все корни, и на нём знаки на концах разные.
псевдокод
функция кубическийКорень(a, b, c, d):
    если a < 0: умножить a, b, c, d на -1          // слева f < 0, справа f > 0
    R = 1 + max(|b/a|, |c/a|, |d/a|)               // граница Коши
    l = -R;  r = R
    повторить 200 раз:
        m = (l + r) / 2
        f = ((a*m + b)*m + c)*m + d                // схема Горнера
        если f > 0: r = m  иначе: l = m
    вернуть l

3.7. Задача H. Провода 2:33:00

2:33:00 Условие. Даны $n$ отрезков проводов длин $L_i$. Нужно получить $k$ одинаковых отрезков целой длины $x$, причём $x$ как можно больше (резать можно как угодно, остатки выбрасываются). Пример: длины $802, 743, 457, 539$, $k=11$ — ответ 200: получается $4+3+2+2=11$ отрезков.

Почему нельзя просто сложить всё и поделить на $k$: остатки у каждого провода пропадают, поэтому нужно считать целые части отдельно для каждого провода.

Монотонность. Если можно получить $k$ отрезков длины $x$, то и меньшей длины тоже (от каждого отрезка отрежем лишнее). Если нельзя для $x$, то для большей длины тем более нельзя. Проверка: $\sum_i\lfloor L_i/x\rfloor\ge k$.

псевдокод
функция провода(L[1..n], k):
    подходит(x):  вернуть сумма по i от (L[i] / x) >= k    // целое деление
    l = 0;  r = 10^9 + 1                           // l «подходит», 10^9 — заведомо нет
    пока l + 1 < r:
        m = (l + r) / 2
        если подходит(m): l = m  иначе: r = m
    вернуть l                                      // 0, если даже длина 1 не получается

Отдельно обрабатывать «ответ 0» не нужно: если даже длины 1 не хватает, $l$ так и остаётся нулём. Время $O(n\log C)$, где $C$ — максимальная длина.

3.8. Задача I. Коровы в стойлах 2:40:26

2:40:26 Условие. Даны координаты $n$ стойл (по возрастанию) и число коров $k\le n$. Нужно расставить коров по стойлам так, чтобы минимальное расстояние между соседними коровами было как можно больше. Пример: стойла $2,5,7,11,15,20$, $k=3$ — ответ 9 (коровы в 2, 11, 20).

Монотонность. Если расстояние $x$ обеспечить можно, то и любое меньшее; если нельзя, то большее тем более.

Проверка для расстояния $d$ — жадность. Первую корову ставим в первое стойло; каждую следующую — в ближайшее стойло, отстоящее от предыдущей коровы не меньше чем на $d$. Считаем, сколько коров поместилось; подходит, если не меньше $k$.

Коровы: стойла 2 5 7 11 15 20, нужно расставить 3, расстояние d = 925711152099жадно: первое стойло занимаем, дальше — ближайшее, отстоящее не меньше чем на d
Для d = 9 жадность ставит коров в 2, 11, 20 — три штуки. Для d = 10 уже не получится, значит ответ 9.
псевдокод
функция коровы(x[1..n], k):                     // x отсортирован
    подходит(d):
        s = 1;  последняя = x[1]
        для i от 2 до n:
            если x[i] - последняя >= d: s += 1;  последняя = x[i]
        вернуть s >= k
    l = 1;  r = 10^9 + 1                           // l подходит, r — нет
    пока l + 1 < r:
        m = (l + r) / 2
        если подходит(m): l = m  иначе: r = m
    вернуть l                                      // левая граница

На записи лектор спрашивает, какую границу выводить в конце, — левую: она соответствует наибольшему $d$, для которого проверка «да».

3.9. Задача J. Лист бумаги и два текста 2:45:00

2:45:00 Условие. Лист бумаги в клеточку имеет известную ширину $W$ и неизвестную высоту. Лист делится вертикальной линией на две части, место разреза выбираем сами. В левую часть нужно записать $N$ слов (заданы их длины $a_1,\dots,a_N$), в правую — $M$ слов (длины $b_1,\dots,b_M$). Слово пишется в клеточках подряд, между словами пропускается одна клетка; если следующее слово целиком не помещается в строку, оно полностью переносится на следующую строку. Нужно найти минимальную высоту листа, при которой всё помещается. Гарантируется, что слова поместиться могут.

Лист: ширина W задана, высота H — неизвестна; разрез — любойразрез: левой части нужна ширина A, правой — Bнужно: A + B ≤ W; каждую часть проверяем отдельным бинпоиском по ширине
Слова записываются в строки через пробел, не помещающееся слово переносится целиком. Две колонки не зависят друг от друга, поэтому для высоты H ищем наименьшую ширину каждой.

Идея: бинарный поиск внутри бинарного поиска. Высота монотонна: если все слова влезают в высоту $h$, то в большую тоже. Поэтому внешний бинарный поиск идёт по высоте $h$. Чтобы проверить высоту $h$, левый и правый тексты можно рассматривать независимо:

Наименьшая ширина для заданного текста тоже монотонна (больше ширина — легче), поэтому её ищет вложенный бинарный поиск: $l$ — ширина, в которую точно не помещается (например, длина самого длинного слова минус 1), $r=W$ — помещается по гарантии. Для проверки конкретной ширины слова записываются жадно: пишем, пока слово влезает, иначе начинаем новую строку, и считаем число строк.

псевдокод
функция сколькоСтрок(w[1..N], ширина):
    строк = 1;  занято = пусто
    для каждой длины len из w:
        если занято пусто:                          занято = len
        иначе если занято + 1 + len <= ширина:      занято += 1 + len    // через пробел
        иначе:                                      строк += 1;  занято = len
    вернуть строк

функция минШирина(w, H, W):
    l = max(w) - 1;  r = W                          // l не помещается, r помещается
    пока l + 1 < r:
        m = (l + r) / 2
        если сколькоСтрок(w, m) <= H: r = m  иначе: l = m
    вернуть r

функция минВысота(A, B, W):
    l = 0;  r = |A| + |B|                           // l не хватает, r хватит точно
    пока l + 1 < r:
        h = (l + r) / 2
        если минШирина(A, h, W) + минШирина(B, h, W) <= W: r = h  иначе: l = h
    вернуть r
Исправление
В ходе разбора лектор дважды поправил себя по условию (сначала знак в неравенстве про ширину, затем асимптотику). Итог: условие существования разреза — $A+B\le W$ (сумма ширин не больше ширины листа). Сложность: внешний поиск делает $O(\log(N+M))$ шагов, на каждом — два вложенных поиска по $O(\log W)$ шагов, и на каждом такой шаг проход по словам за $O(N+M)$. Итого $O\bigl((N+M)\log(N+M)\log W\bigr)$ — слова проходятся не $N$ раз, а один раз за проверку.

3.10. Задача K. Пиццы на олимпиаде 2:58:00

2:58:00 Условие. Между двумя аудиториями расположены $n$ точек с пиццей; в точке $i$ лежит $a_i$ пицц. Каждую секунду из левой аудитории выходит участник, идёт к ближайшей точке, где ещё осталась пицца, и съедает одну; одновременно из правой аудитории выходит участник и делает то же со своей стороны. Если в какой-то момент расстояние между двумя участниками (в точках) не больше $k$, они начинают разговор. Нужно найти, через сколько секунд начнётся первый разговор.

Участники идут навстречу и съедают по пицце за секунду3т.05т.12т.26т.34т.41т.53т.6левый идёт вправоправый идёт влеворазговор начинается, когда расстояние между ними стало ≤ k
Расстояние между участниками со временем только уменьшается, поэтому момент разговора ищем бинпоиском по времени.

Монотонность. Участники всё время идут навстречу друг другу, поэтому расстояние между ними со временем только уменьшается. Если разговор начался к секунде $t$, он уже идёт и в любую более позднюю. Значит, по времени можно делать бинарный поиск.

Проверка для секунды $t$. Нужно узнать, где в секунду $t$ стоит левый участник и где — правый:

Точки $x$ и $y$ находятся бинарным поиском по префиксным суммам (то есть получается три бинарных поиска: внешний по времени и два вложенных; вложенные не зависят друг от друга).

Уточнение
На записи условие позиции формулировалось как «сумма всех пицц до точки хотя бы $t$» и приводился пример про 12 секунд для точек с 2, 4, 6 пиццами. Строгая формулировка такая: в секунду $t$ участник находится в точке $i$ тогда и только тогда, когда $P_i<t\le P_{i+1}$, где $P_i=a_0+\dots+a_{i-1}$. Для точек с 2, 4, 6 пиццами секунды 1–2 участник у первой точки, 3–6 — у второй, 7–12 — у третьей. Именно с включительной суммой код ниже совпал с прямым моделированием на случайных малых тестах.
псевдокод
функция пиццы(a[0..n-1], k):
    p[0] = 0;  p[i+1] = p[i] + a[i]                 // префиксные суммы
    подходит(t):                                     // разговор начался к секунде t?
        x = первая точка, где p[x+1] >= t            // бинпоиск слева
        y = последняя точка, где a[y] + ... + a[n-1] >= t   // бинпоиск справа
        вернуть y - x <= k
    l = 0;  r = p[n]                                 // l — разговора нет, r — точно есть
    пока l + 1 < r:
        m = (l + r) / 2
        если подходит(m): r = m  иначе: l = m
    вернуть r

3:10:00 Вариант проще. Позиции $x$ и $y$ для заданного $t$ можно искать и обычным линейным проходом по массиву — это даёт $O(n\log C)$ вместо $O(\log C\cdot\log n)$, но тоже проходит. В чате спросили, нельзя ли получить позицию «формулой»: нет, это функция от конкретных чисел массива, поэтому нужен либо поиск по префиксным суммам, либо проход.

4. Общие приёмы разобранных задач

Важное дополнение
Краткая шпаргалка по тому, какую идею разбирали в каждом контесте.
Признак задачиПриёмПримеры
Нужно знать минимум/сумму «текущего» содержимого контейнераХранить вместе с элементом агрегат (минимум) или пару (число, количество)Стек и очередь с минимумом, очередь блоков
Операции затрагивают только вершину или начало и конецСтек, очередь, дек; сворачивание в группыШарики, соседние буквы, тупик, парикмахерская
Запрос на отрезке, прямоугольнике, параллелепипедеПрефиксные суммы и включение–исключениеRSQ 1D/2D/3D
Много прибавлений, нужен только итогРазностный массив (в 2D — четыре угла)RUQ, супероперации, прибавление на прямоугольнике
Подотрезки, пары, слияние отсортированногоДва указателя, которые двигаются в одну сторонуКвадраты и кубы, стильная одежда, пары точек, префикс и суффикс
«Максимизировать/минимизировать значение, при котором что-то можно»Бинарный поиск по ответу с монотонной проверкойМодули, дипломы, провода, коровы, лист, пиццы
Нужно вещественное числоФиксированное число итераций вместо сравнения с $\varepsilon$$x^2+\sqrt x=c$, кубическое уравнение