Что тренируем сегодня
Двадцать две тренировки позади. Сегодня — экзамен, и устроен он иначе, чем всё, что было раньше.
Раньше статья сначала объясняла приём, потом показывала задачу, где он применяется. Порядок был удобный, но нечестный: на настоящем контесте никто не говорит, какой приём нужен, и половина работы — как раз догадаться. Сегодня этой подсказки не будет.
Как проходить сессию:
- Прочитай все три условия сразу, до того как начнёшь что-то решать. Оцени каждую задачу: что примерно требуется и сколько это займёт.
- Реши сам, в каком порядке браться. Порядок «A, B, C» в статье — по возрастанию сложности, но твоё личное «легче/тяжелее» может отличаться, и доверять стоит ему.
- Заведи таймер на три часа. Это средняя длина раунда, и ограничение по времени — половина упражнения.
- Разборы читай только после того, как сдался или решил. Причём даже после успешного решения читать полезно: способ может отличаться, а разница между твоим и разобранным решением — самое ценное, что можно вынести из контеста.
Задачи подобраны из верхней части коридора «Пика»: одна на CF ~2300 и две на CF ~2400. Все три — на технику, знакомую по предыдущим тренировкам, но ни в одной приём не назван в условии.
Раздел «Теория» сегодня тоже про другое: не про алгоритмы (это было бы подсказкой), а про два контестных приёма, которые работают на любой задаче.
Теория
Приём 1. Читать ограничения как подсказку к нужной сложности.
Ограничения в условии — не формальность, а самая сильная подсказка из всех доступных. Автор задачи выбирает их так, чтобы задуманное решение проходило, а более простое — нет. Значит по величине входа можно с хорошей точностью восстановить, какой сложности решение от вас ждут.
Когда применять: всегда, сразу после первого прочтения условия, до того как придумывать решение. Это занимает пять секунд и отсекает целые классы неверных идей.
Как решать. Ориентир — примерно элементарных операций в секунду для C++ (для Python смело делите на десять-двадцать). Отсюда таблица:
| Ограничение на | Ожидаемая сложность | Что обычно за этим стоит |
|---|---|---|
| до 12 | перебор подмножеств, динамика по маскам | |
| до 20–24 | встреча посередине | |
| до 100 | тяжёлая динамика по нескольким индексам | |
| до 500 | динамика по отрезкам, Флойд | |
| до 5000 | динамика по двум индексам, перебор пар | |
| до | сортировка, бинарный поиск, деревья, множества | |
| до | два указателя, префиксные суммы, счётчики | |
| до | или | формула, бинарный поиск по ответу, перебор делителей |
Мини-пример. В условии стоит и . Произведение — как раз укладывается. Это почти прямая надпись: «нужна динамика с тремя измерениями, два из которых по , третье по ». Заметьте, насколько это сужает поиск ещё до того, как вы поняли, о чём задача.
Отдельно стоит смотреть на два ограничения рядом. Если написано и , то восьмёрка почти наверняка означает или где-то внутри. Маленькое число рядом с большим — всегда сигнал.
Где ошибаются: читают ограничения после того, как придумали решение, — «чтобы проверить». К этому моменту жалко бросать уже придуманное, и вместо смены подхода начинается оптимизация заведомо неподходящего.
Приём 2. Стресс-тест: поймать ошибку до отправки, а не после.
Решение прошло примеры из условия — это не значит почти ничего: примеры маленькие и специально безобидные. Стресс-тест — способ найти контрпример самому, за пять минут, вместо десяти отправок с «неправильным ответом на тесте 34».
Когда применять:
- решение прошло примеры, но уверенности нет;
- уже получен неправильный ответ, и непонятно, на чём;
- в решении есть жадность или неочевидная формула — то есть места, где ошибка не видна глазами.
Как решать:
- Написать генератор маленьких случайных тестов — настолько маленьких, чтобы ответ можно было проверить перебором (обычно , значения до 8).
- Написать эталон — заведомо верное решение перебором, без оптимизаций и без ума. Оно может работать экспоненциальное время, это нормально.
- Гонять цикл: сгенерировали тест, прогнали оба решения, сравнили вывод. Как только вывод разошёлся — остановиться и распечатать тест.
- Полученный контрпример почти всегда крошечный, и на нём ошибка видна руками.
Генератор:
# gen.py — маленький случайный тест по номеру запуска
import random
import sys
random.seed(int(sys.argv[1]))
n = random.randint(1, 6)
print(n)
print(*[random.randint(1, 8) for _ in range(n)])
Цикл сравнения:
for i in $(seq 1 500); do
python3 gen.py $i > test.txt
./solution < test.txt > fast.txt # проверяемое решение
python3 brute.py < test.txt > slow.txt # заведомо верный перебор
if ! diff -q fast.txt slow.txt > /dev/null; then
echo "Расхождение на тесте $i:"
cat test.txt
break
fi
done
echo "готово"
Мини-пример. Решение с жадностью «брать самый дешёвый элемент» прошло оба примера из условия. Стресс-тест на пятистах случайных входах из шести чисел находит контрпример на четвёртом же тесте — и на четырёх числах сразу видно, почему жадность неверна. Без стресс-теста та же ошибка стоила бы получаса и нескольких отправок.
Где ошибаются: пишут эталон «почти так же, как основное решение». Тогда обе программы содержат одну и ту же ошибку и согласованно выдают неверный ответ. Эталон обязан быть тупым: полный перебор, никаких общих функций с основным решением.
Ещё одна частая беда — генератор со слишком большими значениями. Если тесты крупные, расхождение найдётся, но разбираться в нём будет невозможно. Чем меньше тест, тем полезнее контрпример.
Экзамен: три задачи
Дальше идут три условия. Разборы — после них. Если хочешь честного контеста, промотай прямо сейчас к задаче A и не листай ниже, пока не решишь или не сдашься.
Задача A — CF ~2300 — «Sonya and Problem Wihtout a Legend»
Дан массив из n чисел. За одну операцию разрешается увеличить или уменьшить любой элемент на единицу; каждая операция стоит одну единицу. Нужно минимальной суммарной стоимостью сделать массив строго возрастающим.
Формат ввода:
- Строка 1: число
n(). - Строка 2:
nчиселa[1] … a[n]().
Формат вывода: одно число — минимальная суммарная стоимость.
Пример 1:
Ввод:
7
2 1 5 11 5 9 11
Вывод:
9
Пример 2:
Ввод:
5
5 4 3 2 1
Вывод:
12
Задача B — CF ~2400 — «Tree and Queries»
Дано дерево из n вершин, подвешенное за вершину 1. Каждая вершина покрашена в некоторый цвет. Далее следуют m запросов вида «v, k»: сколько различных цветов встречается в поддереве вершины v не менее k раз.
Формат ввода:
- Строка 1: два числа
nиm(). - Строка 2:
nчисел — цвета вершин (). - Каждая из следующих
n − 1строк: два числа — концы ребра. - Каждая из следующих
mстрок: два числаvиk.
Формат вывода: m строк — ответы на запросы в порядке их поступления.
Пример:
Ввод:
8 5
1 2 2 3 3 2 3 3
1 2
1 5
2 3
2 4
5 6
5 7
5 8
1 2
1 3
1 4
2 3
5 3
Вывод:
2
2
1
0
1
Задача C — CF ~2400 — «Group Projects»
Есть n участников, у каждого известен числовой уровень a[i]. Всех нужно разбить на группы: групп может быть сколько угодно, каждый участник попадает ровно в одну. Несбалансированность группы — разность наибольшего и наименьшего уровня внутри неё (для группы из одного участника это ноль).
Сколько существует разбиений, у которых сумма несбалансированностей всех групп не превосходит k? Ответ вывести по модулю . Два разбиения различны, если какие-то два участника оказались вместе в одном и порознь в другом.
Формат ввода:
- Строка 1: два числа
nиk(, ). - Строка 2:
nчиселa[1] … a[n]().
Формат вывода: одно число — количество разбиений по модулю .
Пример 1:
Ввод:
3 1
1 2 3
Вывод:
3
Пример 2:
Ввод:
4 0
1 1 2 2
Вывод:
4
Разбор задачи A — «Sonya and Problem Wihtout a Legend»
Разберём на примере
Возьмём второй пример: 5 4 3 2 1. Массив убывает, а нужен строго возрастающий. Наивная мысль — «поднять каждый следующий чуть выше предыдущего» — сразу упирается в вопрос: а насколько поднимать, если менять можно и в другую сторону?
Первым делом избавимся от слова «строго». Если массив b строго возрастает, то b[i] ≥ b[i-1] + 1, то есть b[i] − i не убывает. Значит достаточно заменить a[i] на a[i] − i (нумерация с нуля) и искать неубывающий массив — стоимость при этом не меняется, потому что каждый элемент сдвигается на константу вместе со своей целью.
Для 5 4 3 2 1 вычитаем 0, 1, 2, 3, 4 и получаем 5 3 1 −1 −3. Теперь надо сделать этот массив неубывающим минимальной суммой изменений.
Попробуем угадать ответ руками. Сделать всё равным одному числу x стоит — минимум достигается на медиане, то есть при x = 1, и равен . Совпадает с ожидаемым ответом. Значит здесь оптимум — сделать все элементы одинаковыми, и это неслучайно: массив убывает целиком, «разложить» его на возрастающие куски негде.
Ключевое наблюдение, которое превращает задачу в динамику: в оптимальном ответе каждое новое значение можно взять из исходного набора значений. Двигать элемент дальше, чем до ближайшего «нужного» значения из массива, всегда невыгодно — стоимость при этом растёт, а ограничение не становится мягче. Значит кандидатов на значение всего n штук, а не .
Идея решения
Алгоритм в одну фразу: вычесть из каждого элемента его индекс, отсортировать множество получившихся значений в массив кандидатов, и посчитать динамику dp[i][j] — минимальная стоимость, если первые i элементов уже неубывающие и i-й приравнен к j-му кандидату.
Почему «строго возрастающий» сводится к «неубывающему». Массив b строго возрастает тогда и только тогда, когда b[i] − i не убывает: неравенство b[i] > b[i-1] для целых чисел равносильно b[i] ≥ b[i-1] + 1, то есть b[i] − i ≥ b[i-1] − (i-1). Стоимость не меняется, если из обеих частей каждой разности вычесть i.
Почему значения можно брать только из исходного набора. Пусть в оптимальном ответе какое-то значение v не встречается среди исходных. Рассмотрим все позиции, где стоит ровно v (они идут подряд, раз массив неубывающий). Стоимость как функция от v на этом блоке — сумма модулей, то есть кусочно-линейная выпуклая функция с изломами только в исходных значениях. Сдвигая v в сторону уменьшения стоимости, мы упрёмся либо в границу, заданную соседними блоками (тогда блоки сливаются, и рассуждение повторяется), либо в излом — то есть в исходное значение. Значит существует оптимум, все значения которого взяты из набора.
Переход динамики. dp[i][j] = |a[i] − b[j]| + min(dp[i-1][0..j]): i-й элемент можно приравнять к j-му кандидату, если предыдущий приравнен к любому кандидату не больше j-го. Минимум по префиксу считается на лету одной переменной, поэтому один слой обходится за , а вся динамика — за .
Почему достаточно. При это операций — с запасом.
Про типы. Значения до , элементов до 3000: суммарная стоимость доходит до , 32-битный тип переполняется.
Псевдокод
прочитать n, a
для i от 0 до n-1: a[i] -= i // строго возрастающая -> неубывающая
b = отсортированный набор различных значений a
m = размер b
prev[j] = 0 для всех j // до первого элемента ограничений нет
для i от 0 до n-1:
best = бесконечность
для j от 0 до m-1:
best = min(best, prev[j]) // минимум dp[i-1][0..j]
cur[j] = best + |a[i] - b[j]|
prev = cur
вывести min(prev)
Код решения
Комментарии по реализации. Минимум по префиксу dp[i-1][0..j] не пересчитывается заново для каждого j — он накапливается одной переменной по ходу того же цикла. Без этого решение было бы и на трёх тысячах не прошло бы. Начальный слой заполнен нулями: до первого элемента никаких ограничений нет, поэтому «предыдущий» может быть любым. В C++ слои меняются местами через swap, а не переприсваиваются — это избавляет от лишнего выделения памяти на каждой из трёх тысяч итераций.
Проверка на примерах
Пример 2: 5 4 3 2 1. После вычитания индексов — 5 3 1 −1 −3. Кандидаты: −3, −1, 1, 3, 5. Разбор выше показал, что оптимум — сделать все элементы равными 1, стоимость . Наш вывод: 12 — совпадает с ожидаемым.
Пример 1: 2 1 5 11 5 9 11. После вычитания индексов — 2 0 3 8 1 4 5. Массив почти неубывающий: портят его вторая позиция (0 после 2) и пятая (1 после 8). Оптимальный неубывающий массив — 0 0 3 3 3 4 5, стоимость . Наш вывод: 9 — совпадает с ожидаемым.
Крайние случаи
n = 1. Массив из одного элемента уже строго возрастает; ответ 0.- Массив уже строго возрастает. После вычитания индексов он неубывающий, стоимость 0.
- Массив строго убывает. Худший случай по стоимости: оптимум обычно сводит всё к одному значению.
- Все элементы равны. После вычитания индексов массив строго убывает — тот же случай, что выше.
- Значения до при
n = 3000. Суммарная стоимость до ; 64-битный тип обязателен. - Отрицательные значения после сдвига.
a[i] − iуходит ниже нуля — это нормально и не требует отдельной обработки.
Типичные ошибки
- Забыть про «строго». Без вычитания индексов решается задача про неубывающий массив, и ответ занижается.
- Пытаться поднимать элементы только вверх. Уменьшать тоже можно, и в примере 1 оптимум именно уменьшает первый элемент.
- Пересчитывать минимум по префиксу заново для каждого
j. вместо . - Считать стоимость в 32-битном типе. До .
- Перебирать значения из всего диапазона . Кандидатов достаточно
nштук, и это отдельное утверждение, которое надо доказать, а не угадать. - Не убирать дубликаты из набора кандидатов. Работать будет, но лишние равные значения замедляют внутренний цикл вдвое-втрое.
- Инициализировать нулевой слой бесконечностями. До первого элемента ограничений нет — там должны быть нули.
Сложность
Время: — n слоёв по m ≤ n кандидатов. Память: при хранении двух слоёв.
Разбор задачи B — «Tree and Queries»
Разберём на примере
В примере дерево такое: к вершине 1 подвешены вершины 2 и 5; к вершине 2 — вершины 3 и 4; к вершине 5 — вершины 6, 7 и 8. Цвета по порядку: 1 2 2 3 3 2 3 3.
Посчитаем для каждого поддерева, сколько вершин какого цвета:
| Вершина | Поддерево | Цвет 1 | Цвет 2 | Цвет 3 |
|---|---|---|---|---|
| 1 | все восемь | 1 | 3 | 4 |
| 2 | {2,3,4} | 0 | 2 | 1 |
| 5 | {5,6,7,8} | 0 | 1 | 3 |
Теперь ответы: запрос 1 2 — цветов, встречающихся хотя бы дважды в поддереве 1, ровно два (цвета 2 и 3). Запрос 1 3 — тоже два (три и четыре вхождения). Запрос 1 4 — только цвет 3, ответ 1. Запрос 2 3 — в поддереве вершины 2 максимум два вхождения, ответ 0. Запрос 5 3 — цвет 3 встречается трижды, ответ 1.
Что мешает решить в лоб. Для каждого запроса обойти поддерево — запросов по вершин, то есть . Не проходит.
Идея в другом. Заметим: если завести массив f, где f[j] — количество цветов, встречающихся не менее j раз в текущем рассматриваемом множестве вершин, то ответ на запрос (v, k) — это просто f[k]. И массив f очень легко поддерживать: когда счётчик какого-то цвета вырастает с x до x + 1, этот цвет впервые начинает удовлетворять условию «не менее x + 1», значит увеличивается ровно одна ячейка — f[x+1].
Осталось так организовать обход дерева, чтобы «текущее множество» вовремя оказывалось равно нужному поддереву — и чтобы суммарных добавлений и удалений было не , а . Это и делает приём из предыдущей тренировки, только в варианте с явными операциями «добавить вершину» и «убрать вершину».
Идея решения
Алгоритм в одну фразу: обходить дерево, сохраняя счётчики самого крупного поддерева и пересчитывая лёгкие, поддерживать глобальный массив «сколько цветов встречается не менее j раз» и отвечать на запросы вершины в тот момент, когда в счётчиках лежит ровно её поддерево.
Как устроен обход. Для каждой вершины v выделим тяжёлую дочернюю вершину — ту, чьё поддерево крупнее остальных. Обход выглядит так:
- Обработать все лёгкие поддеревья по очереди, каждое — с полной очисткой счётчиков после себя.
- Обработать тяжёлое поддерево без очистки — его счётчики остаются.
- Добавить в счётчики все вершины лёгких поддеревьев и саму
v. Теперь в счётчиках ровно поддеревоv. - Ответить на все запросы вершины
v. - Если
vсама была лёгкой для своего предка — вычистить всё поддеревоvиз счётчиков.
Почему это . Вершина добавляется заново каждый раз, когда она лежит в лёгком поддереве на пути к корню. Каждый переход по лёгкому ребру как минимум удваивает размер поддерева (иначе ребро было бы тяжёлым), а удвоений от 1 до n бывает не больше . Значит на вершину приходится не более добавлений, и всего операций — около при .
Почему ответ равен f[k]. По построению f[j] — количество цветов с числом вхождений не меньше j. Это ровно то, что спрашивают. Поддерживается f при добавлении вершины цвета c строчкой f[++cnt[c]] += 1, а при удалении — f[cnt[c]--] -= 1. Заметьте симметрию: добавление сначала увеличивает счётчик, потом трогает f, удаление — наоборот.
Как быстро добавлять целое поддерево. Пронумеруем вершины в порядке обхода так, чтобы поддерево каждой вершины занимало непрерывный отрезок номеров. Тогда «добавить всё поддерево u» — это пробежать по отрезку массива, без всякой рекурсии.
Про запрос с большим k. Если k больше n, ответ заведомо ноль; массив f такого индекса не содержит, и проверку надо ставить явно.
Псевдокод
прочитать дерево и цвета, запросы сгруппировать по вершине
обойти дерево из вершины 1 нерекурсивно, получив flat[] и tin[] так,
чтобы поддерево v занимало отрезок [tin[v], tin[v] + size[v] - 1]
посчитать size[v] и тяжёлую дочернюю вершину heavy[v]
cnt[цвет] = 0, f[j] = 0
стек = [(1, сохранять, стадия 0)]
пока стек не пуст:
(v, keep, стадия) = снять со стека
если стадия == 0:
положить (v, keep, стадия 1)
положить (heavy[v], сохранять, стадия 0) // если есть
положить каждую лёгкую дочернюю (to, не сохранять, стадия 0)
// лёгкие лежат выше — они и обработаются первыми
иначе:
для каждой лёгкой дочерней to:
для i от tin[to] до tin[to] + size[to] - 1:
c = цвет(flat[i]); cnt[c] += 1; f[cnt[c]] += 1
c = цвет(v); cnt[c] += 1; f[cnt[c]] += 1
для каждого запроса (k, номер) вершины v:
ответ[номер] = f[k], если k <= n, иначе 0
если не keep:
для i от tin[v] до tin[v] + size[v] - 1:
c = цвет(flat[i]); f[cnt[c]] -= 1; cnt[c] -= 1
вывести ответы
Код решения
Комментарии по реализации. Обход развёрнут в цикл со стеком состояний (вершина, сохранять ли, стадия): стадия 0 раскладывает потомков, стадия 1 выполняет саму работу. Порядок укладки на стек существенный — тяжёлая вершина кладётся раньше лёгких, поэтому снимается позже, и лёгкие успевают полностью отработать и вычиститься до неё. Массив cnt размеряется по максимальному цвету, а не по n: цвета в условии независимы от количества вершин. Проверка k <= n перед обращением к f[k] обязательна — запрос вправе спросить про k, которого не бывает.
Проверка на примерах
Пример из условия. Проследим момент, когда в счётчиках лежит поддерево вершины 5 ({5,6,7,8}, цвета 3, 2, 3, 3):
| Добавили | cnt[2] | cnt[3] | f[1] | f[2] | f[3] |
|---|---|---|---|---|---|
| вершину 6 (цвет 2) | 1 | 0 | 1 | 0 | 0 |
| вершину 7 (цвет 3) | 1 | 1 | 2 | 0 | 0 |
| вершину 8 (цвет 3) | 1 | 2 | 2 | 1 | 0 |
| вершину 5 (цвет 3) | 1 | 3 | 2 | 1 | 1 |
Запрос 5 3 читает f[3] = 1 — верно, только цвет 3 встречается не меньше трёх раз.
Для корня счётчики становятся cnt[1] = 1, cnt[2] = 3, cnt[3] = 4, а значит f[1] = 3, f[2] = 2, f[3] = 2, f[4] = 1. Запросы 1 2, 1 3, 1 4 дают 2, 2 и 1. Запрос 2 3 в поддереве вершины 2 читает f[3] = 0.
Наш вывод: 2 2 1 0 1 по строкам — совпадает с ожидаемым.
Проверим ещё вырожденный вход:
Ввод:
1 2
3
1 1
1 2
Вывод:
1
0
Одна вершина цвета 3: один цвет встречается хотя бы раз и ни одного — хотя бы дважды.
Крайние случаи
n = 1. Рёбер нет; тяжёлой вершины тоже нет, и стадия 0 не кладёт на стек ничего лишнего.k > n. Ответ 0; без явной проверки — обращение за границу массива.- Все цвета различны. Тогда
f[1] = размер поддерева, аf[2]и дальше — нули. - Все цвета одинаковы.
f[j] = 1для всехjдо размера поддерева. - Дерево-цепочка на вершин. Тяжёлая вершина всегда одна, лёгких поддеревьев нет — добавлений ровно
n. Одновременно тест на нерекурсивность. - Звезда. Все лучи лёгкие, но каждый размера 1 — добавлений тоже около
n. - Цвет больше
n. Массив счётчиков должен размеряться по максимальному цвету.
Типичные ошибки
- Обходить поддерево на каждый запрос. операций.
- Не сохранять счётчики тяжёлого поддерева. Тогда каждая вершина пересчитывается на каждом уровне — снова квадрат.
- Перепутать порядок в
fпри удалении. При добавлении сначала растётcnt, потомf; при удалении сначала уменьшаетсяf, потомcnt. - Забыть проверку
k <= n. Выход за границу массива. - Размерить
cntпоn. Цвета могут быть больше количества вершин. - Отвечать на запросы не в тот момент. Ответ читается ровно тогда, когда в счётчиках лежит поддерево
v— до очистки и после добавления лёгких. - Рекурсивный обход. Цепочка из вершин.
- Класть тяжёлую вершину на стек после лёгких. Тогда она обработается первой, а лёгкие затрут её счётчики своей очисткой.
Сложность
Время: — каждая вершина добавляется не более раз. Память: .
Разбор задачи C — «Group Projects»
Разберём на примере
Возьмём первый пример: уровни 1 2 3, порог k = 1. Выпишем все разбиения трёх участников и посчитаем несбалансированность:
| Разбиение | Несбалансированности | Сумма | Подходит? |
|---|---|---|---|
{1} {2} {3} | 0, 0, 0 | 0 | да |
{1,2} {3} | 1, 0 | 1 | да |
{1} {2,3} | 0, 1 | 1 | да |
{1,3} {2} | 2, 0 | 2 | нет |
{1,2,3} | 2 | 2 | нет |
Ответ 3. Обратите внимание на строку {1,3} {2}: группа из уровней 1 и 3 стоит два, хотя внутри неё всего два участника. Несбалансированность зависит только от крайних значений, а не от количества.
Отсюда первое решение: отсортировать уровни. Тогда в любой группе несбалансированность — это разность между последним и первым её участником в отсортированном порядке.
Второе, менее очевидное. Разность «последний минус первый» можно разложить по промежуткам между соседними отсортированными значениями. Пусть отсортированные уровни , и . Тогда несбалансированность группы — это сумма по всем промежуткам, которые группа «накрывает», то есть промежуткам между её первым и последним участником.
А общая сумма несбалансированностей — это сумма по всем промежуткам от «, умноженного на количество групп, накрывающих этот промежуток». И количество накрывающих групп очень легко отслеживать по ходу: это число групп, которые уже начались, но ещё не закончились.
Проверим на разбиении {1,2} {3}: промежутков два, оба размера 1. Первый промежуток (между уровнями 1 и 2) накрывает одна группа — {1,2}. Второй (между 2 и 3) — ни одной. Сумма . Совпадает.
Значит можно идти по отсортированным участникам слева направо и хранить в состоянии, сколько групп сейчас «открыто». Это и есть решение.
Идея решения
Алгоритм в одну фразу: отсортировать уровни и считать динамику dp[j][t] — количество способов разложить первых участников так, чтобы сейчас было j незакрытых групп, а накопленная сумма несбалансированностей равнялась t.
Что такое «незакрытая группа». Группа считается открытой с момента, когда в неё попал первый (то есть наименьший) её участник, и закрытой, когда в неё попал последний (наибольший). Промежуток между соседними участниками накрывается ровно теми группами, которые в этот момент открыты.
Переход. Перед обработкой i-го участника все j открытых групп «переваливают» через промежуток , добавляя к сумме . Дальше у участника четыре возможности:
- Открыть новую группу и оставить её открытой: одним способом,
jувеличивается на единицу. - Составить группу из себя одного (открыл и сразу закрыл): одним способом,
jне меняется. - Войти в одну из
jоткрытых групп, не закрывая её:jспособов,jне меняется. - Войти в одну из
jоткрытых групп и закрыть её:jспособов,jуменьшается на единицу.
Варианты 2 и 3 дают вместе множитель j + 1 при неизменном j — именно так они и записаны в коде.
Ответ. Сумма dp[0][t] по всем t от 0 до k после обработки всех участников: незакрытых групп остаться не должно.
Почему разбиения не считаются дважды. Каждое разбиение однозначно определяет, в какой момент какая группа открывается и закрывается: открытие — это появление её минимального участника, закрытие — максимального. Значит каждому разбиению соответствует ровно одна последовательность решений, а разным разбиениям — разные последовательности.
Про равные уровни. Если , то , и промежуток ничего не стоит. Порядок среди равных участников фиксируется сортировкой, и двойного счёта это не создаёт: участники различимы, а решения принимаются в фиксированном порядке.
Оценка размера. Состояний на слой, слоёв n, переходов четыре — около при , . Ровно то, на что намекают ограничения.
Псевдокод
прочитать n, k, отсортировать a
dp[0][0] = 1 // ни одного участника, ноль открытых групп
для i от 0 до n-1:
d = a[i] - a[i-1], если i > 0, иначе 0
ndp = нули
для j от 0 до i:
доп = j * d // все открытые группы переваливают промежуток
если доп > k: пропустить
для t от 0 до k - доп:
val = dp[j][t]
если val == 0: продолжить
nt = t + доп
ndp[j+1][nt] += val // открыть новую группу
ndp[j][nt] += val * (j + 1) // группа из себя одного либо войти в открытую
если j > 0:
ndp[j-1][nt] += val * j // войти в открытую и закрыть её
dp = ndp
вывести сумму dp[0][t] по всем t, по модулю 10^9 + 7
Код решения
Комментарии по реализации. Пропуск нулевых состояний (if not val: continue) — не косметика: на реальных входах заполнена лишь малая часть таблицы, и без пропуска решение работает в разы дольше. Проверка add > k отсекает целые строки сразу, не заходя во внутренний цикл. Внутренний цикл ограничен так, чтобы t + add не превышало k — это избавляет от проверки внутри тела. Слои меняются местами, а не копируются: таблица размера пересоздаётся двести раз, и лишнее копирование было бы заметно.
Проверка на примерах
Пример 1: n = 3, k = 1, уровни 1 2 3 (уже отсортированы, оба промежутка равны 1). Проследим по слоям, оставляя только достижимые состояния:
| После участника | Состояния (j, t) → количество |
|---|---|
| 1-го | (0,0) → 1 (сам себе группа), (1,0) → 1 (открыл группу) |
| 2-го | (0,0) → 1, (1,0) → 2, (0,1) → 1, (1,1) → 1, (2,1) → 1 |
| 3-го | суммарно по (0, t) при t ≤ 1 получается 3 |
Ручной перебор в разборе выше дал те же 3 разбиения. Наш вывод: 3 — совпадает с ожидаемым.
Пример 2: n = 4, k = 0, уровни 1 1 2 2. Нулевой порог означает, что каждая группа обязана состоять из участников одного уровня. Две единицы можно разложить двумя способами (вместе или порознь), две двойки — тоже двумя, и выборы независимы: . Наш вывод: 4 — совпадает с ожидаемым.
Проверим ещё границу:
Ввод:
1 0
7
Вывод:
1
Один участник — единственное разбиение, несбалансированность ноль.
Крайние случаи
n = 1. Ответ всегда 1 при любомk.k = 0. Группы обязаны быть из одинаковых уровней; ответ — произведение чисел Белла по группам равных значений.- Все уровни равны. Все промежутки нулевые, ограничение не работает вовсе, и ответ — число Белла от
nпо модулю. kзаведомо больше максимально возможной суммы. Ответ — снова число Белла отn: подходят все разбиения.- Максимальные размеры. , — таблица из ячеек и двести слоёв.
- Большой разброс уровней. Промежутки быстро превышают
k, и почти все состояния отсекаются — решение работает заметно быстрее худшей оценки.
Типичные ошибки
- Не сортировать уровни. Вся конструкция «промежуток накрывается открытыми группами» опирается на порядок.
- Считать несбалансированность по всем парам внутри группы. Она определяется только крайними значениями.
- Забыть вариант «группа из себя одного». Он даёт единицу, которая складывается с
jв множительj + 1; без него теряется целый класс разбиений. - Перепутать множители при входе в открытую группу. Войти можно в любую из
j, поэтому множитель именноj, а не 1. - Прибавлять после решения участника, а не до. Промежуток проходят те группы, которые открыты на подходе к участнику.
- Считать ответ по всем
j, а не только поj = 0. Незакрытые группы означают, что кто-то так и не получил максимума, — такого разбиения не существует. - Не брать модуль при умножении. Значение
val * jприjдо 200 иvalдо выходит за 32 бита. - Обходить все состояния подряд без пропуска нулей. Формально сложность та же, на практике — разница в разы.
Сложность
Время: в худшем случае — около переходов. Память: на два слоя.
Что дальше
Двадцать три тренировки — от разминки на прямой симуляции до экзамена с задачами CF ~2400. Если пройти маршрут глазами, видно, как менялся не столько список алгоритмов, сколько характер работы.
На «Разогреве» приём был виден в условии почти сразу, и вся задача сводилась к аккуратной реализации. На «Плато» приём приходилось находить: очевидный подход работал, но не укладывался по времени, и главным навыком стало умение доказать, что выбранное состояние или жадный шаг корректны. На «Пике» задачи перестали решаться одним приёмом: типичное решение здесь — два-три известных кирпича, сложенных в правильном порядке, и большая часть работы уходит на то, чтобы понять, какие именно.
Что делать дальше — без цикла и без расписания:
- Регулярные раунды. Обычные соревнования на Codeforces раз в неделю дают то, чего не даёт никакой разбор: решение под таймером, когда неизвестно, какой приём нужен. Это ровно то, что репетировала сегодняшняя сессия.
- Разбор своих неудач, а не чужих решений. Самое ценное после раунда — не прочитать официальный разбор, а понять, в какой момент своя мысль свернула не туда. Обычно это одно конкретное место: неверно оценил ограничения, не проверил крайний случай, поленился доказать жадность.
- Возврат к отложенным задачам. Если по ходу цикла какая-то задача самостоятельной тренировки осталась нерешённой — сейчас удобный момент. За двадцать три сессии инструментов прибавилось, и часть отложенного решается уже без сопротивления.
- Заготовки. Двоичные подъёмы, система непересекающихся множеств, Дейкстра, слияние от меньшего к большему — стоит держать их написанными и уметь воспроизвести по памяти за несколько минут. На контесте это экономит не код, а внимание.
Цикл на этом закончен. Дальше — обычная практика, свои раунды и свои разборы; и если по ходу застрянешь на задаче, разобрать её можно с AI-партнёром на codepal.ru — он не выдаёт готовое решение, а доводит до него вопросами.