Маски, картины, тайные покупатели и анализ продаж: разбираем решения задач для Go-разработчиков
3 апреля на платформе All Cups прошло отборочное соревнование на курс «Продвинутая разработка микросервисов на Go» — это уже второй поток бесплатных курсов для разработчиков от Ozon Tech. Программа предназначена для мидлов, поэтому нужно было придумать задания и провести контест, чтобы отобрать релевантных участников.
Методисты All Cups совместно с организаторами разработали алгоритмические задачи, добавив актуального контекста. Здесь много любителей головоломок: предлагаем попробовать свои силы в задачах и сравнить с решениями.
Задача «Маски»
Никита собирается открывать свой пункт выдачи заказов (ПВЗ). Для обеспечения безопасности посетителей Никита решил обеспечить ПВЗ одноразовыми медицинскими масками, которые будут поставляться каждый месяц в количестве штук. Местные поставщики продают маски по цене 0,55 рублей за одну штуку, но можно сэкономить, купив упаковку из 24 масок за 11 рублей или коробку из 16 упаковок за 160 рублей. Помогите Никите, определив, сколько коробок, упаковок и отдельных масок он должен купить, чтобы заплатить как можно меньше денег.
Примечание: Никита готов купить больше масок, чем ему нужно, если это позволит сэкономить.
Формат входных данных
В одной строке вводится одно целое число .
Формат выходных данных
Требуется вывести три числа в одну строку через пробел — количество отдельных масок, упаковок и коробок, которые надо купить.
- Сначала купить как можно больше коробок .
- Затем — как можно больше упаковок .
- И остаток M докупить поштучно.
Однако такой подход не всегда самый выгодный, потому что можно купить лишние маски, если это позволит сэкономить. К тому же при таком подходе количества коробок и упаковок отличаются от оптимальных максимум на единицу. Давайте теперь проверим неравенство, при котором выгоднее купить упаковку, чем докупать поштучно: и — количества масок и упаковок соответственно, обновлённые с учётом покупки ещё одной упаковки.
Остаётся лишь представить эти формулы в коде.
Задача «Склады»
Компания-застройщик занимается строительством недвижимости промышленного назначения, а именно складских помещений. Каждый квадратный метр площади стоит рублей. Компания имеет достаточный запас оборотных средств, чтобы продавать построенные помещения не сразу после завершения строительства, а в тот момент, когда ей это выгодно. Рынок недвижимости очень динамичный, поэтому цена квадратного метра меняется каждый день. Аналитики застройщика смогли точно спрогнозировать цену на ближайшие дней (пронумеруем дни в хронологическом порядке от до ). Теперь требуется определить, в какие из этих дней нужно продавать, чтобы по истечении дней заработать как можно больше денег. Известно, что стройка новых площадей происходит с равномерной скоростью кв. метров в сутки. А к нулевому дню объем построенной площади составлял кв. метров, при том что .
Формат входных данных
В первой строке вводится одно целое число — количество дней. Во второй строке вводится последовательность из целых положительных чисел — цена квадратного метра складской площади в каждый из дней.
Формат выходных данных
Вывести одно число — максимальное количество денег, которое может заработать компания-застройщик.
Задача «Картины»
Максим решил заняться продажей картин на NFT-площадках, а для этого нужно придумать что-то свое и оригинальное. Максиму очень нравится, как выглядят цифры на черном фоне. Нужно написать программу, которая будет рисовать квадрат, состоящий из целых чисел и выводить информацию о его размере.
Формат входных данных
В одной строке через пробел вводится набор чисел или символов.
Формат выходных данных
В первой строке выводится сообщение «Квадрат со стороной N». В следующих строках требуется вывести квадрат, сформированный из набора чисел или символов, длина стороны — длина введённого массива.
- Для начала прочитаем массив из входных данных и узнаем его длину:
read line array=($line) len=${#array[@]}
- Затем выведем фразу про «квадрат»:
echo "Квадрат со стороной $len"
- И в завершение воспользуемся циклом со счётчиком и отобразим исходный массив раз:
for (( i = 0; i < $len; i++ )) do echo $line done
Задача «Продажи»
В базе данных есть таблица Invoice
следующего вида:
Напишите запрос, который покажет общие продажи по всем странам, отсортированные в порядке возрастания. На выходе в первой колонке должно быть название страны, а во второй показатель общих продаж.
На чемпионате во фразе «отсортированные в порядке» было некорректное склонение, что заставляло пользователей думать, что сортировать необходимо по странам. Это было оперативно исправлено и сейчас формулировка корректна.
Примечание: для решения задачи используется база данных Chinook Database в формате SQLite — см. файл Chinook_Sqlite.sqlite.
GROUP BY
, а второе — командой ORDER BY
. Тогда запрос, который покажет общие продажи по всем странам, отсортированные в порядке возрастания, может выглядеть так: SELECT
BillingCountry,
SUM(total)
FROM
Invoice
GROUP BY BillingCountry
ORDER BY SUM(total);
Функция
SUM
в рамках GROUP BY
посчитает именно то, что мы хотим — сумму всех продаж для каждой страны.Задача «Тайные покупатели»
Паша и Саша проживают в большом подмосковном городе, в котором районов. Не так давно два друга решили стать тайными покупателями в Ozon. Они решили подойти к делу очень ответственно, поэтому очень хотят обойти все пункты выдачи, посовещаться и выбрать самый лучший. В каждом районе города есть только одна точка выдачи. Внимательно изучив карту города, ребята поняли, что проверить все пункты у них физически не получится, уж очень далеко им придётся ходить, поэтому они решили ограничиться количеством путешествий, равным количеству всех возможных вариантов обхода, взятому по модулю , а также решили позвать на помощь друзей, которые будут параллельно обходить точки. При этом каждому из друзей выделено разное подмножество пунктов выдачи: , , …, . Найдите количество вариантов обходов всех пунктов выдачи в произвольном порядке.
Примечание: Паша и Саша ходят вместе, а друзья поодиночке. Например, для есть 6 вариантов обходов (, , , , , ), по модулю 6 это 0. А для — 24 варианта обходов (, , …, ). 24 по модулю 7 будет 3.
Формат входных данных
В первой строке вводится число . Далее на вход поступает строк, каждая из которых содержит два целых числа и (, ).
Участников соревнования могло ввести в заблуждение неопределенность в ограничении . Во время соревнования задание было дополнено, сейчас вам доступен финальный текст.
Формат выходных данных
Вывести чисел — количество обходов для каждого участника.
Задача «Маршруты»
Кирилл работает аналитиком в Ozon, и недавно ему в руки попал отчёт, из которого он понял, что время доставки товаров в пункты выдачи можно значительно сократить. Он заметил, что пункты выдачи в городе образуют выпуклый многоугольник с количеством вершин, равным , и располагаются на его вершинах, где одна вершина = один пункт выдачи. Кирилл решил воспользоваться всеми прелестями современных технологий, он хочет проложить воздушные пути между пунктами выдачи, по которым будут перемещаться курьеры на грузоподъёмных реактивных ранцах.
Можно выбрать какое-то число , при котором каждый пункт выдачи будет соединен с соседними пунктами выдачи слева и справа. Нужно найти минимальное , чтобы кратчайшее расстояние между любыми двумя пунктами выдачи было меньше или равно . Расстояние между пунктами выдачи примерно одинаковое, поэтому расстояние будет измеряться в количестве переходов, которые нужно сделать, чтобы попасть из одного пункта выдачи в другой.
Пояснение к примеру
- Если при выбрать (соединив каждую вершину только с одним ближайшим соседом слева и справа), то минимальное расстояние, например от узла до узла будет равно 3. Но выбрать (соединив каждую вершину с двумя ближайшими соседями с каждой стороны), то минимальное расстояние между любой парой вершин получается равным 2. Это удовлетворяет условию , значит это верное решение.
- Для треугольника () все вершины являются соседними друг другу, значит единственно возможным решением является , что удовлетворяет условию .
Формат входных данных
В первую строку вводится одно целое число — количество наборов входных данных. Для каждого набора входных данных в строку через пробел вводится два целых числа и . Ограничения: , .
Формат выходных данных
Для каждого набора чисел выведите минимальное , удовлетворяющее условию.
А что будет, если не делится на без остатка? В этом случае для прохождения оставшегося пути нам понадобится ещё один переход. С этого момента есть способа решения: двоичный поиск по ответу или доработка формулы для получения минимального . Возьмём максимум полученного значения и единицы, так как нам в любом случае нужно соединять точку хотя бы с одним соседом. Тогда для формулы мы можем получить , например при и .
Задача «Даты»
Напиши скрипт, который будет получать на вход stdin
два параметра d1
и d2
в формате YYYY-MM-DD, и будет считать разницу между этими датами в днях. Скрипт проверяется на Bash 5.1.4 (запуск под Ubuntu 20.04).
Формат входных данных
Две даты через пробел в формате YYYY-MM-DD.
Формат выходных данных
Одно целое число — разница в днях.
date
, которая позволяет получить из строки дату и преобразить её в формат unix time. Давайте так и поступим, затем найдём разность и переведём её в дни: - Считываем даты из входного потока:
read s1 s2 d1=`date -d "$s1" "+%Y-%m-%d"` d2=`date -d "$s2" "+%Y-%m-%d"`
- Переводим даты в unix time:
ut1=`date -d "$d1" +%s` ut2=`date -d "$d2" +%s`
- Считаем разность секунд и переводим в дни:
diff=$(($ut1 - $ut2)) diff_days=$(($diff / (60 * 60 * 24)))
- Выводим абсолютное значение разности (можно было бы написать
if
, но такой «чит» выглядит лаконичнее: он переводит значение в строку и удаляет лидирующий минус, если он есть):echo ${diff_days#-}
Бонус: уже после написания разбора обнаружилось два забавных факта:
- Во всех тестах вторая дата идёт хронологически позже первой.
- Формат даты в условии не надо дополнительно парсить, а можно сразу переводить в unix time.
Таким образом, немного упрощённое решение выглядит так:
read d1 d2
ut1=`date -d $d1 +%s`
ut2=`date -d $d2 +%s`
diff=$(($ut2 - $ut1))
diff_days=$(($diff / (60 * 60 * 24)))
echo $diff_days
Задача «Анализ продаж»
В базе данных есть таблицы Invoice
, Customer
, Employee
следующего вида:
Напишите запрос, который будет искать трех продавцов на маркетплейсе, совершивших больше всего продаж, начиная с 2010 года. На выходе в первой колонке должны быть имя и фамилия продавца, а во второй количество их продаж, отсортированное в порядке убывания.
Примечание: для решения задачи используется база данных Chinook Database в формате SQLite — см. файл Chinook_Sqlite.sqlite. Также во время чемпионата у участников возникал вопрос относительно имени и фамилии в первой колонке таблицы. Поясним, что они должны быть через пробел.
Invoice
и Сustomer
можно объединить по CustomerId
, а Сustomer
и Employee
по Customer.SupportRepId = Employee.EmployeeId
.Теперь отфильтруем все продажи начиная с 2010 года. Для этого можно можно преобразовать даты с помощью CAST
и сравнить, либо воспользоваться выражением LIKE «201%»
, тестовые данные допускают оба варианта.
Далее остается лишь соединить фамилию и имя и выполнить простейшую агрегацию. Пример финального запроса:
SELECT
Employee.FirstName || ' ' || Employee.LastName as Name,
COUNT(Customer.SupportRepId) AS Total
FROM
Invoice
INNER JOIN
Customer
ON
Invoice.CustomerId = Customer.CustomerId
INNER JOIN
Employee
ON
Customer.SupportRepId = Employee.EmployeeId
WHERE
Invoice.InvoiceDate LIKE "201%"
GROUP BY Name
ORDER BY Total DESC
LIMIT 3
Задача «Треки»
В базе данных есть таблицы Invoice
, InvoiceLine
, Track
следующего вида:
Напишите запрос, который составит рейтинг треков по их продаваемости, начиная с 2010 года. На выходе должны получиться две колонки. В первой колонке должны быть Id
трека, отсортированные в порядке возрастания, а во второй колонке — количество проданных копий трека, отсортированных в порядке убывания.
Примечание: для решения задачи используется база данных Chinook Database в формате SQLite — см. файл Chinook_Sqlite.sqlite.
INNER JOIN
. Облегчает задачу то, что у нас есть общее поле TrackId
в таблицах Track
и InvoiceLine
, а также общее поле InvoiceId
в таблицах InvoiceLine
и Invoice
. Затем отфильтруем все продажи начиная с 2010 года. Как и в предыдущей задаче, для этого есть два способа: применить к датам CAST
и сравнить, или выполнить выражение LIKE «201%»
.Остаётся подсчитать сумму продаж по каждому треку: сгруппируем данные, применим функцию SUM
к значению InvoiceLine.Quantity
, а затем отсортируем значения в получившейся таблице. Вариант финального запроса:
SELECT
Track.TrackId AS TrackId,
SUM(InvoiceLine.Quantity) AS Total
FROM
Track
INNER JOIN
InvoiceLine
ON
Track.TrackId = InvoiceLine.TrackId
INNER JOIN
Invoice
ON
InvoiceLine.InvoiceId = Invoice.InvoiceId
WHERE
Invoice.InvoiceDate LIKE '201%'
GROUP BY Track.TrackId
ORDER BY Total DESC, TrackId ASC
Такие задачи были в отборочном и основном раунде для поступления на курс «Продвинутая разработка микросервисов на Go». Какая задача вам показалась самой лёгкой, а какая — наиболее сложной? А если вы нашли более эффективное или элегантное решение, делитесь в комментариях :)