Привет, Хабр! Я Ани, отвечаю в Ozon Tech за обучение. Сегодня поводом для поста на столь многоуважаемую аудиторию стал разбор задач контеста, который прошёл в рамках отбора участников на курсы Route 256. Для справки: Route 256 — бесплатные курсы для мидл разработчиков от опытных инженеров Ozon Tech. В мае этого года мы запустили пять направлений: Go, QA, C#, iOS, Android. Это наш второй набор, сам проект стартовал осенью прошлого года.  Контест нам заменяет скрининг — мы проверяем технические навыки и опыт работы будущих участников, так как курсы рассчитаны на мидлов. Ранее мы публиковали разбор задач по направлениям Go и QA (раз, два), пришло время поделиться задачами для C#, iOS (Swift) и Android (Kotlin, Java). В этот раз нам посчастливилось провести контест совместно с Codeforces, и, судя по фидбеку участников, задачи зашли на ура. Надеюсь, разбор будет полезен и вам. А если захотите попробовать свои силы и попасть на курс, ищите ссылку на регистрацию ниже.  Дан массив цен за список продуктов, купленных в магазине. Товары с одинаковой стоимостью считаются одинаковыми.  Требуется посчитать, сколько потребуется заплатить суммарно за весь поход в магазин при условии, что в магазине проводится акция — «купи три одинаковых товара и заплати только за два». План решения данной задачи можно сформулировать следующим образом: Первым делом требуется посчитать  количество товаров, имеющих одинаковую цену — обозначим эту величину для некоторой стоимости  за . Сделать это можно несколькими способами, самым простым из них является складывание всех цен товаров в структуру данных, предоставляющую быстрое изменение элемента и последующий проход по полученному контейнеру. Map в Kotlin, Dictionary в Swift, Dictionary в C#. Также есть решение, требующее только константную дополнительную память и работающее быстрее на практике  — отсортировать массив любой сортировкой и затем разбить получившийся массив на блоки одинаковых элементов линейным проходом. Затем нам просто требуется посчитать , это и есть ответ. Асимптотика такого решения —  где  — сумма количество купленных товаров по всем тестам, дополнительной памяти требуется линейное или константное количество в зависимости от реализации.  Задана прямоугольная таблица  из чисел. Требуется обработать  запросов: примените стабильную сортировку по неубыванию к столбцу .  Требуется вывести таблицу после обработки всех запросов. В данной задаче можно было сдать решение в лоб:  Первым делом считаем таблицу — будем хранить ее как массив , состоящий из массивов строк таблицы. Затем будем по порядку считывать запросы. Для каждого запроса (отсортировать столбец ) нам нужно применить стабильную сортировку к массиву  с компаратором, сравнивающим в  и  элемент с индексом , то есть нам просто нужно использовать компаратор . Самое главное в этой задаче — придумать и написать правильно компаратор, а также знать стабильную сортировку из стандартной библиотеки  или уметь писать ее самому. OrderBy в C#, sort() (начиная с Swift 5), sortedWith в Kotlin. В данной задаче проходили решения с любой разумной асимптотикой, но например решение описанное выше работает за  . Дополнительной памяти требуется константное количество.  На вход подаются  логинов, для каждого логина вам нужно сказать, можно ли  его зарегистрировать. Логин должен соответствовать следующим правилам: Логин — это последовательность из латинских букв в любом регистре, цифр и символов «_» или «-» (подчеркивание и дефис). Логин не должен начинаться с дефиса. Логин должен иметь длину от  до  символов. Логины, которые отличаются только регистром, считаются одинаковыми. Решение данной задачи состоит из двух основных идей:  Первая часть решения — проверка логина на первые три правила, это можно сделать либо напрямую проверить каждое из условий без использования регулярных выражений, либо с помощью регулярных выражений.  "[a-zA-Z0-9_][a-zA-Z0-9_-]{1,23}" в языках Kotlin и Java, "^[a-z0-9_]{1}[a-z0-9_\-]{1,23}$" в языке Swift, "^[a-zA-Z0-9_][a-zA-Z0-9-_]{1,23}$" в языке C#. Вторая часть решения — проверка логина, на то что раньше его еще не было. Для этого нам нужно привести логин в общий вид (например в нижний регистр), а затем использовать структуру, которая умеет быстро добавлять элемент и проверять элемент на наличие в множестве.  Set в C#, Set в Kotlin, Set в Swift. Асимптотика такого решения  , дополнительной памяти требуется линейное количество.  Дается журнал звонков — набор записей (имя звонившего, телефон звонившего). Записи даны в хронологическом порядке от наиболее ранней к самой последней. Требуется восстановить для каждого звонившего 5 последних его номеров телефона.  Записи могут встречаться несколько раз, то есть возможна ситуация, когда одна пара (имя звонившего, телефон звонившего) встречается два и более раза во входных данных. Заведем словарь из звонившего в очередь его номеров телефонов. Затем проходимся по набору всех записей. Для каждой записи проверяем, верно ли, что у звонившего в очереди уже есть этот номер телефона. Если это так, то просто удаляем его из очереди и кладем заново в конец очереди. Если же этого номера в очереди нет, то просто добавляем номер в очередь и в случае необходимости (число элементов в очереди больше 5) удаляем элемент из очереди. В конце просто проходимся по словарю в лексикографическом порядке и выводим ответ. Время работы — , где  — сумма количеств записей по всем тестам, такое время работы обуславливается тем, что очередь у нас всегда размера не более 5, а следовательно все операции с ней работают за константное время.  Дополнительная память в данном случае будет линейной.  Есть  купе, пронумерованных от  до , в каждом купе два места, поступают запросы трех видов: Занять место, если оно еще не занято. Освободить место, если оно занято. Занять купе с наименьшим номером, в котором все места свободны. Если в купе освободили все места — то оно также считается свободным. Собственно решим задачу сначала только для обычных мест (без купе). Для решения этой задачи давайте заведем словарь для мест — где для каждого места запомним свободно оно или нет. Для первого запроса мы просто проверим занято место и если нет, пометим занятым. Для второго же запроса мы наоборот проверим занято ли место и если да, пометим его незанятым.  Для обработки купе заведем структуру, которая позволяет удалять произвольный элемент, находить минимальный элемент и вставлять — по таким критериям подходит любое балансирующее дерево или правильная куча, поддерживающая удаление произвольного элемента. SortedSet в C#, sortedSetOf в Kotlin. На запросы третьего типа мы теперь просто ищем минимум в нашей структуре и помечаем места в словаре занятыми. Также теперь нам требуется дополнительно реализовать в запросах первого и второго типа обработку проверки: забронировали купе или его освободили (это можно делать даже в наивную просто проверяя оба места из купе). Для каждого запроса мы требуем  времени (поиск/удаление/вставка в выбранную нами структуру или словарь), итого суммарное время работы — , где  — суммарное количество запросов.  Также нам требуется  дополнительной памяти для хранения всех мест и всех купе.  Есть  модулей, для каждого известны зависимости, которые нужны для его установки. Есть  запросов — на каждой запрос нужно установить какой-то модуль, предварительно поставив все его зависимости, игнорируя уже установленные модули.   Для решения этой задачи нужно было составить для каждого модуля список его зависимостей. Рисунок связей для первого теста из условия: Затем на запрос установки модуля запустим функцию, которая для каждого модуля проверяет были ли установлены все его зависимости и если нет, то поочередно будет запускаться из его зависимостей, до тех пор пока не установит все зависимости модуля. Например, когда на рисунке выше вызывается установка модуля web, то наша функция сначала попытается установить model, установит для нее database и utils, а затем точно также попробует установить commons и получит нужный порядок установки.     Пояснение к рисункам выше: Красным будет выделен элемент, который мы уже установили. Синим — элемент который мы пытались установить, но перешли в его зависимости.  Зеленым — элемент который мы пытаемся установить сейчас.  Не выделены цветом — элементы, которые мы вообще не трогали. На самом деле такое решение называется топологической сортировкой. Каждый модуль мы не можем установить более 1 раза, а также каждую зависимость мы не можем посмотреть более 1 раза на каждый запрос. А следовательно наше решение работает за . Дополнительная память требуется только для рекурсии, которая в худшем случае просто обойдет все модули, а следовательно нам требуется не более  дополнительной памяти.  Есть поле , некоторые клетки свободны, они обозначаются символом «.», а некоторые заняты — «*». Корабль — Г-образная фигура размера 1, 3, 5 или 7 точек.Ниже можно видеть все примеры кораблей: Корабли можно вращать Требуется проверить может ли поле, данное в тесте, быть полем для морского боя, при условии правильного размещения кораблей (корабли не должны прикасаться ни по горизонтали, ни по вертикали, ни по диагонали). Данная задача — задача в основном на реализацию, так как требуется просто реализовать, то что спрашивается в условии — то есть просто проходить по полю и явно убирать корабли, если они размещены правильно. Но есть несколько способов упростить себе решение:  Первым делом для проверки, правильно ли размещён корабль, можно использовать шаблоны: сделать 4 шаблона для каждого из кораблей. Для проверки «верно ли, что в этой позиции находится корабль» — нам просто нужно проверять, подходит ли кусочек поля под шаблон. При сравнении по шаблону нам нужно считать, что элементы за границами поля при сравнении считаются пустыми ячейками. 2. Так как корабли можно вращать, то нужно было сделать не 4 шаблона, а целых 16, но так как в шаблонах было бы легко запутаться, то можно просто написать функцию rotate, которая будет вращать шаблон на 90 градусов и таким образом получить все нужные нам шаблоны.   Начнем с клетки (0, 0) — там будет стоять звёздочка, далее:  Будем по очереди проверять все шаблоны. Когда дойдем до шаблона размера 2, увидим, что он нам подходит, так как клетки (-1, -1), (-1, 0), (-1, 1), (-1, 2), (0, -1), (0, -2) находятся за границей и считаются в нашем алгоритме свободными.  Удалим корабль с центром в точке (0, 0). Продолжим алгоритм.   Затем проверим клетку (0, 3) — найдём подходящий шаблон и удалим корабль, также проверим клетку (3, 0) — в результате получим пустое поле. Так что изначальное поле было полем для морского боя. Так как в каждую клеточку поля мы пытаемся подставить корабль, напрямую проверяя кусок поля с шаблоном, то данное решение будет требовать , где  — размер шаблона,  — количество шаблонов. Так как  и  — константы, то в итоге мы получили решение за . Дополнительной памяти же нам достаточно , то есть константное число дополнительной памяти.  Такими были задачи для поступления на курсы: Продвинутая разработка микросервисов на C# Продвинутая iOS-разработка: SwiftUI и Backend Driven UI Продвинутая Android-разработка: Compose, Kotlin Flow, MultiModule Если вы нашли более изящное решение — делитесь в комментариях:)