← Вернуться в блог

Тренировочная сессия 6: прямая формула и префиксные суммы на отсортированном массиве СЕССИЯ

  • letnyaya-podgotovka
  • matematika
  • prefiksnye-summy
  • formula
  • sortirovka

Что тренируем сегодня

Пятая сессия познакомила нас с бинарным поиском по ответу — первым инструментом, где вместо прямого вычисления мы угадываем ответ и проверяем догадку. Сегодня возвращаемся к более прямому стилю мышления, но поднимаем планку: обе задачи решаются без циклов по всем числам — либо замкнутой формулой, либо предвычислением, которое превращает каждый запрос в одну операцию O(1). Это верхняя часть коридора разогрева: рейтинг CF ~1200, чуть выше предыдущих сессий.

Сегодня в фокусе два инструмента: вывод формулы через периодичность (когда числа с нужным свойством повторяются одинаковыми блоками) и префиксные суммы по отсортированной копии массива (когда запрос звучит не про исходный порядок элементов, а про их место в отсортированном ряду). Оба приёма экономят время не за счёт более быстрого перебора, а за счёт того, что перебора вообще не нужно — типичный сдвиг мышления на пути от простой реализации к настоящей алгоритмике.


Теория

Вывод формулы через периодичность. Если нужное свойство повторяется через равные интервалы, можно перепрыгнуть сразу к ответу одной формулой, не перебирая элементы по одному.

Когда применять:

  • нужно найти kk-й элемент в очень длинной или бесконечной последовательности;
  • свойство повторяется блоками: «каждое nn-е число обладает свойством X».

Как решать:

  1. Считаем, сколько полных «периодов» (блоков) укладывается до нужного элемента.
  2. Одним арифметическим выражением находим ответ, не перебирая элементы поштучно.

Например: если свойством обладает каждое 3-е число, а нужен 10-й элемент по счёту среди таких чисел, ответ — это просто 10×310 \times 3 с поправкой на начало отсчёта.

Где ошибаются: главная трудность не в идее, а в границах — когда искомый индекс попадает ровно на конец периода, наивная формула часто сдвигается на единицу. Такие случаи стоит проверить руками на маленьком примере, прежде чем доверять формуле.

Префиксные суммы сразу по двум массивам — исходному и отсортированному. Это тот же приём предвычисления за один проход из сессии 4, только применённый дважды — к обычному массиву и к его отсортированной копии.

Когда применять:

  • запросы явно делятся на два типа: одни — про элементы «как они даны», другие — про элементы «как они были бы после сортировки».

Как решать:

  1. Строим массив префиксных сумм для исходного порядка.
  2. Отдельно строим такой же массив для отсортированной копии.
  3. Любой запрос суммы на отрезке — одно вычитание prefix[r] - prefix[l-1], независимо от того, к какому из двух массивов он относится.

Где ошибаются: пытаются обойтись одним массивом там, где запросы смешивают два разных порядка — сортировка «на месте» без сохранения исходного массива портит ответы на запросы первого типа.


Задача 1 — CF ~1200 — «K-th Not Divisible by n»

Что дано

Даны два положительных целых числа nn и kk. Нужно вывести kk-е по счёту положительное целое число, которое не делится на nn.

Например, при n=3n = 3 и k=7k = 7: числа, не делящиеся на 3, — это 1,2,4,5,7,8,10,11,13,1, 2, 4, 5, 7, 8, 10, 11, 13, \ldots. Седьмое число в этом списке — 1010.

Формат ввода: первая строка содержит число tt (1t10001 \le t \le 1000) — количество независимых тестовых случаев. Далее идут tt строк, в каждой — два целых числа nn и kk (2n1092 \le n \le 10^9, 1k1091 \le k \le 10^9).

Формат вывода: для каждого теста — одно число, kk-е положительное целое, не делящееся на nn.

Разберём на примере

Возьмём n=3n = 3, k=7k = 7. Выпишем первые числа подряд и отметим, какие делятся на 3:

