Прыжки по блокам
Из каждой клетки заранее известно, куда она выведет за пределы своего блока и за сколько шагов. Путь длиной n проходится за корень.
2 мин
Задача: в каждой клетке написано, на сколько прыгать вперёд. Надо быстро отвечать, за сколько прыжков шарик вылетит за край, - при этом значения меняются.
Наивный проход - на запрос. Двоичные подъёмы дали бы , но плохо переживают изменения: одно изменение портит таблицу для многих клеток.
Предпосчёт выхода из блока
Для каждой клетки храним три числа:
- - первая клетка вне блока, куда мы попадём, прыгая из ;
- - сколько прыжков для этого нужно;
- - последняя клетка внутри блока перед этим выходом.
Считается это одним проходом справа налево внутри блока: если прыжок из уже выводит за блок, то ответ тривиален, иначе он берётся у клетки , которая лежит правее и уже посчитана.
for (int i = right; i >= left; i--) {
int to = i + a[i];
if (to > right) { next[i] = to; steps[i] = 1; last[i] = i; }
else { next[i] = next[to]; steps[i] = steps[to] + 1; last[i] = last[to]; }
}
Порядок обязателен: слева направо не сработает, потому что нужная клетка окажется ещё не посчитанной.
Запрос и обновление
Запрос: прыгаем не по клеткам, а по блокам. Каждый прыжок по выводит из текущего блока, значит их не больше .
Обновление: изменили - испортился только его блок, потому что внутри блока зависит лишь от клеток того же блока. Пересчитываем блок за .
Оба действия - .
Почему это общий приём
Схема работает для любого «прыжка вперёд по фиксированному правилу»: клетки с шагами, ссылки на следующий элемент, переходы автомата. Существенны два свойства:
- прыжок всегда вперёд - иначе предпосчёт справа налево не замкнётся;
- изменение одной клетки портит только её блок.
Если прыжки могут вести назад, появляются циклы, и приём ломается: выход из блока может не существовать.