Что тренируем сегодня
На прошлой тренировке мы разминались на прямой симуляции: перебирали ситуации руками, аккуратно разбирали случаи. Сегодня — шаг в сторону, но не менее важный: аккуратная работа с числами. Разогрев продолжается, сложность растёт плавно, и сегодняшняя тема — из тех, что кажутся тривиальными, пока не встретишься с первым TLE от лишнего цикла или неверным ответом от / вместо целочисленного деления.
Тренируем две вещи: деление с округлением вверх без ухода в float, и работу с суммами координат вместо честной симуляции векторов. Обе задачи решаются в одну-две строчки формулы — но именно в таких формулах прячутся классические ловушки округления и типов данных, которые дальше будут стоить очков на настоящих контестах.
Нормально, если сначала кажется, что тут решать нечего — само по себе решение действительно короткое. Интереснее то, почему именно такая формула работает и где она обычно ломается.
Теория
Целочисленный потолок — округление вверх без float. Иногда результат деления нужно округлить в большую сторону, а не просто отбросить дробную часть. Делать это через float опасно — из-за погрешности можно получить неверный ответ на одном из тестов.
Когда применять:
- в условии просят «минимальное количество плит/коробок/групп, чтобы вместить»;
- нужно разделить поровну, но с запасом на «хвост», который не поместился целиком.
Как решать:
- вместо
ceil(a / b)через деление с плавающей точкой считаем(a + b - 1) // b; - прибавка
b - 1«дотягивает» целочисленное деление вниз до следующего целого — но только когда деление было неточным; - если деление было точным, прибавка ничего не меняет.
Например, (7 + 3 - 1) // 3 = 9 // 3 = 3 — ровно то, сколько плит по 3 метра нужно, чтобы покрыть 7 метров.
Где ошибаются: путают с обычным округлением вниз (a // b) там, где нужен запас — например, «сколько коробок нужно» всегда округляется вверх, даже если слово «вверх» в условии не произнесено явно.
Покоординатное суммирование вместо симуляции векторов. Задача описана в терминах векторов или точек, но реальный вопрос сводится к отдельным числам по каждой координате.
Когда применять:
- в условии — сложение или сравнение векторов/точек с нулём;
- на самом деле каждую координату (x, y) можно считать отдельно, без «геометрии».
Как решать: сложение векторов покомпонентно — сумма по x не зависит от суммы по y. Поэтому «сумма векторов равна нулю» — это просто два отдельных равенства: сумма всех x равна нулю и сумма всех y равна нулю.
Где ошибаются: начинают думать в терминах направлений и углов там, где достаточно двух простых числовых счётчиков — по одному на ось.
Задача 1 — CF ~1000 — «Театральная площадь»
Что дано
Театральная площадь в вымышленном городе имеет прямоугольную форму размером метров. К годовщине города решено вымостить площадь квадратными гранитными плитами со стороной .
Разрешено покрыть плитами площадь, большую, чем сама площадь (плиты могут свисать за края) — лишь бы вся площадь была покрыта. Ломать плиты нельзя. Стороны плит должны быть параллельны сторонам площади.
Нужно найти наименьшее количество плит, которое для этого потребуется.
Формат ввода. Одна строка с тремя натуральными числами: , , ().
Формат вывода. Одно число — минимальное количество плит.
Пример:
| Вход | Выход |
|---|---|
6 6 4 | 4 |
Разберём на примере
Площадь , плита .
Вдоль каждой стороны длиной 6 плита стороной 4 укладывается один раз полностью (закрывает от 0 до 4) и один раз частично (нужно закрыть от 4 до 6, а плита покрывает от 4 до 8 — с запасом, но это разрешено). Значит, вдоль каждой стороны нужно 2 плиты, а не 1 (одной не хватит: ) и не 3 (двух уже достаточно).
Итого по одной стороне — плиты. Площадь двумерная, значит плиты укладываются сеткой: плиты. Это и есть ответ.
Ключевое наблюдение: задача распадается на две независимые одномерные — сколько плит нужно вдоль стороны , и сколько вдоль стороны . Ответ — произведение этих двух чисел.
Идея решения
Вдоль одной стороны длиной плитами шириной нужно штук — округление вверх, потому что последняя частично покрытая плита всё равно ставится целиком (ломать нельзя, а не закрыть — нельзя тоже).
Ответ:
Почему это верно, а не просто «разумное предположение». Оси и независимы: то, сколько плит нужно по горизонтали, не зависит от того, сколько нужно по вертикали — сетка складывается как декартово произведение одномерных решений. А минимальное число сегментов длины , покрывающих отрезок длины (без разрывов, с разрешённым выходом за границу и без дробления сегментов) — это ровно : меньшее число сегментов покроет самое большее при нецелом делении, то есть не докроет отрезок; на одну больше — уже избыточно.
Целочисленный потолок без float. Считать ceil(n / a) через деление с плавающей точкой рискованно — при до округление float начинает давать неверный результат на некоторых значениях. Вместо этого используем целочисленный трюк:
Это обычное целочисленное деление (округление вниз), но к числителю прибавлен запас , который «дотягивает» до следующего целого именно тогда, когда деление не было точным, и не меняет результат, когда было точным.
Псевдокод
прочитать n, m, a
k_n = (n + a - 1) // a // сколько плит нужно вдоль стороны n
k_m = (m + a - 1) // a // сколько плит вдоль стороны m
вывести k_n * k_m
Код решения
Проверка на примере
n | m | a | Ответ | ||
|---|---|---|---|---|---|
| 6 | 6 | 4 | ✓ |
Совпадает с ожидаемым ответом 4.
Крайние случаи
- Сторона делится нацело на
a. Например, , : — ровно , лишняя плита не добавляется. Формула не «переокругляет» точные случаи. aбольше стороны. , : — одна плита с большим запасом, что и требуется (плиту дробить нельзя, значит нужна хотя бы одна).- Максимальные значения. даёт ответ , но при , ответ — , что не помещается в 32-битный
int(переполнение на ). Нужен 64-битный тип.
Типичные ошибки
intвместоlong longв C++. Ответ до — обычныйint(до ) переполняется мгновенно. В Python эта проблема отсутствует — целые неограниченной точности.- Деление через
float/double.ceil(n / (double)a)даёт неверный ответ на некоторых крупных значениях из-за погрешности представления чисел с плавающей точкой — плата за удобство синтаксисаceil()из<cmath>на самом деле дороже, чем кажется. - Забыли округлить вверх. Простое
n / a(округление вниз) — недосчитает одну плиту ровно там, где деление не точное. Проверяется тестом с , :6 / 4 = 1(неверно) против(6 + 3) / 4 = 2(верно). - Спутали, что нужно перемножить, а не сложить. Ответ — произведение (это площадь сетки плит), а не сумма — частая невнимательность при беглом чтении условия.
Сложность
Время: . Память: .
Задача 2 — CF ~1000 — «Юный физик»
Что дано
Дано тело в пространстве, на которое действует сил. Каждая сила задаётся тремя целыми координатами вектора . Нужно определить, находится ли тело в равновесии — то есть равна ли нулю сумма всех действующих на него сил (векторная сумма).
Формат ввода. Первая строка — целое число () — количество сил. Далее следуют строк, в каждой — три целых числа () — компоненты соответствующего вектора силы.
Формат вывода. «YES», если тело находится в равновесии, иначе «NO».
Примеры:
Пример 1 — вход:
3
4 1 7
-2 4 -1
1 -5 -3
Ожидаемый выход: NO.
Пример 2 — вход:
3
3 -1 7
-5 2 -4
2 -1 -3
Ожидаемый выход: YES.
Разберём на примере
Первый пример: три вектора , , .
Складываем отдельно по каждой оси, а не как «геометрическую» сумму векторов:
- по оси :
- по оси :
- по оси :
Сумма по равна 3, не 0 — значит тело не в равновесии. Ответ — NO, что совпадает с ожидаемым.
Второй пример: , , .
- по оси :
- по оси :
- по оси :
Все три суммы равны 0 — тело в равновесии, ответ YES. Тоже совпадает.
Наблюдение: нам вообще не нужно ничего знать про физику или геометрию векторов сверх того, что равновесие — это когда векторная сумма равна нулевому вектору. А нулевой вектор — это когда каждая координата суммы равна нулю. Задача сводится к трём независимым суммам целых чисел.
Идея решения
Заводим три накопителя — sum_x, sum_y, sum_z, изначально равные нулю. Читаем троек чисел, к каждому накопителю прибавляем соответствующую компоненту. В конце проверяем: все три суммы равны нулю — выводим YES, иначе — NO.
Почему это корректно, а не только «похоже на верно». Векторная сумма по определению покомпонентна: -компонента суммы — это сумма всех -компонент слагаемых, аналогично для и (сложение векторов в декартовых координатах линейно и покоординатно). Равенство векторной суммы нулевому вектору эквивалентно системе из трёх скалярных равенств , , одновременно. Значит, порядок сложения и любая «векторная» интерпретация здесь не нужны вовсе — задача полностью сводится к трём обычным целочисленным суммам.
Псевдокод
прочитать n
sum_x = 0, sum_y = 0, sum_z = 0
повторить n раз:
прочитать x, y, z
sum_x += x
sum_y += y
sum_z += z
если sum_x == 0 и sum_y == 0 и sum_z == 0:
вывести "YES"
иначе:
вывести "NO"
Код решения
Проверка на примерах
| # | Векторы | Ожидание | Наш ответ | |||
|---|---|---|---|---|---|---|
| 1 | (4,1,7), (-2,4,-1), (1,-5,-3) | 3 | 0 | 3 | NO | NO ✓ |
| 2 | (3,-1,7), (-5,2,-4), (2,-1,-3) | 0 | 0 | 0 | YES | YES ✓ |
Оба примера сходятся.
Крайние случаи
- . Единственная сила — тело в равновесии, только если сама эта сила нулевой вектор . Формула работает без изменений: суммы совпадают со значениями единственной строки.
- Все нули. строк вида
0 0 0— суммы по всем осям нулевые, ответYES. Тривиально, но проверяется тестами. - Максимальные по модулю значения. , каждая компонента — максимальная сумма по модулю . В пределы
intукладывается с огромным запасом,long longв C++ здесь избыточен, но не вреден (используем для единообразия и защиты от будущих правок ограничений). - Сумма даёт ноль «случайно» при ненулевых слагаемых. Например, компоненты по одной оси — сумма 0, хотя ни одно слагаемое не ноль. Код проверяет именно сумму, а не то, что все числа нулевые, поэтому такой случай обрабатывается корректно.
Типичные ошибки
- Проверка каждой строки на равенство нулю вместо суммы по столбцам. Частая путаница: считать, что тело в равновесии, если каждая отдельная сила нулевая. На самом деле равновесие — про сумму всех сил, а не про то, что каждая сила по отдельности нулевая.
- Досрочный
breakпри первой ненулевой сумме. Если прерывать чтение входных данных сразу, как толькоsum_x != 0, оставшиеся строки входа не будут считаны, и последующее чтение (если оно есть в программе) собьётся или тестирующая система не получит ожидаемого количества операций чтения. Дочитывать нужно всё до конца, даже если ответ уже ясен. - Сравнение с
-0или использованиеfloatдля сумм. Если по ошибке взятьfloat/doubleдля накопителей (например, чтобы «на всякий случай»), сравнениеsum == 0.0менее надёжно из-за потенциальных ошибок округления при промежуточных операциях — хотя для целых входных данных это маловероятно, привычка держать суммы целыми числами убирает даже теоретический риск. - Перепутан порядок чтения
x, y, z. Если считать координаты не в том порядке, что дан во входе (например,z, y, x), суммы посчитаются неверно для несимметричных тестов — на первый взгляд код «работает», но падает на скрытых тестах, где оси действительно различаются.
Сложность
Время: — один проход по всем силам. Память: (или , если данные прочитаны в массив целиком, как в решении на Python — не влияет на порядок сложности, но можно писать и потоково, накапливая суммы на лету без хранения всего массива).
Самостоятельная тренировка
Codeforces 58A «Chat room» (~CF 1000) — по данной строке определить, можно ли, удаляя из неё символы, получить слово «hello» (как подпоследовательность, не обязательно подряд идущую).
Codeforces 118A «String Task» (~CF 1000) — из строки убрать все гласные буквы, перед каждой оставшейся согласной вставить точку, и привести все буквы к нижнему регистру.
Codeforces 476A «Dreamoon and Stairs» (~CF 1000) — по лестнице из ступеней можно подниматься шагами по 1 или 2 ступени; нужно найти минимальное число шагов, которое кратно заданному (или определить, что это невозможно).
Codeforces 108A «Palindromic Times» (~CF 1000) — по времени в формате HH:MM найти ближайший следующий момент, когда цифры на табло читаются одинаково в обе стороны.
Прорешай их самостоятельно — если застрянешь, разбери с AI-партнёром на codepal.ru: он не даёт готовый ответ, а подводит к решению вопросами.
Что дальше
На следующей тренировке поднимаем сложность до ~CF 1000–1100 и добавляем первую жадность: сортировку по остаткам и обоснование того, почему жадный выбор порядка вообще работает.