Число12345678910
Делится на 3?нетнетданетнетданетнетданет
Номер среди «не делится»1234567

Число 10 — седьмое среди тех, что не делятся на 3. Совпадает с ответом из условия.

Заметим закономерность: числа идут блоками по nn — в каждом блоке из nn подряд идущих чисел ровно одно делится на nn (последнее в блоке), а остальные n1n - 1 — не делятся. Блок 1, 2, 3 даёт 2 «хороших» числа (1, 2), блок 4, 5, 6 — ещё 2 (4, 5), блок 7, 8, 9 — ещё 2 (7, 8). Перебирать эти блоки по одному, пока не наберём kk штук, — рабочая идея, но при kk и nn до 10910^9 таких блоков может быть слишком много для перебора за отведённое время. Нужна формула, которая сразу «перепрыгивает» к нужному числу.

Идея решения

Идея в одну фразу: число блоков, которые нужно полностью пройти, чтобы набрать kk «хороших» чисел, — это (k1)/(n1)\lfloor (k - 1) / (n - 1) \rfloor; прибавляем это количество к kk и получаем ответ.

Почему так: если бы все числа от 1 до какого-то XX не делились на nn, ответом было бы просто X=kX = k. Но начиная с числа nn (и далее 2n2n, 3n3n, …) через каждые nn чисел «теряется» ровно одно — то, что делится на nn. Значит, чтобы досчитать до kk-го «не делящегося» числа, нужно сдвинуться вправо на столько единиц, сколько кратных nn чисел встретится по дороге.

Формально: пусть ответ равен xx. Среди чисел от 1 до xx ровно x/n\lfloor x/n \rfloor делятся на nn, значит, не делящихся — ровно xx/nx - \lfloor x/n \rfloor. Нужно, чтобы это было равно kk:

xxn=kx - \left\lfloor \frac{x}{n} \right\rfloor = k

Прямо решать это уравнение относительно xx неудобно из-за целой части, но можно рассуждать через блоки: в каждом полном блоке из nn чисел — ровно n1n - 1 «хороших». Значит, чтобы накопить kk хороших чисел, нужно (k1)/(n1)\lfloor (k - 1) / (n - 1) \rfloor полностью пройденных кратных nn (используем k1k - 1, а не kk, чтобы обработать пограничный случай, когда kk ровно делится на n1n - 1 — тогда последнее нужное число само оказывается последним перед очередным кратным, а не после него). Это количество кратных, оставшихся позади, и нужно прибавить к kk, чтобы получить итоговое число:

x=k+k1n1x = k + \left\lfloor \frac{k - 1}{n - 1} \right\rfloor

Проверим интуицию на примере: n=3n = 3, k=7k = 7 — среди первых xx чисел на каждые 2 «хороших» приходится 1 «плохое» (кратное 3). Нужно 7 хороших → они укладываются в 73/2\lceil 7 \cdot 3 / 2 \rceil-окрестности, а точная формула даёт 7+6/2=7+3=107 + \lfloor 6/2 \rfloor = 7 + 3 = 10 — ровно то, что мы посчитали руками.

Важный частный случай — n=2n = 2. Тогда n1=1n - 1 = 1, и формула превращается в x=k+(k1)=2k1x = k + (k - 1) = 2k - 1 — это просто kk-е нечётное число, что логично: числа, не делящиеся на 2, — это все нечётные.

Псевдокод

прочитать t
повторить t раз:
    прочитать n, k
    need = (k - 1) целочисленно_разделить (n - 1)
    ответ = k + need
    вывести ответ

Код решения

Проверка на примерах

Полный набор примеров условия — шесть тестов в одном запуске:

