Где применяется
1. Задача
У тебя календарь бронирований. В начале у каждого дня ноль гостей. Одна бронь занимает дни с 3-го по 9-й на 4 гостей, и ты прибавляешь 4 к каждому из этих семи дней. Другая бронь занимает дни со 2-го по 20-й на 2 гостей, и ты прибавляешь 2 к девятнадцати дням. И так тысячи броней.
Первое, что приходит в голову: для каждой брони пройти по её дням и прибавить гостей. Одна бронь на 300 дней это 300 прибавлений. Десять тысяч таких броней дают миллионы записей, а перебронирование ты ещё даже не проверял.
Итоговые числа по дням ты читаешь только один раз, в конце. До этого ты просто накладываешь прибавления по диапазонам друг на друга. Есть способ записать каждое такое прибавление двумя пометками, а потом за один проход превратить все пометки в итоговые числа.
2. Простыми словами
Возьми 5 дней, с 0-го по 4-й, и бронь «+2 гостя на дни с 1-го по 3-й». Вместо трёх прибавлений ты оставляешь две пометки. В день 1 пишешь «+2»: отсюда гостей стало на 2 больше. В день 4, сразу после конца брони, пишешь «-2»: отсюда эти гости ушли.
Таких пар пометок можно поставить сколько угодно, для любых броней. В самом конце ты один раз проходишь дни с начала и ведёшь итог всех встреченных пометок. День 0: 0. Дни 1, 2, 3: по 2. День 4: снова 0. Две пометки на бронь и один проход в конце.
3. Идея
Заведи вспомогательный массив diff на одну ячейку длиннее данных, заполненный нулями. Чтобы прибавить val на диапазоне [l, r], поставь две пометки: diff[l] += val и diff[r+1] -= val. Пометка +val включает прибавку начиная с l. Пометка -val выключает её сразу после r.
На одно обновление больше ничего не нужно. Две записи при любой ширине диапазона.
Чтобы получить настоящие значения, пройди слева направо и веди накопленную сумму: running += diff[i], затем result[i] = running. Накопленная сумма несёт каждое +val вперёд от места, где оно включилось, и теряет его в r+1. Поэтому прибавку получают ровно дни с l по r.
Вычитай в r+1, а не в r. Прибавка должна действовать по день r включительно, поэтому выключается она на одну ячейку дальше. Если выключить её в r, последний день каждого диапазона недополучит прибавку.
Проследим на n = 5 с обновлениями [[1,3,2], [2,4,1]].
- Обновление
[1,3,2]:diff[1] += 2,diff[4] -= 2. - Обновление
[2,4,1]:diff[2] += 1,diff[5] -= 1. - Теперь
diff = [0, 2, 1, 0, -2, -1]. - Накопленная сумма: 0, потом 0 + 2 = 2, потом 2 + 1 = 3, потом 3 + 0 = 3, потом 3 - 2 = 1. Значит,
result = [0, 2, 3, 3, 1].
Проверь вручную: +2 на дни с 1-го по 3-й и +1 на дни со 2-го по 4-й дают ровно это.
4. Во что это обходится
Каждое обновление это две записи: O(1). Для u обновлений выходит O(u). Последний проход обходит каждую ячейку один раз: O(n). Всего O(n + u). Память O(n) на массив diff.
Если прибавлять к каждому элементу вручную, это O(n) на обновление и O(n·u) всего. Пометки на двух краях убирают этот множитель.
Есть одно условие: способ работает, когда сначала идут все записи, а потом чтение. Если чтения и записи чередуются (запись, чтение, запись, чтение), простой массив разностей не справится. Для этого нужны дерево Фенвика или дерево отрезков, до них в курсе ещё далеко.
5. Где это встречается
Занятость мест и дней в календаре с проверкой вместимости, поиск перебронирования рейсов и отелей. Применение множества правил вида «прибавить N на этом диапазоне IP-адресов» или «поднять цены в этом ценовом диапазоне», после которых результат читают один раз. Тепловая карта, на которую наносят тысячи диапазонов, а потом один раз рисуют. Везде, где сначала накапливается пачка прибавлений по диапазонам, а итоги смотрят потом.
applyRangeUpdates(6, [[1, 3, 2], [2, 5, 3], [0, 1, 1]])Применяем 3 обновления отрезков к 6 нулям. diff на одну ячейку длиннее, чтобы для r + 1 всегда было место.
Столбики: 40 ячеек. Массив разностей пишет отметки в отдельный массив, поэтому его столбики меняются только в последнем проходе. Обновлений столько же, сколько ячеек, и здесь, и в таблице ниже.
Одна операция: одна запись. За каждый кадр все дорожки делают одинаковое число операций, поэтому медленная отстаёт.
O(n·u)O(n + u)
Операции для n чисел и примерное время при 100 миллионах простых операций в секунду. Это оценка по тому же счёту, а не замер.
Как узнать
Эти слова в условии значат этот паттерн
- сначала много обновлений диапазонов, затем один раз прочитать итоговый массив
- прибавить значение к каждому элементу диапазона, повторить много раз
- применить k приращений диапазона перед тем как отвечать на запросы
- счётчики бронирований/интервалов по массиву
Похоже, но не то
- Префиксные суммы (Prefix sums)
Префиксные суммы читают много отрезков из неизменных данных. Здесь зеркально: много записей, одно чтение.
- Линия развёртки (подсчёт событий) (Sweep line (event counting))
Развёртка сортирует события, когда координаты огромны или разрежены. Здесь они прямо индексируют массив.
- Дерево Фенвика / дерево отрезков (Fenwick tree / segment tree)
Дерево Фенвика отвечает на запросы между обновлениями. Массив разностей отвечает только после последнего.
- Скользящее окно (фиксированный размер) (Sliding window (fixed size))
Окно ведёт один отрезок постоянной ширины. Здесь отрезки разной ширины и свободно накладываются.
n до 1e6 и m обновлений на отрезках дают O(n + m). Обновление это две записи, сборка это один проход.
На LeetCode
Задачи, где много обновлений прибавляют значение целому диапазону, а результат читается один раз в конце, например брони, поездки и сдвиги букв. Отметь два конца каждого диапазона и сложи отметки за один проход.
Прибавь 1 в начале каждого диапазона и вычти 1 сразу после конца, потом проверь, что каждое число от left до right покрыто.
Каждая бронь и есть обновление [l, r, val] из урока, только рейсы считаются с 1.
Пассажиры садятся в точке from и выходят в to, и бегущая сумма ни разу не должна превысить capacity.
Каждый сдвиг прибавляет или вычитает 1 на диапазоне позиций, а итог заворачивается по алфавиту через % 26.
Твоя задача (Tier 1)
Напиши функцию applyRangeUpdates(n, updates).
Начни со списка из n нулей. Каждое обновление прибавляет значение к диапазону позиций. Примени все обновления и верни итоговый список. Приём такой: помечай только два конца каждого диапазона, а потом сложи пометки за один проход. Тогда даже много обновлений стоят дёшево.
- Вход:
n, длина списка, иupdates, список обновлений. Каждое обновление имеет вид[l, r, val]: прибавьvalк каждой позиции отlдоr,rтоже входит. Пример:n = 5, обновления[[1, 3, 2], [2, 4, 1]]. - Возвращает: итоговый список из
nчисел.
Шаги:
- Создай вспомогательный список
diffизn + 1нулей. Лишняя ячейка в конце принимает пометкуr + 1, когда диапазон доходит до последней позиции. - Для каждого обновления
[l, r, val]прибавьvalвdiffна позицииlи вычтиvalна позицииr + 1. - Иди по
diffслева направо и веди накопленную сумму. Записывай её в каждую позицию списка результата.
Готово, когда проходят все тесты:
n = 5,[[1,3,2],[2,4,1]]->[0, 2, 3, 3, 1](два диапазона пересекаются)n = 3,[[0,2,5]]->[5, 5, 5](диапазон покрывает весь список)n = 4,[[2,2,7]]->[0, 0, 7, 0](одна позиция)n = 4,[[0,1,1],[1,3,1]]->[1, 2, 1, 1](оба диапазона включают позицию 1)n = 3,[]->[0, 0, 0](нет обновлений, все нули)n = 1,[[0,0,9]]->[9](одна позиция)
Практика
applyRangeUpdates
applyRangeUpdates(n, updates) -> number[]
export function applyRangeUpdates(n, updates) {
// Your code here. The task is described in the box above the editor.
}
Ты здесь как гость
Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.
Лестница подсказок
Застрял? Открывай по одной, но сначала попробуй сам.