OCPC-2019. Разбор задач

Прочитать условия задач OCPC-2019 и отправить на проверку свое решение можно на acm.pp.ua. Регистрация свободная.

A. Ахиллес и черепаха

Поскольку Ахиллес бежит быстрее черепахи, но не должен ее обогнать, нас интересует точка, в которой он ее догонит. Для этого вначале найдем время, за которое Ахиллес догонит черепаху: $t =\frac{ x_t — x_a}{v_a — v_t}$. Теперь найдем координату, в которую в этот момент прибежит Ахиллес (и черепаха тоже): $x = x_a + v_a \cdot t$. Это число и будет ответом.

B. Буковель

Заметим, что расстоянием между двумя вершинами в нашей задаче является упорядоченная пара {количество подъемов, количество спусков}. Значит, сравнивая 2 пути, нужно сравнивать их сначала по первому параметру, затем по второму. Это сравнение уже реализовано в C++ в структуре std::pair<int, int>. Теперь просто построим наш граф, где весом каждого ребра будет пара {1, 0}, если это подъем, и {0, 1}, если это спуск.  Запустим на нем алгоритм Дейкстры — он найдет кратчайшее расстояние. Сложность алгоритма $O(n \cdot \log m)$

C. Свидание

Заметим, что период от одного «любит» до следующего занимает ровно $k+1$ лепесток. При этом номера «счастливых» лепестков такие: $1$, $k+2$, $2 \cdot k + 3$… То есть, эти числа дают остаток $1$ по модулю $k+1$. Все, что нужно, проверить, дает ли такой же остаток число $n$.

D. Дивный новый мир

Отсортируем всех студентов по возрастанию оценок. Понятно, что в оптимальном решении, если остались не отчисленными какие-то студенты, то они в этом массиве будут идти подряд. Теперь переберем все возможные диапазоны из ровно $n-k$ студентов — это минимальное количество, которое можно оставить. Пройдемся по нашему массиву «окном» {i, i + n - k - 1}, где $i$ — текущий кандидат на минимальную границу диапазона оценок, а $i + n — k — 1$ — на максимальную. Будем заменять ответ текущим кандидатом, в случае, если новый диапазон не шире имеющегося. Сложность алгоритма $O(n \cdot \log n)$.

E. Энигма

Название задачи намекает, что надо расшифровать текст. И действительно — условие зашифровано. Поскольку все задачи данного контеста начинаются с «Ни для кого не секрет, что студенты ОНУ…», то у вас уже есть ключ. По нему можно понять, что к первому символу слова всегда прибавляется $1$, ко второму — $2$ и т.д. Это дает возможность написать программу и расшифровать условие, в котором сказано, что шифруются только латинские буквы и цифры, причем большие буквы заменяются большими, строчные — строчными, цифры — цифрами, а слова разделяются пробелами, т.е. пока не встретим пробел или конец строки, слово продолжается. Далее пишем простой конечный автомат и обрабатываем ввод посимвольно. Важно не читать ввод по словам, поскольку между двумя соседними словами может быть несколько пробелов и нужно сохранить их все. Сложность алгоритма $O(n)$

F. Fiasco del ACM

Будем обрабатывать отдельно «победные» контракты, отдельно — «ничейно-победные». Для простоты рассмотрим решение только для контрактов первого типа. Пусть у нас есть отрезок из $k$ побед подряд и мы добавляем еще одну победу. Тогда призовые команды увеличатся на $\sum_{i=1}^{k+1}w_i$, где $w_i$ — сумма денег за контракты длины ровно $i$. Действительно, в $k+1$-й ячейке закончатся победные серии длины $1$, $2$,… $k+1$. Таким образом мы можем посчитать для строк всех длин призовые за них. Обозначим такой массив $s$. Если накапливать сумму $\sum_{i=1}^{k}w_i$, а не считать ее каждый раз заново, то все эти вычисления займут $O(g)$ операций.