nkОжиданиеНаш расчёт
3710need=6/2=3need = \lfloor 6/2 \rfloor = 3, x=7+3=10x = 7+3=10
41215need=11/3=3need = \lfloor 11/3 \rfloor = 3, x=12+3=15x = 12+3=15
210000000001999999999need=999999999/1=999999999need = \lfloor 999999999/1 \rfloor = 999999999, x=109+999999999=1999999999x = 10^9+999999999=1999999999
797113need=96/6=16need = \lfloor 96/6 \rfloor = 16, x=97+16=113x = 97+16=113
100000000010000000001000000001need=999999999/999999999=1need = \lfloor 999999999/999999999 \rfloor = 1, x=109+1x = 10^9+1
211need=0/1=0need = \lfloor 0/1 \rfloor = 0, x=1+0=1x = 1+0=1

Все шесть совпадают с ожидаемым выводом.

Крайние случаи

  1. n=2n = 2 — минимально допустимое значение nn. Формула превращается в 2k12k - 1 (см. вывод выше) — это тестируется вторым и последним примерами условия (n=2,k=109n=2, k=10^9 и n=2,k=1n=2, k=1), и оба совпадают.
  2. kk кратно (n1)(n-1) без остатка — например, n=7,k=96n=7, k=96: тогда need=95/6=15need = \lfloor 95/6 \rfloor = 15, а не 1616, как для k=97k=97. Именно поэтому в формуле используется k1k - 1, а не kk — без этого сдвига на единицу ответ был бы занижен ровно на пограничных значениях.
  3. Максимальные значения обоих чиселn=k=109n = k = 10^9: ответ немного больше 10910^9 (10000000011000000001 в примере условия), что уже не помещается в диапазон int в C++ с запасом на промежуточные операции — отсюда long long.
  4. k=1k = 1 — первое «не делящееся» число всегда равно 1 (единица не делится ни на какое n2n \ge 2), формула даёт need=0/(n1)=0need = \lfloor 0/(n-1) \rfloor = 0, ответ 11 при любом nn.

Типичные ошибки

  1. int вместо long long в C++. При n=k=109n = k = 10^9 ответ превышает 10910^9, и хотя сам по себе не переполняет 32-битный int (максимум ~2.1×1092.1 \times 10^9), уже следующий подобный тест с чуть большими промежуточными вычислениями легко выйдет за границу — надёжнее сразу считать в long long.
  2. Забыть -1 в числителе формулы. Частая ошибка — писать need = k / (n - 1) без сдвига. Это даёт неверный ответ ровно на пограничных значениях, где kk кратно (n1)(n-1) (см. «Крайние случаи», пункт 2).
  3. Целочисленное деление в Python через / вместо //. / вернёт float, и код либо упадёт при попытке вывести дробное число как позицию, либо даст видимо похожий, но неточный по типу результат.
  4. Попытка честно перебирать числа one-by-one до kk-го «не делящегося». При n=k=109n = k = 10^9 цикл потребует до 2×1092 \times 10^9 итераций на один тест — с учётом t1000t \le 1000 тестов это точно не уложится в лимит времени. Только формула.

Сложность

Время: O(1)O(1) на тест, O(t)O(t) суммарно. Память: O(1)O(1).


Задача 2 — CF ~1200 — «Kuriyama Mirai's Stones»

Что дано

Дан массив из nn камней (1n1051 \le n \le 10^5), пронумерованных от 1 до nn; стоимость ii-го камня — viv_i (1vi1091 \le v_i \le 10^9). Обозначим через uiu_i стоимость ii-го по счёту камня, если отсортировать все стоимости по неубыванию (то есть uu — это отсортированная копия массива vv).

Дальше поступает mm запросов (1m1051 \le m \le 10^5), каждый одного из двух типов, заданных числами type, ll, rr (1lrn1 \le l \le r \le n):

  • Тип 1 — вывести сумму vl+vl+1++vrv_l + v_{l+1} + \ldots + v_r (сумма в исходном порядке массива).
  • Тип 2 — вывести сумму ul+ul+1++uru_l + u_{l+1} + \ldots + u_r (сумма в отсортированном порядке).

Ответы на запросы могут превышать диапазон 32-битного целого числа.

Разберём на примере

Возьмём второй пример из условия: n=4n = 4, массив v=[5,5,2,3]v = [5, 5, 2, 3].

