Финальный раунд
Задача А. Терпение.
Идея: Артем Васильев.
Реализация: Артем Васильев.
Разбор: Артем Васильев.
В условии была описана игра для двух игроков. Состояние игры описывалось одним числом x, и игроки по очереди могли сделать один из двух ходов: заменить x на x+1, либо на 2x. Тот игрок, который первым написал число, большее, чем n, проиграл, Следовало найти k по возрастанию число x, такое, что первый игрок имеет выигрышную стратегию, если начальное число равнялось x.
Поскольку ограничения на n были достаточно большие, надо было понять структуру чисел, являющихся выигрышными позициями для игрока, который начинает из такого состояния. Мы покажем, что это множество представляется в виде объединения O(log n) отрезков подряд идущих чисел, в каждом из которых либо каждое число, либо каждое второе число это выигрышная позиция для первого игрока.
Будем строить это множество рекурсивно. Пусть процедура F(n) возвращает множество всех таких отрезков, и при этом выполняется инвариант: игрок, сделавший ход в число, большее n, проигрывает.
Рассмотрим два случая. Пусть n нечетно. Тогда любое четное число от 2 до n-1 это выигрышная позиция, а любое нечетное — проигрышная. Покажем, что первый игрок всегда может сделать так, что он всегда ходит из четной позиции, а его противник — из нечетной. Действительно, если первый игрок переводит x в число x+1, то второй игрок может перевести x+1 только в четное число. Осталось лишь заметить, что число n нечетно, и поэтому первый игрок всегда имеет ход, который не приводит к его поражению. Продолжая по индукции, получаем, что если первый игрок начинает с четного числа, то у него есть выигрышная стратегия. В случае, когда первый игрок начинает с нечетного числа, второй игрок использует ту же стратегию и выигрывает.
Теперь пусть n четно. Все нечетные позиции, большие n/2 выигрышны, поскольку удвоение такого числа сразу приводит к поражению, а в таком случае, выигрышная ли позиция, определяется четностью числа оставшихся ходов. Заметим также, что позиция n/2 выигрышна, поскольку из n/2 можно получить n, и другой игрок обязательно превышает n своим следующим ходом.
Рассмотрим такое число l, что l больше, чем n/2, и позиция l является проигрышной. В зависимости от четности числа n/2, l равно либо n/2 + 1, либо n/2 + 2. Теперь рассмотрим все числа от l/2 до n/2 включительно. Все эти позиции будут выигрышными, поскольку если удвоить любое из этих чисел, получится число, большее n/2 и четное, а такая позиция проигрышна. Получили два отрезка, на первом из которых выигрышна каждая позиция, а на втором — каждая вторая.
Заметим, что остальные отрезки можно получить, вызвав F(l/2 - 1). Разбором случаев можно показать, что l/2 - 1 всегда равно n/4, округленному вниз. Убедимся в том, что требуемое свойство выполняется. Пусть x ≤ l/2 - 1. Тогда 2x ≤ l - 2 ≤ n/2, то есть все позиции, большие l/2 - 1, и в которые возможен ход из позиции, не большей l/2 - 1 являются выигрышными, и потому такой ход приведет к проигрышу.
При каждом рекурсивном вызове n уменьшается как минимум в 4 раза, поэтому программа работает за O(log n). После этого решить изначальную задачу становится просто. Так как все эти отрезки не пересекаются, можно пройтись по ним в возрастающем порядке и определять, попадает ли ответ в текущий отрезок. Если текущее k больше количества чисел на данном отрезке, то вычтем это количество из k и перейдем к следующему отрезку. Иначе мы можем получить k нужное нам число на этом отрезке и вывести его. Если такого отрезка не нашлось, следует вывести -1.
Задача B. Белоснежка и n гномов.
Идея: Виталик Аксёнов.
Реализация: Виталик Аксёнов, Николай Ведерников.
Разбор: Виталик Аксёнов.
Построим ориентированный граф на n вершинах. Занумеруем их от 1 до n. Из вершины i будет выходить одно ребро в вершину ai. Несложно понять, что номера вершин, которые представляют какой-нибудь цикл в графе, будет удовлетворять условию задачи.
Когда мы свели нашу задачу к поиску цикла в таком графе, она решается следующим алгоритмом: выберем любую вершину и начнём идти из неё по рёбрам, заметим, что из каждой вершины выходит ровно одно ребро, поэтому альтернативы у нас нет. Заметим, что так как количество вершин конечно, мы придём в посещённую. Тем самым мы нашли цикл.
Задача C. Быстрая-пребыстрая сортировка.
Идея: Николай Ведерников
Реализация: Андрей Комаров.
Разбор: Андрей Комаров.
В задаче была дана некоторая модификация алгоритма быстрой сортировки и требовалось для заданной перестановки найти наименьшее необходимое для её сортировки количество вызовов функции pick.
Обозначим за left(a, x) то, что в коде из условия называлось aLess — элементы массива a, строго меньшие x. Аналогично введём right(a, x) — элементы, бо́льшие x. Докажем несколько утверждений про left и right.
Если x ∈ a и a — множество подряд идущих натуральных чисел, то left(a, x) и right(a, x) — тоже множества подряд идущих чисел. Это верно, так как если a — множество целых чисел от L до R, то left(a, x) — множество целых чисел от L до x-1, а right(a, x) — множество целых чисел от x+1 до R.
Посмотрим на заданный массив a. Обозначим за w[i] позицию элемента i в a: w[a[i]] = i. Разобьём массив на цепочки увеличивающихся на 1 чисел так, чтобы элементы внутри одной цепочки в массиве шли подряд: добавим 1 в первую цепочку, далее, если w[1] < w[2], то добавим в неё 2, и т.д. Например, chains([1, 2, 6, 7, 5, 3, 4, 8, 9]) = [ [1, 2, 3, 4], [5], [6, 7, 8, 9] ]. Посмотрим, что произойдёт с этими цепочками после разбиения по x. Разобьём на цепочки left(a, x) и right(a, x). Изучим left. right рассматривается аналогично. Рассмотрим взаимное расположение цепочки [c..d] ∈ chains(a) и x.
- x > d. В этом случае, [c..d] ∈ chains(left(a, x))
- c < x. В этом случае, [c..d] ∉ chains(left(a, x)). А также, ни один элемент из [c..d] не представлен ни в одной цепочке из chains(left(a, x))
- x = d, x ≠ c. Здесь [c..d-1] ∈ chains(left(a, x))
- x = c, x ≠ d. Аналогично второму случаю, [c..d] ∉ chains(left(a, x))
- Наконец, последний случай: x = c = d. В этом случае ничто из цепочки [c..d] (которая, на самом деле, [c]) не попадает ни в chains(left(a, x)), ни в chains(right(a, x))
Посмотрев на эти случаи, можно заметить следующее: если два элемента когда-то были в одной цепочке, то они никогда не окажутся в разных цепочках одного сортируемого массива. Однако, они могут оказаться в разных цепочках, но в рамках сортировки разных массивов. Например, a = [5, 2, 3, 4, 1], chains(a) = [ [1], [2, 3, 4], [5] ], x = 3, left(a, 3) = [2, 1], chains(left(a, 3)) = [ [1], [2] ], right(a, 3) = [5, 4], chains(right(a, 3)) = [ [4], [5] ]. Видно, что числа 2 и 4, бывшие сначала в одной цепочке [2, 3, 4], разошлись по разным, но, при этом, и сортируемые массивы разные ([2, 1] и [5, 4]).
Из этого можно сделать первое важное наблюдение: брать в качестве разделяющего элемент, не являющийся крайним в цепочке, не имеет смысла. Если взять вместо взятого из середины элемента какой-нибудь из крайних, ответ будет не хуже (так как элементы одной цепочки не могут стать элементами разных).
Опишем неформально то, чего хочется добиться. Имеем массив a. Построим по нему chains(a). В полученном наборе в качестве разделителя выгодно выбирать только концы цепочек. Для того, чтобы получить отсортированный массив, необходимо устранить все зазоры между цепочками. Если изначально были цепочки [c..d] и [d+1..e], а в качестве разделяющего был выбран элемент d, то зазор (d..d+1) перестаёт нас волновать, так как все элементы левее него попали в один массив, а все правее — в другой. Таким образом, устранился один зазор, а всего их надо устранить #chains(a) - 1, где # обозначает размер.
Пусть была не первая и не последняя цепочка длины 1: [c..d-1], [d], [d+1..e]. Возьмём d в качестве разделяющего элемента. Тогда, так как он крайний слева для цепочки [d], то устранится зазор (d-1..d). Так как он же и крайний справа для этой же цепочки, устранится также и зазор (d..d+1). Таким образом, выбор в качестве разделяющего элемента единственного элемента некрайней цепочки устранит сразу два зазора.
Пускай есть какой-то оптимальный ответ и в нём фигурировали разделяющие элементы d1, d2, ..., dk. Каждый из них устранял сколько-то зазоров. Сделаем следующее: выберем в качестве первого разделяющего элемента min {d1, ..., dk}, массив разобьётся на отсортированный левый и ещё не отсортированный правый куски. Выберем среди di второй минимум и сделаем его разделяющим элементом для правого куска. Будем повторять, пока di не кончатся. Несложно убедиться, что при такой стратегии выбора разделяющих элементов, каждый устранит то же количество зазоров, что и во взятом ранее оптимальном выборе. Таким образом, можно решать задачу с помощью динамического программирования, считая минимально необходимое число действий для того, чтобы объединить несколько первых цепочек в одну. Ответом будет минимальное число действий, необходимых для объединения всех цепочек в одну.
Задача D. Робот.
Идея: Борис Минаев.
Реализация: Борис Минаев.
Разбор: Борис Минаев.
В данной задаче необходимо было найти матожидание длины кратчайшего пути из одного угла поля в другой. При этом некоторые клетки поля были покрашены в белый цвет, и при попадении робота на такую клетку он переставлялся в случайную белую клетку поля.
Разберемся подробнее, как устроена оптимальная стратегия выбора пути. Для каждой клетки поля можно посчитать матожидание количества ходов, необходимое для того, чтобы попасть в правую нижнюю клетку. При этом ответ для задачи будет соответствовать расстоянию, которое записано в левой верхней клетке. Как же вычислить эти значения? Рассмотрим конкретную клетку. Рассмотрим всех ее соседей. Если сосед покрашен в черный цвет, то ответ для клетки будет как минимум (1 + значение, которое записано в соседней клетке). Если же у клетки есть белый сосед, то ответ для нее равен как минимум (1 + среднее арифметическое ответов для всех белых клеток).
Понятно, что если робот два раза попадает в одну и ту же клетку, то, существует оптимальная стратегия, в которой следующий ход в обоих случаях одинаковый. Если это не так, то в каком-то из ходов робот поступил не правильно и пошел в клетку с большим матожиданием количества ходов до конца, а, значит, стратегию можно улучшить. Есть два принципиально разных действия, которые может сделать робот, находясь в некоторой клетке. Он может пойти сразу в правый нижний угол по черным клеткам (если такой путь существует), а может пойти в некоторую белую клетку. Очевидно, что среди всех белых клеток, в которые можно пойти следует выбрать ближайшую.
Посчитаем для каждой клетки расстояние до правой нижней по черным клеткам и расстояние до ближайшей белой. Заметим, что для любой белой клетки расстояние до ближайшей белой не больше двух. Теперь разобъем все белые клетки на два множества — те, из которых робот сразу должен идти в правую нижнюю клетку и те, из которых робот должен идти в соседнюю белую. Когда такое разбиение призведено, ответ вычислить не сложно. Записав рекурентное соотношение для среднего ответа для всех белых клеток, найдем его. Это будет некоторая дробь со знаменателем не больше количества белых клеток. Теперь ответ это min(расстояние по черным клеткам до правой нижней, расстояние до ближайшей белой + средний ответ для всех белых).
Осталось научится находить правильное разбиение белых клеток на множества. Отсортируем все белые клетки по критерию (расстояние до правой нижней по черным - расстояние до ближайшей белой). Утверждается, что оптимальное разбиение следующее — из всех клеток некоторого префикса отсортированного массива необходимо идти сразу в правую нижнюю клетку, а из остальных в ближайшую белую. Этот факт не сложно доказывается методом от противного (пусть оптимальное разбиение имеет другой вид, рассмотрим средний ответ для белых клеток, увидим, что некоторые клетки можно переместить из одного множества в другое не ухудшив ответ, но приведя разбиение к нужному виду).
В итоге решение сводится к следующему. Посчитаем расстояния, которые обсуждались ранее, для всех клеток. Отсортируем белые клетки. Переберем разбиение на множества, каждый раз пересчитывая средний ответ для белых клеток за O(1). Ответ на задачу — минимум из расстояния по черным клеткам и расстояния до ближайшей белой клетки + средний ответ для белых клеток. Если сортировать белые клетки подсчетом, то общая сложность решения будет O(общее количество клеток).
Задача E. Вакцинация.
Идея: Виталий Аксенов.
Реализация: Борис Минаев.
Разбор: Борис Минаев.
В данной задаче были заданы правила, по которым изменяется некоторая величина. Необходимо было найти значения величины при разных начальных условиях. Сами изменения происходили следующим образом. Каждую единицу времени величина уменьшалась на некоторое число. Если величина становилась отрицательной, то она приравнивалась к нулю. В некоторые моменты времени к этой величине прибавляли какие-то значения.
Каждый запрос характеризовался тройкой чисел — начало и конец промежутка наблюдения, а также начальное значение величины. Рассмотрим функцию ответа на запрос, зависящую от третьего параметра. Утверждается, что она всегда имеет следующий вид: до некоторого значения ответ является константой, а, если величина больше этого значения на x, то ответ будет эта константа + x. Это несложно доказать по индукции по количеству "добавлений", которые есть на отрезке. База индукции — на отрезке значения только уменьшаются каждую единицу времени на константу. Справедливость утверждения очевидна. Пусть мы рассмотрели первые k добавлений, а теперь добавляем еще одно, которое происходит позже уже имеющихся. Необходимо разобрать два случая — когда минимальное значение, с которым мы можем прийти к последнему "добавлению" равно нулю, и когда оно больше нуля. В обеих случаях конечная функция имеет такой же вид.
Зная факт, который описан выше, правильное решение придумать не сложно. Построим дерево отрезков на всех "добавлениях". В вершинах дерева будем хранить в каком-нибудь виде функцию ответа для промежутка. Чтобы соединить два промежутка, необходимо аккуратно рассмотреть два случая, о которых говорилось выше. Для обработки очередного запроса возьмем из дерева отрезков соответствующий промежуток, учтем, что концы промежутка могут не совпадать точно с концами запроса, вычислим значение функции. Общая сложность решения получается O(n + m·log(n)), где n — количество "добавлений", а m — количество запросов.
Задача F. Расписание перелетов.
Идея: Виталий Аксенов.
Реализация: Павел Кротков.
Разбор: Павел Кротков.
Данная задача задумывалась как самая технически сложная задача финала. К сожалению, полное и подробное описание ее решения окажется очень громоздким, поэтому мы приведем основные идеи, ведущие к решению задачи, опустив детали конкретной реализации.
Заметим, что описанный в условии граф является вершинным кактусом, и это означает, что любая вершина в этом графе лежит не более чем на одном цикле. Научимся решать сначала более простой случай, в котором данный граф является деревом, после чего несколько усложним наш алгоритм и решим всю задачу.
Подвесим дерево за любую вершину. Рассмотрим все пути, идущие в этом дереве строго сверху вниз, число ребер в которых равно некоторой степени двойки. Для каждого такого пути посчитаем две величины:
- продолжительности пути из верхней вершины в нижнюю при условии, что на ожидание самолета в верхней вершине тратится 0 часов (пассажир прибыл ровно к вылету самолета)
- продолжительности пути из нижней вершины в верхнюю при условии, что на ожидание самолета в нижней вершине тратится 0 часов
Заметим, что при посчитанных величинах для путей из 2k ребер, величины для путей длины 2k + 1 считаются за O(1) каждая, так как каждый путь из 2k + 1 ребер разбивается на два пути из 2k ребер каждый, длину которых в часах мы уже выяснили, а объединить их можно, использовав несложную формулу подсчета длительности ожидания в городе, стоящем посередине пути.
Любой путь между двумя вершинами в дереве можно разбить не более чем на два пути, идущих из наименьшего общего предка этих вершин в каждую из них, а каждый из этих путей, в свою очередь, разбивается на O(logn) путей, продолжительности которых в часах мы уже предпосчитали. Таким образом, мы научились обрабатывать запросы, поступающие в дерево.
Посмотрим теперь, как необходимо модифицировать решение, чтобы оно решало оригинальную задачу. Во-первых, заметим, что из-за подвешивания циклов понятие предка в дереве несколько стирается, вследствие чего стирается понятие пути, идущего снизу вверх. Решим эту проблему следующим образом: для любой вершины, находящейся на цикле, ее предком в дереве будет вершина, за которую соответствующий цикл подвешен. Предок же той вершины, за которую весь цикл подвешен, определяется из очевидных соображений.
Но теперь, как несложно заметить, величины, которые мы считали для дерева, несколько утратили свой смысл: из вершины, лежащей на цикле, в вершину, за которую цикл подвешен, существуют два возможных пути, и однозначно определить более выгодный путь заранее невозможно. Поэтому, новый набор величин, которые мы считаем для нового дерева, можно сформулировать так:
- продолжительности пути из верхней вершины в нижнюю при условии, что на ожидание самолета в верхней вершине тратится 0 часов (пассажир прибыл ровно к вылету самолета) и в случае, если верхняя вершина лежит на цикле, цикл будет обходиться против часовой стрелки
- продолжительности пути из верхней вершины в нижнюю при условии, что на ожидание самолета в верхней вершине тратится 0 часов и в случае, если верхняя вершина лежит на цикле, цикл будет обходиться по часовой стрелке
- продолжительности пути из нижней вершины в верхнюю при условии, что на ожидание самолета в нижней вершине тратится 0 часов и в случае, если нижняя вершина лежит на цикле, цикл будет обходиться против часовой стрелки
- продолжительности пути из нижней вершины в верхнюю при условии, что на ожидание самолета в нижней вершине тратится 0 часов и в случае, если нижняя вершина лежит на цикле, цикл будет обходиться по часовой стрелке
Между делом заметим, что для путей, концы которых не лежат на циклах, некоторые величины будут совпадать.
Предпосчитав описанные выше значения, мы можем приступать к решению задачи. Заметим, что путь между двумя вершинами разбивается на два пути (возможно, пустых) снизу вверх и сверху вниз, между ними же, возможно, придется пройти по циклу, выбрав направление его обхода.
В качестве последнего уточнения заметим, что считать длину прохода по циклу от вершины к вершине в ту или иную сторону нам поможет точно такой же трюк - подсчет продолжительности прохода по путям, состоящим из числа ребер, равного некоторой степени двойки.