Теперь пусть у нас есть ячейка, где мы не выиграли. Например, «WWWDWWWWW». Чтобы посчитать сколько денег мы потеряли в этом матче, найдем максимальную длину побед слева и максимальную длину побед справа. Пусть это числа $w_l$ и $w_r$. В нашем примере $w_l=3$, $w_r=5$. Если заменить теперь ничью на победу, то у нас пропадут 2 выигрышных участка длины $w_l$ и $w_r$, зато появится один большой длины $w_l+w_r+1$. Значит, недополученная в матче сумма равна $s[w_l+w_r+1]-s[w_l]-s[w_r]$. Для получения ответа нужно сложить все такие числа для матчей, где мы не победили.

Теперь осталось научиться быстро считать $w_l$ и $w_r$. Научимся для $w_l$, для $w_r$ это делается абсолютно аналогично. Будем накапливать «хвост» слева в переменной $l$. Если текущий символ ‘W’, увеличим переменную на $1$. Иначе — обнулим. Будем заносить результаты в массив. Сложность всего решения $O(g)$.

G. Героиня оптимизации

Заметим, что для $k$ человек будет совершено $k \cdot (k-1)$ нажатий. Чтобы в памяти осталось ровно 0 нажатий, необходимо и достаточно, чтобы $k \cdot (k-1) \equiv 0 \mod n$. Поскольку $gcd(k, k-1) = 1$, из предыдущего тождества можно вывести систему новых:

$k \equiv 0 \mod a$
$k \equiv 1 \mod b$
$a \cdot b = n$
$gcd(a, b) = 1$

Из китайской теоремы об остатках следует, что для каждой такой системы будет существовать ровно одно решение на диапазоне $[0, n-1]$. Осталось посчитать сколько таких систем существует. А их ровно столько, сколько существует пар делителей $a, b: a \cdot b = n, gcd(a, b) = 1$. Поскольку у $a$ и $b$ нет общих простых делителей, это означает, что если какой-то простой делитель $n$ попал в $a$, то и все остальные такие же делители попадут в $a$. Поэтому количество различных значений $a$ равно количеству способов выбрать набор различных простых делителей числа $n$. Это $2^{cnt}$, где $cnt$ — количество различных простых делителей $n$.

Поскольку 0 и 1 всегда являются корнями нашего уравнения, а ответы просят искать с двойки, то уменьшим мощность множества ответов на 2. Ответом будет $\frac {n — 2 — (2^{cnt} — 2)}{n — 2} = \frac {n — 2^{cnt}}{n — 2}$. Чтобы факторизовать число $n$, достаточно $O(\sqrt{n})$ операций, что и является сложностью алгоритма.

H. Heap Search

В псевдокоде поиска реализован обход «корень-левое-правое» с отсечением в корне: если текущее значение меньше искомого, то идти в потомков не следует, потому что они еще меньше. Переупорядочим числа в куче в порядке этого обхода. Тогда если искомого числа $k$ в куче нет, то наш обход посетит каждую вершину с большими значениями, чем $k$ и из них сделает еще по $2$ захода — влево и вправо. По индукции можно показать, что всего заходов будет $2 \cdot g + 1$, где $g$ — количество чисел в куче, больших, чем $k$.

Если же число $k$ в куче есть, то мы будем идти до его первого вхождения по массиву слева направо, заходя только в большие вершины и их непосредственных потомков. Значит, нам нужно для каждого числа быстро находить на префиксе количество чисел, больших, чем оно. Это можно делать с помощью какой-то структуры данных, например, дерева Фенвика. К ответу будем прибавлять $2 \cdot g$, если $k$ — левый потомок своего родителя или $2 \cdot g + 1$, если правый. Сложность алгоритма $O(n + m \cdot \log n)$, где $m$ — размер диапазона, в котором запускается HeapSearch.

I. Игра белко-бобров