Отсортируем копию массива по неубыванию: u=[2,3,5,5]u = [2, 3, 5, 5].

Разберём несколько запросов:

ЗапросЧто считаемВычислениеОтвет
1 2 4сумма v2+v3+v4v_2+v_3+v_4 (исходный порядок)5+2+35+2+310
2 1 4сумма всего uu (отсортированный порядок)2+3+5+52+3+5+515
1 1 1сумма v1v_1555
2 1 2сумма u1+u2u_1+u_22+32+35

Ключевое наблюдение: массив vv и массив uu — это два разных массива одной и той же мультимножины чисел, и запросы к ним независимы. Если для каждого запроса заново суммировать элементы диапазона в цикле, при n,m105n, m \le 10^5 в худшем случае получим до 101010^{10} операций — слишком медленно. Нужен способ отвечать на запрос суммы диапазона за O(1)O(1).

Идея решения

Идея в одну фразу: посчитать заранее два массива префиксных сумм — один по исходному массиву vv, второй по его отсортированной копии uu — и отвечать на каждый запрос одним вычитанием.

Префиксная сумма массива aa — это массив PP, где P0=0P_0 = 0 и Pi=Pi1+aiP_i = P_{i-1} + a_i (сумма первых ii элементов). Тогда сумма элементов на отрезке [l,r][l, r] равна PrPl1P_r - P_{l-1} — стандартный факт: PrP_r включает всё от 1 до rr, Pl1P_{l-1} включает всё от 1 до l1l-1, разность оставляет ровно отрезок [l,r][l, r].

Ничего не мешает применить этот же приём дважды к разным массивам: раз запросы типа 1 всегда про vv, а запросы типа 2 всегда про uu (отсортированную копию), можно один раз построить PvP^{v} и PuP^{u}, а дальше каждый запрос — это выбор нужного префиксного массива по типу и одно вычитание. Порядок элементов в vv и в uu разный, но каждый массив по отдельности статичен (не меняется между запросами) — значит, префиксные суммы считаются один раз и переиспользуются для всех mm запросов.

Псевдокод

прочитать 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. n=6n=6, v=[6,4,2,7,2,7]v=[6,4,2,7,2,7]. Отсортированная копия u=[2,2,4,6,7,7]u=[2,2,4,6,7,7].

ЗапросОжиданиеНаш расчёт
2 3 624u3+u4+u5+u6=4+6+7+7=24u_3+u_4+u_5+u_6 = 4+6+7+7=24
1 3 49v3+v4=2+7=9v_3+v_4 = 2+7=9
1 1 628сумма всего vv: 6+4+2+7+2+7=286+4+2+7+2+7=28

Пример 2. n=4n=4, v=[5,5,2,3]v=[5,5,2,3], u=[2,3,5,5]u=[2,3,5,5], 10 запросов — все выходы совпали при ручном прогоне (см. таблицу в разделе «Разберём на примере» для первых четырёх; остальные шесть — 5, 5, 2, 12, 3, 5 — тоже подтверждены прямым вычитанием префиксных сумм на обоих массивах).

Все примеры условия сходятся.

Крайние случаи

  1. n=1n = 1 — единственный камень, префиксные суммы вырождаются в массив из двух элементов [0, v_1]; любой запрос l=r=1 даёт просто v1v_1 независимо от типа (сортировка одного элемента ничего не меняет).
  2. Повторяющиеся значения стоимости — как в обоих примерах условия (v1=v2=5v_1=v_2=5, v4=v6=7v_4=v_6=7). Сортировка с повторами работает штатно, порядок одинаковых элементов между собой не важен для суммы диапазона.
  3. Запрос на весь массив (l=1,r=nl=1, r=n) — сумма по типу 1 и по типу 2 совпадает (это сумма всех элементов независимо от порядка), но вычисляется отдельно через свой префиксный массив — специальной оптимизации не требуется, оба пути дают одинаковый числовой результат.
  4. Переполнение суммы. При n=105n = 10^5 и viv_i до 10910^9 сумма всех элементов может достигать 101410^{14} — далеко за пределами 32-битного int (до ~2.1×1092.1 \times 10^9). В условии прямо указано на этот риск.

