Все темы
27

Поднять весь диапазон, не трогая каждую ячейку

Tier 1Бесплатно

Где применяется

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, последний день каждого диапазона недополучит прибавку.

diff (+2 на позиции 1, -2 на позиции 4): 0 +2 0 0 -2 0 после прохода, result: 0 2 2 2 0

Проследим на 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]])
diff (изменение в каждой точке)
0
0
0
0
0
0
0
result
running = 0

Применяем 3 обновления отрезков к 6 нулям. diff на одну ячейку длиннее, чтобы для r + 1 всегда было место.

Гонка скорости
40 прибавлений на отрезках к одним и тем же 40 ячейкам

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

Одна операция: одна запись. За каждый кадр все дорожки делают одинаковое число операций, поэтому медленная отстаёт.

Прибавление к каждой ячейке O(n·u)
операций: 0
Массив разностей O(n + u)
операций: 0

 

А если чисел больше

Операции для n чисел и примерное время при 100 миллионах простых операций в секунду. Это оценка по тому же счёту, а не замер.

n = 1 000
Прибавление к каждой ячейке334,3 тыс. · 3,3 мс
Массив разностей3 тыс. · меньше 1 мс
n = 100 000
Прибавление к каждой ячейке3,3 млрд · 33 с
Массив разностей300 тыс. · 3 мс
n = 10 000 000
Прибавление к каждой ячейке33,3 трлн · 3,9 дн.
Массив разностей30 млн · 300 мс

Как узнать

Массив разностей (Difference array)

Эти слова в условии значат этот паттерн

  • сначала много обновлений диапазонов, затем один раз прочитать итоговый массив
  • прибавить значение к каждому элементу диапазона, повторить много раз
  • применить k приращений диапазона перед тем как отвечать на запросы
  • счётчики бронирований/интервалов по массиву

Похоже, но не то

n до 1e6 и m обновлений на отрезках дают O(n + m). Обновление это две записи, сборка это один проход.

На LeetCode

Задачи, где много обновлений прибавляют значение целому диапазону, а результат читается один раз в конце, например брони, поездки и сдвиги букв. Отметь два конца каждого диапазона и сложи отметки за один проход.

Твоя задача (Tier 1)

Напиши функцию applyRangeUpdates(n, updates).

Начни со списка из n нулей. Каждое обновление прибавляет значение к диапазону позиций. Примени все обновления и верни итоговый список. Приём такой: помечай только два конца каждого диапазона, а потом сложи пометки за один проход. Тогда даже много обновлений стоят дёшево.

  • Вход: n, длина списка, и updates, список обновлений. Каждое обновление имеет вид [l, r, val]: прибавь val к каждой позиции от l до r, r тоже входит. Пример: n = 5, обновления [[1, 3, 2], [2, 4, 1]].
  • Возвращает: итоговый список из n чисел.

Шаги:

  1. Создай вспомогательный список diff из n + 1 нулей. Лишняя ячейка в конце принимает пометку r + 1, когда диапазон доходит до последней позиции.
  2. Для каждого обновления [l, r, val] прибавь val в diff на позиции l и вычти val на позиции r + 1.
  3. Иди по 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.
}

Ты здесь как гость

Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.

Лестница подсказок

Застрял? Открывай по одной, но сначала попробуй сам.

    Мы используем необходимые куки для работы сайта (вход и язык). Если ты согласишься, мы также загрузим Google Analytics, чтобы видеть, какие страницы читают, и Google reCAPTCHA для защиты форм обратной связи и сообщений об ошибке от спама. Политика конфиденциальности