Будем анализировать данную игру с помощью обхода в глубину. Назовем проигрышной позицию, в которой текущий игрок проигрывает, а выигрышной — ту, в которой выигрывает. Если мы в тупике, то мы точно проиграли. Если из нашей позиции есть ход в проигрышную (для соперника, который ходит следующим), то мы выиграли. Если же все ходы в выигрышную (для соперника), то мы проиграем за исключением случая, когда у текущей вершины нечетное количество потомков. Действительно, в выигрышную позицию никто ходить не будет, а значит каждый игрок будет перегрызать ребро графа. Если их нечетное количество, то тот, кто начнет грызть первым, тот и закончит последним. Сложность алгоритма $O(n)$.

J. За двумя зайцами

Пусть мы выбрали какой-то набор матчей, которые мы будем играть, и теперь хотим проверить, можно ли распределить очки между всеми соперниками так, чтобы мы стали чемпионами. Эту задачу будем решать потоками с помощью алгоритма Диница.

Сначала построим граф. Из истока кинем ребра пропускной способности 3 во все вершины, соответствующие еще не сыгранным матчам. Пропускная способность 3 будет означать, что в матче распределяются 3 очка в какой-то пропорции. Заметим, что матчей нашей команды среди них не будет, потому что мы уже выбрали все матчи, которые мы выиграем и проиграем. Из каждого матча кинем по ребру пропускной способности 3 в вершины, соответствующие тем командам, которые играют в этом матче. Это означает, что команда может набрать от 0 до 3 очков в каждом матче. Из каждой команды кинем в сток ребро пропускной способности, равной тому числу очков, которое имеет право набрать эта команда так, чтобы не догнать нашу команду. Поскольку мы уже распределили результаты нашей команды, то ее очки известны и очевидно, сколько имеет право набрать каждый соперник. Построив этот граф и пустив по нему поток Диницем, проверим, смогли ли мы распределить $3 \cdot g$ очков, где $g$ — количество еще не сыгранных матчей.

Теперь мы можем перебрать все возможные наборы сыгранных нашей командой матчей и для каждого проверить потоком, можем ли мы победить при таком выборе или нет. Заметим, что структура графа для всех наборов будет идентичной, различаться будут только пропускные способности ребер от команд к стоку. Всего различных наборов не больше, чем $2^{n-1}$.

Далее заметим, что поскольку нас интересует минимальный по количеству набор игр, можем воспользоваться бинарным поиском. Это дает нам целых 2 преимущества:
1) мы будем проверять множества не всех мощностей, а приблизительно половину из них;
2) если для какой-то мощности множества уже есть подходящий ответ, не нужно проверять все остальные, нам достаточно одного положительного исхода;
Распределим все возможные маски из диапазона $[0; 2^{n-1}-1]$ по векторам в зависимости от количества единичных бит и будем для текущего $k$ проверять только маски с $k$ единичными битами.

Теперь заметим еще одну вещь: для фиксированного $k$, если две маски различаются только двумя битами, то графы идентичны, за исключением пропускной способности двух ребер, соответствующих тем командам, с которыми мы играем в первом наборе и не играем во втором (и наоборот). Значит, если мы пихнули поток по графу, соответствующему какой-то маске, а потом перешли к почти такой же маске, мы имеем право не искать поток заново, а чуть поменять пропускные способности (не более чем на 3), пихнуть лишний поток величиной до 3 обычным DFS-ом назад в исток и затем пустить Диница, который всего лишь достроит недостающий поток.

Чтобы перебирать маски в указанном порядке, будем помещать их в векторы не в порядке возрастания, а в порядке построения кодов Грея. Тогда каждый запуск ДФС+Диница будет иметь сложность $O(n^2)$. Итоговая сложность алгоритма $O(2^{n-2} \cdot n^2)$, что укладывается в 4.5 секунды из 10, данных на задачу.

Related Images:

Тимус-марафон начинается!

Добрый день, уважаемые друзья!