Типичные ошибки

  1. int вместо long long для сумм в C++. Суммарная стоимость всех камней легко достигает 101410^{14} — переполнение int даст неверный (обычно отрицательный) ответ без явной ошибки времени выполнения.
  2. Сортировать исходный массив вместо создания отдельной копии. Если отсортировать v "на месте", запросы типа 1 (про исходный порядок) станут отвечать неверно — нужно две отдельные структуры: исходный порядок и отсортированная копия.
  3. Забыть про l - 1 при вычитании префиксной суммы. Формула суммы отрезка [l,r][l, r] — это prefix[r] - prefix[l - 1], а не prefix[r] - prefix[l]; ошибка на единицу здесь выбрасывает из суммы либо на один элемент больше, либо на один меньше, чем нужно.
  4. Быстрый ввод-вывод не настроен в C++. При n,mn, m до 10510^5 и посимвольном cin/cout без ios::sync_with_stdio(false) возможен вылет по времени — привычка отключать синхронизацию с stdio в начале main защищает от этого системно.

Сложность

Время: O(nlogn)O(n \log n) на сортировку и построение префиксных сумм, далее O(1)O(1) на каждый из mm запросов — итого O(nlogn+m)O(n \log n + m). Память: O(n)O(n) на оба префиксных массива.


Самостоятельная тренировка

Прорешай эти четыре задачи самостоятельно — все того же уровня CF ~1200, и все тренируют похожий стиль мышления: выразить ответ формулой или предвычислением вместо прямого перебора.

  • Codeforces 1343C «Alternating Subsequence» (~CF 1200) — среди подпоследовательности знакопеременных по знаку чисел найти самую длинную, а среди самых длинных — с максимальной суммой элементов.
  • Codeforces 977C «Less or Equal» (~CF 1200) — по заданному массиву и числу kk найти любое xx в диапазоне [1,109][1, 10^9], для которого ровно kk элементов массива меньше или равны xx, либо определить, что такого xx не существует.
  • Codeforces 112B «Petya and Square» (~CF 1200) — по стороне квадрата 2n2n и координатам отмеченной клетки определить, можно ли разрезать квадрат ломаной линией по сетке на две равные (с точностью до поворота) части, не задевающие эту клетку.
  • Codeforces 176A «Trading Business» (~CF 1200) — по ценам покупки и продажи товаров на разных планетах найти максимальную прибыль от одной сделки «купить на одной планете — продать на другой».

Если застрянешь — разбери задачу с AI-партнёром на codepal.ru: он не даёт готовый ответ, а подводит к решению вопросами.


Что дальше

На следующей тренировке остаёмся на уровне CF ~1200–1300 и добавляем ещё два инструмента в связке: бинарный поиск по отсортированному массиву (не по ответу, а напрямую по значениям) и решето Эратосфена для быстрой проверки чисел на простоту.

В серии: Летняя подготовка: разогрев

  1. 1Тренировочная сессия 1: разбор случаев на строке и сортировка при развороте гравитации
  2. 2Тренировочная сессия 2: целочисленная арифметика без ошибок округления
  3. 3Тренировочная сессия 3: первая жадность и сортировка по остаткам
  4. 4Тренировочная сессия 4: сортировка, бинарный поиск и суффиксные массивы
  5. 5Тренировочная сессия 5: бинарный поиск по ответу и монотонный предикат — первое знакомство
  6. 6Тренировочная сессия 6: прямая формула и префиксные суммы на отсортированном массиве — эта статья
  7. 7Тренировочная сессия 7: бинарный поиск по массиву и решето Эратосфена
  8. 8Тренировочная сессия 8: сортировка с бинарным поиском и хэш-таблица — мост к плато

Попробуй разобрать похожие задачи

В CodePal AI-партнёр подсказывает идею, а не ответ. Разбор в диалоге, код проверяется в браузере.