Что тренируем сегодня
Пятая сессия познакомила нас с бинарным поиском по ответу — первым инструментом, где вместо прямого вычисления мы угадываем ответ и проверяем догадку. Сегодня возвращаемся к более прямому стилю мышления, но поднимаем планку: обе задачи решаются без циклов по всем числам — либо замкнутой формулой, либо предвычислением, которое превращает каждый запрос в одну операцию O(1). Это верхняя часть коридора разогрева: рейтинг CF ~1200, чуть выше предыдущих сессий.
Сегодня в фокусе два инструмента: вывод формулы через периодичность (когда числа с нужным свойством повторяются одинаковыми блоками) и префиксные суммы по отсортированной копии массива (когда запрос звучит не про исходный порядок элементов, а про их место в отсортированном ряду). Оба приёма экономят время не за счёт более быстрого перебора, а за счёт того, что перебора вообще не нужно — типичный сдвиг мышления на пути от простой реализации к настоящей алгоритмике.
Теория
Вывод формулы через периодичность. Если нужное свойство повторяется через равные интервалы, можно перепрыгнуть сразу к ответу одной формулой, не перебирая элементы по одному.
Когда применять:
- нужно найти -й элемент в очень длинной или бесконечной последовательности;
- свойство повторяется блоками: «каждое -е число обладает свойством X».
Как решать:
- Считаем, сколько полных «периодов» (блоков) укладывается до нужного элемента.
- Одним арифметическим выражением находим ответ, не перебирая элементы поштучно.
Например: если свойством обладает каждое 3-е число, а нужен 10-й элемент по счёту среди таких чисел, ответ — это просто с поправкой на начало отсчёта.
Где ошибаются: главная трудность не в идее, а в границах — когда искомый индекс попадает ровно на конец периода, наивная формула часто сдвигается на единицу. Такие случаи стоит проверить руками на маленьком примере, прежде чем доверять формуле.
Префиксные суммы сразу по двум массивам — исходному и отсортированному. Это тот же приём предвычисления за один проход из сессии 4, только применённый дважды — к обычному массиву и к его отсортированной копии.
Когда применять:
- запросы явно делятся на два типа: одни — про элементы «как они даны», другие — про элементы «как они были бы после сортировки».
Как решать:
- Строим массив префиксных сумм для исходного порядка.
- Отдельно строим такой же массив для отсортированной копии.
- Любой запрос суммы на отрезке — одно вычитание
prefix[r] - prefix[l-1], независимо от того, к какому из двух массивов он относится.
Где ошибаются: пытаются обойтись одним массивом там, где запросы смешивают два разных порядка — сортировка «на месте» без сохранения исходного массива портит ответы на запросы первого типа.
Задача 1 — CF ~1200 — «K-th Not Divisible by n»
Что дано
Даны два положительных целых числа и . Нужно вывести -е по счёту положительное целое число, которое не делится на .
Например, при и : числа, не делящиеся на 3, — это . Седьмое число в этом списке — .
Формат ввода: первая строка содержит число () — количество независимых тестовых случаев. Далее идут строк, в каждой — два целых числа и (, ).
Формат вывода: для каждого теста — одно число, -е положительное целое, не делящееся на .
Разберём на примере
Возьмём , . Выпишем первые числа подряд и отметим, какие делятся на 3:
| Число | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Делится на 3? | нет | нет | да | нет | нет | да | нет | нет | да | нет |
| Номер среди «не делится» | 1 | 2 | — | 3 | 4 | — | 5 | 6 | — | 7 |
Число 10 — седьмое среди тех, что не делятся на 3. Совпадает с ответом из условия.
Заметим закономерность: числа идут блоками по — в каждом блоке из подряд идущих чисел ровно одно делится на (последнее в блоке), а остальные — не делятся. Блок 1, 2, 3 даёт 2 «хороших» числа (1, 2), блок 4, 5, 6 — ещё 2 (4, 5), блок 7, 8, 9 — ещё 2 (7, 8). Перебирать эти блоки по одному, пока не наберём штук, — рабочая идея, но при и до таких блоков может быть слишком много для перебора за отведённое время. Нужна формула, которая сразу «перепрыгивает» к нужному числу.
Идея решения
Идея в одну фразу: число блоков, которые нужно полностью пройти, чтобы набрать «хороших» чисел, — это ; прибавляем это количество к и получаем ответ.
Почему так: если бы все числа от 1 до какого-то не делились на , ответом было бы просто . Но начиная с числа (и далее , , …) через каждые чисел «теряется» ровно одно — то, что делится на . Значит, чтобы досчитать до -го «не делящегося» числа, нужно сдвинуться вправо на столько единиц, сколько кратных чисел встретится по дороге.
Формально: пусть ответ равен . Среди чисел от 1 до ровно делятся на , значит, не делящихся — ровно . Нужно, чтобы это было равно :
Прямо решать это уравнение относительно неудобно из-за целой части, но можно рассуждать через блоки: в каждом полном блоке из чисел — ровно «хороших». Значит, чтобы накопить хороших чисел, нужно полностью пройденных кратных (используем , а не , чтобы обработать пограничный случай, когда ровно делится на — тогда последнее нужное число само оказывается последним перед очередным кратным, а не после него). Это количество кратных, оставшихся позади, и нужно прибавить к , чтобы получить итоговое число:
Проверим интуицию на примере: , — среди первых чисел на каждые 2 «хороших» приходится 1 «плохое» (кратное 3). Нужно 7 хороших → они укладываются в -окрестности, а точная формула даёт — ровно то, что мы посчитали руками.
Важный частный случай — . Тогда , и формула превращается в — это просто -е нечётное число, что логично: числа, не делящиеся на 2, — это все нечётные.
Псевдокод
прочитать t
повторить t раз:
прочитать n, k
need = (k - 1) целочисленно_разделить (n - 1)
ответ = k + need
вывести ответ
Код решения
Проверка на примерах
Полный набор примеров условия — шесть тестов в одном запуске:
| n | k | Ожидание | Наш расчёт |
|---|---|---|---|
| 3 | 7 | 10 | , ✓ |
| 4 | 12 | 15 | , ✓ |
| 2 | 1000000000 | 1999999999 | , ✓ |
| 7 | 97 | 113 | , ✓ |
| 1000000000 | 1000000000 | 1000000001 | , ✓ |
| 2 | 1 | 1 | , ✓ |
Все шесть совпадают с ожидаемым выводом.
Крайние случаи
- — минимально допустимое значение . Формула превращается в (см. вывод выше) — это тестируется вторым и последним примерами условия ( и ), и оба совпадают.
- кратно без остатка — например, : тогда , а не , как для . Именно поэтому в формуле используется , а не — без этого сдвига на единицу ответ был бы занижен ровно на пограничных значениях.
- Максимальные значения обоих чисел — : ответ немного больше ( в примере условия), что уже не помещается в диапазон
intв C++ с запасом на промежуточные операции — отсюдаlong long. - — первое «не делящееся» число всегда равно 1 (единица не делится ни на какое ), формула даёт , ответ при любом .
Типичные ошибки
intвместоlong longв C++. При ответ превышает , и хотя сам по себе не переполняет 32-битныйint(максимум ~), уже следующий подобный тест с чуть большими промежуточными вычислениями легко выйдет за границу — надёжнее сразу считать вlong long.- Забыть
-1в числителе формулы. Частая ошибка — писатьneed = k / (n - 1)без сдвига. Это даёт неверный ответ ровно на пограничных значениях, где кратно (см. «Крайние случаи», пункт 2). - Целочисленное деление в Python через
/вместо//./вернётfloat, и код либо упадёт при попытке вывести дробное число как позицию, либо даст видимо похожий, но неточный по типу результат. - Попытка честно перебирать числа one-by-one до -го «не делящегося». При цикл потребует до итераций на один тест — с учётом тестов это точно не уложится в лимит времени. Только формула.
Сложность
Время: на тест, суммарно. Память: .
Задача 2 — CF ~1200 — «Kuriyama Mirai's Stones»
Что дано
Дан массив из камней (), пронумерованных от 1 до ; стоимость -го камня — (). Обозначим через стоимость -го по счёту камня, если отсортировать все стоимости по неубыванию (то есть — это отсортированная копия массива ).
Дальше поступает запросов (), каждый одного из двух типов, заданных числами type, , ():
- Тип 1 — вывести сумму (сумма в исходном порядке массива).
- Тип 2 — вывести сумму (сумма в отсортированном порядке).
Ответы на запросы могут превышать диапазон 32-битного целого числа.
Разберём на примере
Возьмём второй пример из условия: , массив .
Отсортируем копию массива по неубыванию: .
Разберём несколько запросов:
| Запрос | Что считаем | Вычисление | Ответ |
|---|---|---|---|
1 2 4 | сумма (исходный порядок) | 10 | |
2 1 4 | сумма всего (отсортированный порядок) | 15 | |
1 1 1 | сумма | 5 | |
2 1 2 | сумма | 5 |
Ключевое наблюдение: массив и массив — это два разных массива одной и той же мультимножины чисел, и запросы к ним независимы. Если для каждого запроса заново суммировать элементы диапазона в цикле, при в худшем случае получим до операций — слишком медленно. Нужен способ отвечать на запрос суммы диапазона за .
Идея решения
Идея в одну фразу: посчитать заранее два массива префиксных сумм — один по исходному массиву , второй по его отсортированной копии — и отвечать на каждый запрос одним вычитанием.
Префиксная сумма массива — это массив , где и (сумма первых элементов). Тогда сумма элементов на отрезке равна — стандартный факт: включает всё от 1 до , включает всё от 1 до , разность оставляет ровно отрезок .
Ничего не мешает применить этот же приём дважды к разным массивам: раз запросы типа 1 всегда про , а запросы типа 2 всегда про (отсортированную копию), можно один раз построить и , а дальше каждый запрос — это выбор нужного префиксного массива по типу и одно вычитание. Порядок элементов в и в разный, но каждый массив по отдельности статичен (не меняется между запросами) — значит, префиксные суммы считаются один раз и переиспользуются для всех запросов.
Псевдокод
прочитать n
прочитать массив v из n чисел
// префиксные суммы по исходному массиву
prefix_orig[0] = 0
для i от 1 до n:
prefix_orig[i] = prefix_orig[i-1] + v[i-1]
// отсортированная копия + префиксные суммы по ней
u = копия v, отсортированная по неубыванию
prefix_sorted[0] = 0
для i от 1 до n:
prefix_sorted[i] = prefix_sorted[i-1] + u[i-1]
прочитать m
повторить m раз:
прочитать type, l, r
если type == 1:
вывести prefix_orig[r] - prefix_orig[l-1]
иначе:
вывести prefix_sorted[r] - prefix_sorted[l-1]
Код решения
Проверка на примерах
Пример 1. , . Отсортированная копия .
| Запрос | Ожидание | Наш расчёт |
|---|---|---|
2 3 6 | 24 | ✓ |
1 3 4 | 9 | ✓ |
1 1 6 | 28 | сумма всего : ✓ |
Пример 2. , , , 10 запросов — все выходы совпали при ручном прогоне (см. таблицу в разделе «Разберём на примере» для первых четырёх; остальные шесть — 5, 5, 2, 12, 3, 5 — тоже подтверждены прямым вычитанием префиксных сумм на обоих массивах).
Все примеры условия сходятся.
Крайние случаи
- — единственный камень, префиксные суммы вырождаются в массив из двух элементов
[0, v_1]; любой запросl=r=1даёт просто независимо от типа (сортировка одного элемента ничего не меняет). - Повторяющиеся значения стоимости — как в обоих примерах условия (, ). Сортировка с повторами работает штатно, порядок одинаковых элементов между собой не важен для суммы диапазона.
- Запрос на весь массив () — сумма по типу 1 и по типу 2 совпадает (это сумма всех элементов независимо от порядка), но вычисляется отдельно через свой префиксный массив — специальной оптимизации не требуется, оба пути дают одинаковый числовой результат.
- Переполнение суммы. При и до сумма всех элементов может достигать — далеко за пределами 32-битного
int(до ~). В условии прямо указано на этот риск.
Типичные ошибки
intвместоlong longдля сумм в C++. Суммарная стоимость всех камней легко достигает — переполнениеintдаст неверный (обычно отрицательный) ответ без явной ошибки времени выполнения.- Сортировать исходный массив вместо создания отдельной копии. Если отсортировать
v"на месте", запросы типа 1 (про исходный порядок) станут отвечать неверно — нужно две отдельные структуры: исходный порядок и отсортированная копия. - Забыть про
l - 1при вычитании префиксной суммы. Формула суммы отрезка — этоprefix[r] - prefix[l - 1], а неprefix[r] - prefix[l]; ошибка на единицу здесь выбрасывает из суммы либо на один элемент больше, либо на один меньше, чем нужно. - Быстрый ввод-вывод не настроен в C++. При до и посимвольном
cin/coutбезios::sync_with_stdio(false)возможен вылет по времени — привычка отключать синхронизацию сstdioв началеmainзащищает от этого системно.
Сложность
Время: на сортировку и построение префиксных сумм, далее на каждый из запросов — итого . Память: на оба префиксных массива.
Самостоятельная тренировка
Прорешай эти четыре задачи самостоятельно — все того же уровня CF ~1200, и все тренируют похожий стиль мышления: выразить ответ формулой или предвычислением вместо прямого перебора.
- Codeforces 1343C «Alternating Subsequence» (~CF 1200) — среди подпоследовательности знакопеременных по знаку чисел найти самую длинную, а среди самых длинных — с максимальной суммой элементов.
- Codeforces 977C «Less or Equal» (~CF 1200) — по заданному массиву и числу найти любое в диапазоне , для которого ровно элементов массива меньше или равны , либо определить, что такого не существует.
- Codeforces 112B «Petya and Square» (~CF 1200) — по стороне квадрата и координатам отмеченной клетки определить, можно ли разрезать квадрат ломаной линией по сетке на две равные (с точностью до поворота) части, не задевающие эту клетку.
- Codeforces 176A «Trading Business» (~CF 1200) — по ценам покупки и продажи товаров на разных планетах найти максимальную прибыль от одной сделки «купить на одной планете — продать на другой».
Если застрянешь — разбери задачу с AI-партнёром на codepal.ru: он не даёт готовый ответ, а подводит к решению вопросами.
Что дальше
На следующей тренировке остаёмся на уровне CF ~1200–1300 и добавляем ещё два инструмента в связке: бинарный поиск по отсортированному массиву (не по ответу, а напрямую по значениям) и решето Эратосфена для быстрой проверки чисел на простоту.