С опозданием, но все же я решил начать тимус-марафон. Причиной для этого послужил тот факт, что у наших команд теоретическая подготовка стала опережать практические навыки. Сбалансировать это соотношение можно только кодингом большого количества не очень сложных задач, а лучший архив для этих целей, чем acm.timus.ru, трудно представить.

Посему настоятельно прошу всех товарищей, которым не безразличны их результаты на предстоящих соревнованиях, зарегистрироваться на acm.timus.ru и сообщить мне свой никнейм сообщением под этим постом. +к карме, если ваш никнем будет содержать подстроку «ONU» (без кавычек).

Правила тимус-марафона:

1) Итоги подводятся каждый месяц, 1-го числа.

2) Всеобщего восхищения удостаиваются два человека: лидер в общем зачете (тот у кого больше всего сданных задач за все время) и лидер месяца (добившийся наибольшего прогресса за месяц).

3) Задачи рекомендую сдавать, отсортировав их по сложности — это вполне подходит для наших целей.

4) Первое подведение итогов назначено на 1 мая 2019.

Да пребудет с вами сила и терпение!

Related Images:

[Базовый олимпиадный курс] Занятие 4. Остовные деревья

Добрый день, уважаемые друзья!

В преддверие 1/8 чемпионата Мира ACM ICPC мы возобновляем наше вещание темой остовных деревьев. Андрей Станкевич милостиво явит свой голос и образ по этой ссылке, а задачи соответствующей темы возникнут здесь.

Для команд, которые желают продолжать тренировать командное взаимодействие (что очень важно, так что рекомендую всем), есть опция собраться в субботу вместе дома или в кафе и решить вот этот командный контест. Его сложность на мой взгляд адекватна сложности задач 1/8.

Related Images:

[Базовый олимпиадный курс] Занятие 3. Поиск путей на графах

Добрый день, дорогие, друзья!

Сегодня предметом нашего изучения станут графы на путях пути на графах. С целью входа в тему Вам следует до 9 января посмотреть лекцию 1, а с целью дальнейшего ее развития до 12 января посмотреть лекцию 2, а также прочитать и понять статью об алгоритме Флойда.  Далее 10 и 13 января пишем по одному тренировочному контесту. Сначала этот, затем этот. Контесты пишем в виртуальном режиме. Разумеется, кто будет готов раньше, может написать их раньше.

И да пребудет с Вами Сила!

Related Images:

[Базовый олимпиадный курс] Занятие 2. Динамическое программирование

Добрый день, уважаемые друзья!

На этот раз нашей темой будет динамическое программирование. Тема очень важная и обширная, поэтому одной лекцией Андрея Станкевича не исчерпывается. Однако, в для начала вам следует посмотреть его лекцию на youtube до воскресенья, а в воскресенье, 2 декабря, решить в виртуальном режиме следующий трехчасовой контест. Убедительная просьба условия до начала виртуального контеста не читать — хороших тренировок не так много и портить их себе не стоит.

Также напоминаю про соревнование Proggy-Baggy в субботу, 1 декабря.

Всем неравнодушным настоятельно советую Educational Codeforces Round 55. Контесты этой серии вообще пропускать не стоит, что напрямую следует из их названия.

Также напоминаю про необходимость добивания задач прошедших соревнований. Пока всего два студента занимаются дорешиванием первого контеста нашей тренировочной серии. Присоединяйтесь!

Related Images:

[Базовый олимпиадный курс] Занятие 1. Сложность алгоритмов, сортировка и поиск

Добрый день, уважаемые друзья!

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

Каждый участник курса должен создать аккаунт на сайте codeforces.com, где будут проходить подготовительные соревнования в рамках специально созданной группы. Подавайте заявки на вступление прямо на сайте!

В качестве первого задания олимпийцам следует принять участие в завтрашнем контесте, прочитать разбор и дорешать нерешенные задачи. Также до воскресенья следует посмотреть две лекции Андрея Станкевича 1, 2, по темам которых в нашей группе будет проведен 3-часовой индивидуальный контест в воскресенье, 25 ноября в 12:00. Разбор задач будет опубликован под этом постом.

