Где применяется
1. Задача
В последних уроках окно двигалось по списку, а сумма обновлялась на ходу. Это работает, когда окно одно и движется в одну сторону. С дашбордом иначе. Он задаёт сотни вопросов, и каждый про свой кусок списка, никак не связанный с другими. Скользящее окно тут не поможет.
У тебя дашборд с метриками. На каждую минуту одно число: запросы, байты, ошибки, что угодно. Пользователь двигает ползунок времени и спрашивает: «сколько всего запросов было с 40-й минуты по 90-ю?» Ты складываешь эти 51 число и показываешь ответ.
Потом он двигает ползунок снова: с 5-й минуты по 800-ю. Ты складываешь 796 чисел. Он двигает ещё раз, и ты снова складываешь новый кусок с нуля. Ползунок выдаёт новый диапазон через каждые несколько пикселей движения мыши. Каждый раз ты заново складываешь тысячи чисел. А можно отвечать на любой вопрос «сумма отсюда досюда» одним вычитанием, какой бы длины ни был диапазон.
2. Простыми словами
На некоторых чеках рядом с каждым товаром печатают ещё и итог на этот момент. Допустим, нужно узнать, сколько вместе стоят три товара из середины чека. Складывать их заново не нужно. Возьми итог, напечатанный после третьего из них, и вычти итог, напечатанный прямо перед первым. Разница и есть цена этих трёх товаров.
На этом всё и держится. Один раз запиши итог на каждой позиции. После этого сумма любого куска равна одному итогу минус другой. Это одно вычитание, какой бы длины ни был кусок.
3. Идея
Пройди список один раз и веди накопленную сумму: после каждого числа записывай сумму всех чисел до него включительно. Назови этот список накопленных сумм prefix.
Главное правило: сумма куска равна накопленной сумме в его конце минус накопленная сумма прямо перед его началом. Всё, что стоит до начала куска, при вычитании пропадает. Остаётся ровно тот кусок, который ты просил.
Чтобы индексы сходились, начни список с нуля, ещё до первого числа. Тогда prefix[0] = 0, а каждая следующая ячейка равна предыдущей плюс очередное число: prefix[i] = prefix[i-1] + nums[i-1]. Теперь prefix[i] означает «сумма первых i чисел». Сумма диапазона от l до r (оба конца включены) равна:
prefix[r+1] - prefix[l]
Благодаря начальному нулю диапазон, который начинается с самой первой позиции, считается по той же формуле, без отдельного случая. Без этого нуля каждый диапазон съехал бы на одну позицию.
Проследим на nums = [2, 4, 1, 3].
- Строим
prefix: 0, потом 0 + 2 = 2, потом 2 + 4 = 6, потом 6 + 1 = 7, потом 7 + 3 = 10. Получаемprefix = [0, 2, 6, 7, 10]. - Диапазон
[1, 3](числа 4, 1, 3):prefix[4] - prefix[1] = 10 - 2 = 8. Проверка: 4 + 1 + 3 = 8. - Диапазон
[0, 0](только 2):prefix[1] - prefix[0] = 2 - 0 = 2.
Ничего не пришлось складывать заново.
4. Во что это обходится
Массив prefix строится за один проход по данным: O(n). После этого каждый вопрос про диапазон решается одним вычитанием: O(1). Для q вопросов всё вместе стоит O(n + q).
Память O(n) на сам массив prefix. Ты тратишь место размером с копию данных, и за это вопросы становятся дешёвыми.
Выигрыш заметен, когда вопросов много. Если складывать каждый диапазон вручную, это O(n) на вопрос и O(n·q) всего. Если один раз посчитать prefix, каждый вопрос стоит постоянное время. Разницу видно уже на одном ползунке дашборда.
5. Где это встречается
Дашборды временных рядов и сводки мониторинга считают сумму за любой диапазон дат и не перечитывают исходные строки. Так же работают накопительные отчёты по выручке и использованию. И любой аналитический API, где одни и те же данные запрашивают по множеству разных диапазонов.
Двумерная версия называется «интегральное изображение». Так фильтры изображений считают сумму любого прямоугольника за постоянное время. Это ускоряет размытие и поиск признаков при распознавании лиц. Приём тот же, только измерений на одно больше.
rangeSum(buildPrefix([3, 1, 4, 1, 5, 9]), 1, 4)Строим prefix на одну ячейку длиннее arr. prefix[0] это сумма ничего, поэтому там 0.
Столбики: 40 значений. Префиксные суммы сначала превращают их в накопленные итоги. Запросов столько же, сколько значений, и здесь, и в таблице ниже.
Одна операция: одно чтение значения. За каждый кадр все дорожки делают одинаковое число операций, поэтому медленная отстаёт.
O(n·q)O(n + q)
Операции для n чисел и примерное время при 100 миллионах простых операций в секунду. Это оценка по тому же счёту, а не замер.
Как узнать
Эти слова в условии значат этот паттерн
- много запросов на сумму по диапазону
- ответить на несколько вопросов о сумме диапазона в одном массиве
- сумма от индекса l до r
- накопленная или кумулятивная сумма
Похоже, но не то
- Скользящее окно (фиксированный размер) (Sliding window (fixed size))
Окно отвечает про один движущийся отрезок. Префиксные суммы отвечают про любой названный отрезок.
- Массив разностей (Difference array)
Это зеркальный случай: много записей на отрезках, одно чтение в конце. Здесь наоборот, много чтений.
- Дерево Фенвика / дерево отрезков (Fenwick tree / segment tree)
Префиксный массив ломается при первом же изменении значения. Дерево Фенвика платит log n за право менять.
- Хеш-множество / словарь (Hash set / map)
У словаря нет понятия отрезка вообще. Для подсчёта подмассивов рядом нужна накопленная сумма.
n до 1e6 и много запросов на отрезках дают O(n) на подготовку. Каждый запрос стоит O(1), плюс O(n) памяти.
На LeetCode
Задачи, где нужно много раз узнать сумму отрезка или сравнить то, что слева от индекса, с тем, что справа. Один раз построй префиксные суммы и отвечай на каждый вопрос одним вычитанием.
Задача урока в виде класса, prefix строится в конструкторе, а sumRange делает вычитание.
Ответ и есть массив prefix, только без нуля в начале.
На каждом индексе сравни сумму слева с общей суммой минус сумма слева минус само число.
Та же идея с произведениями, один проход слева и один справа.
Твоя задача (Tier 1)
Напиши функцию rangeSum(nums, queries).
Нужно быстро отвечать на много вопросов вида «сумма с позиции l по позицию r». Один раз построй вспомогательный список накопленных сумм, потом отвечай на каждый вопрос одним вычитанием.
- Вход:
nums, список чисел, иqueries, список пар[l, r](начальная и конечная позиция). Пример:[2, 4, 1, 3],[[1, 3]]. - Возвращает: по одной сумме на каждый запрос, то есть сумму
numsс позицииlпо позициюr.
Шаги:
- Построй вспомогательный список
prefix. Начни его с0. Затем каждое число прибавляй к последнему значению:prefix[i] = prefix[i-1] + nums[i-1]. Такprefix[i]будет суммой первыхiчисел. - Для каждой пары
[l, r]изqueriesсумма сlпоrравнаprefix[r+1] - prefix[l]. Добавь её в список ответов. - Верни список ответов. Начальный
0вprefixнужен, чтобы каждый диапазон считался верно.
Готово, когда проходят все тесты:
[2, 4, 1, 3],[[1,3], [0,0], [0,3]]->[8, 2, 10][5, 6, 7],[[1,1]]->[6](одна позиция)[4],[[0,0]]->[4][],[]->[][1, 2, 3],[]->[](нет запросов)
Практика
rangeSum
rangeSum(nums, queries) -> number[]
export function rangeSum(nums, queries) {
// Your code here. The task is described in the box above the editor.
}
Ты здесь как гость
Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.
Лестница подсказок
Застрял? Открывай по одной, но сначала попробуй сам.