В качестве дополнительного задания следует зарегистрировать свою команду на онсайт-контест Proggy-Baggy, намеченный на субботу, 1 декабря, обнаружить его место проведения — офис Data-Art (предположительно в комплексе стадиона «Черноморец») и принять в нем активное участие.

Также следует зарегистрироваться на сайте topcoder.com и участвовать во всех личных контестах, проводимых там и на codeforces по мере их появления.

С 1 декабря объявляется бессрочный timus-марафон — личное соревнование по количеству сданных задач на платформе acm.timus.ru, промежуточные итоги которого будут проводиться ежемесячно. Для участия в нем следует создать аккаунт на тимусе и сообщить мне имя пользователя. Победителям, как водится, почет, слава и уважение.

Возникающие вопросы по задачам и командной тактике можно адресовать мне лично, либо выносить на обсуждение под постами данной тематики.

Related Images:

Медали для всех, даром, и пусть никто не уйдет обыгранным!

Добрый день, уважаемые друзья!

Глядя на нижеследующие ссылки (2011, 2012, 2013, 2014 — следует нажать ссылочку Standings), многие современники не спрашивают у меня, как прийти к подобным результатам. А зря.

К сожалению, сам по себе ритуал еженедельного посещения кружка умелых информационных ручек с опцией прорешивания особо приглянувшихся задач способен перевести Вас лет за 5 из состояния «сдаю на OCPC-18 три задачи» в состояние «сдаю на OCPC-23 семь задач». Цели и задачи факультатива не могут выходить за пределы предоставления Вам базовых олимпиадных знаний и навыков, а также развития способности самостоятельно ориентироваться в джунглях алгоритмов и языков программирования. Поэтому, воспользовавшись лицензией GNU (то есть, бесплатно), хочу распространить методику превращения обычной команды в призера полуфинала чемпионата Мира.

Данный подход зиждится на трех китах и одной черепахе:
1) Практика
2) Дорешивание
3) Теория
4) Собственно, черепаха

1) Практика. Состоит из личной и командной подготовки.
а) Личная тренировка заключается в ежедневном прорешивании по 1-5 задач в течение нескольких лет. Для этого на начальном этапе неплохо подойдет сайт acm.timus.ru, поскольку там, в отличие от е-олимпа, нет тысяч задач уровня а+б и задач с битыми тестами и поломанными чекерами. Через какое-то время после начала Ваших тренировок на тимусе Вы неизбежно столкнетесь с необходимостью овладения новыми алгоритмами, что и требуется. Также крайне рекомендуется решать личные контесты на codeforces.com и topcoder.com каждый раз, когда они начинаются не в 4 утра.
б) Командная тренировка заключается в еженедельном совместном с командой решении 5-часового контеста, соответствующего Вашему текущему уровню. Для этого отлично подойдет субботний кружок. Здесь Вы отрабатываете командную тактику, опыт взаимодействия, одновременное придумывание одной задачи, написание второй и дебага третьей за одним и тем же компьютером. Для этой цели может подойти, например, codeforces.com — там много соревнований самого разного уровня с очень хорошими тестами и возможностью соревноваться с другими участниками этого контеста.
Если закончился контест, а у Вас еще 2 недодебаженные задачи и 3 — с придуманными решениями, но Вы не успеваете их сдать, это верный признак того, что нужно поднажать на личные тренировки с целью довести до блеска известные Вам методы решения.

2) Дорешивание. Задачи, не решенные во время личной или командной тренировки, необходимо дорешать после разбора. На контесте Вы учитесь сдавать задачи, которые Вы умеете решать. На дорешивании — учитесь сдавать задачи, которые решать до сих пор не умели. Это основа любого роста, который состоит из двух частей — вширь и вглубь. Недорешенная задача может преследовать Вас годами из Полуфинала в Полуфинал, пока Вы не избавитесь от нее, как нерешенной, либо она — от Вас, как участника. Второе наступает всегда в силу возрастных ограничений для ACM ICPC.

3) Теория. Однажды наступает такой момент, когда прошла половина контеста, у Вас 5 задач, сданных с плюса, и еще 5, с которыми Вы понятия не имеете, что делать. Скорее всего, это означает, что настало время изучать новые алгоритмы. Для этих целей служит книга Кормена и др. «Алгоритмы. Построение и анализ», а также сайт e-maxx.ru/algo. В первую очередь стоит изучать те алгоритмы, которые чаще других упоминаются на разборе нерешенных Вами задач.

В команде для каждого «разлоченного» алгоритма должен быть один человек, который пишет его за известное время и со строго ограниченным сверху количеством ошибок и еще 2 человека должны иметь представление, что этот алгоритм делает, чтобы узнавать на него задачи. Для более простых/распространенных алгоритмов важно, чтобы их хорошо писали все трое. Чтобы изучить новый алгоритм, нужно прочитать и понять соответствующую статью, написать соответствующий рабочий код, а затем прорешать все доступные по теме задачи (их можно найти на тимусе и кодфорсе). Очень важно не заниматься копипастом, а писать решение каждый раз заново. Более того, желательно одну и ту же задачу сдавать по несколько раз, засекая время. В идеале, Вы должны тратить на реализацию от 5 до 15 минут (и знать заранее, сколько именно).
Где-то раз в пару месяцев нужно делать пересдачу хотя бы одной задачи по каждому изученному алгоритму, дабы навыки находились в тонусе. В целом, это касается более редких алгоритмов, поскольку часто встречающиеся и так будут попадаться Вам каждую неделю.
Кстати, важная составляющая теории — знание того языка программирования, на котором Вы пишете. Для C++ могу порекомендовать книгу Аммерааля по STL — в ней довольно неплохо описаны структуры данных и алгоритмы, реализованные в стандартной библиотеке.

4) Черепаха, на которой все и стоит, — осознание того, что никто не научит Вас решать задачи. Ни ув. преподаватели ОНУ, ни я, ни ув. Андрей Лопатин с ув. Андреем Станкевичем не проделают за Вас когнитивную работу по утрамбовыванию алгоритмов и методов решения задач в Ваш персональный мозг. Только Ваши усилия и Ваша инициатива способны перевести Вашу команду из аутсайдеров в лидеры. И всякое творчество приветствуется: читайте участникам кружка лекции по изученным Вами алгоритмам, придумывайте свои задачи и проводите по ним контесты (благо, сервер у нас есть), ездите на школы и сборы, общайтесь с единомышленниками. Искренняя заинтересованность и упорный труд свернут горы!

Поначалу Вам будет казаться, что Ваш прогресс ничтожен, а монстры вокруг Вас сдают третью задачу, пока Вы только читаете первую. Но вода камень точит и рост незаметен в точке, но весьма ощутим в перспективе.

Завершить хочу мотивирующей ссылкой (ищем слово Odessa). С целью не повторения, но преодоления, разумеется.

FAQ или диалоги с умным человеком.

  1. Q: Ну выиграю я этот ваш Финал и что дальше?
    A: Ничего особенного.

  2. Q: Ваша команда возникла не на пустом месте. В ней было два призера школьных Всеукров по математике, не считая вашего основного кодера, который может работать по 16 часов в сутки. Мы не такие талантливые/трудоспособные.
    A: В команде ONU_Antananarivu не было ни одного призера Всеукра, однако до 2013 года они тренировались где-то на 70-80% мощности от ONU 1 2/3 и смогли решить достаточное для выхода в Финал количество задач. Их остановило только правило «один ВУЗ — одна команда». В то же время, к 2015-му году они почти перестали тренироваться, а победителем стала команда, занявшая 13-е место двумя годами ранее, а в 2012-м вообще не вышедшая в Полуфинал. Тем не менее, все участницы означенной команды сегодня работают в Корпорации Добра.

Related Images: