← Параллель A'Все параллели⬇ Скачать конспект

Занятие 1. Параллель A' — Корневые эвристики

Блочная декомпозиция (sqrt-decomposition), тяжёлые/лёгкие вершины, корневая по запросам, алгоритм Мо (в том числе на дереве, с откатами и 3D).
Лекция ≈ 4 ч 32 мин Язык кода: C++

1. Разминка: делители за $O(\sqrt n)$ 0:03:00

Чтобы найти все натуральные делители числа $n$, перебираем $x$ от $1$ до $\sqrt n$: если $n$ делится на $x$, то делителями являются и $x$, и $n/x$.

Если $n$ делится на $x$, то либо $x \le \sqrt n$, либо $n/x \le \sqrt n$.

Поэтому все делители находятся за $O(\sqrt n)$. Аналогично факторизация: перебираем $x$, пока $n$ делится на $x$ — записываем простой множитель. После перебора останется либо $1$, либо один большой простой делитель $> \sqrt n$ (такой может быть не более одного).

Факт про число делителей
Число делителей у $n$ — не более $O(\sqrt n)$ (грубая оценка); этого достаточно для спортивного программирования. Делители ищутся за $O(\sqrt n)$, и это «корневая эвристика» в чистом виде.

2. Блочная декомпозиция массива 0:07:00

Задача. Дан массив длины $n$. Запросы двух типов:

  1. add(l, r, x) — прибавить $x$ ко всем элементам отрезка $[l, r]$;
  2. sum(l, r) — сумма на отрезке $[l, r]$.

Идея

Разобьём массив на блоки размера $K$ (последний блок может быть неполным). Для каждого блока храним две величины:

Обработка запроса add(l, r, x)

Отрезок разбивается на три части: префикс, несколько полных блоков и суффикс.

add(l, r, x) на блоках: n = 20, K = 4, запрос [2, 17]Отрезок распадается на хвост слева, целые блоки и хвост справа.блок 0блок 1блок 2блок 3блок 4012345678910111213141516171819хвостa[i] += xпо одному3 целых блокаadd[блок] += x — O(1) на блок,элементы не трогаемхвостa[i] += xпо одномуХвосты — не больше 2K элементов, блоков — не больше n/K. При K ≈ √n запрос стоит O(√n).
Хвосты обновляем поэлементно, а целые блоки — одной пометкой add; так запрос стоит $O(K + n/K)$.

Значение элемента и сумма

Настоящее значение $a_i = a_i^{\text{исходное}} + \text{add}[\text{блок}(i)]$.

Сумма на отрезке: префикс/суффикс считаем по элементам, а для полного блока $j$ прибавляем $\text{sum}[j] + \text{add}[j] \cdot \text{len}[j]$.

Сложность

$O\left(K + \dfrac{n}{K}\right)$ на запрос; при $K \approx \sqrt n$ это $O(\sqrt n)$.

При $n = 10^5$ это $\approx 300$–$900$ операций на запрос — «прибавление на отрезке» за $\sqrt n$ это очень хорошо.

C++
const int K = 320;  // размер блока ≈ sqrt(n); константа компилируется быстрее

void add(int l, int r, long long x) {
    int bl = l / K, br = r / K;
    if (bl == br) {
        for (int i = l; i <= r; ++i) { a[i] += x; blockSum[bl] += x; }
        return;
    }
    for (int i = l; i < (bl + 1) * K; ++i) { a[i] += x; blockSum[bl] += x; }
    for (int b = bl + 1; b < br; ++b) blockAdd[b] += x;          // ленивая добавка
    for (int i = br * K; i <= r; ++i) { a[i] += x; blockSum[br] += x; }
}

long long sum(int l, int r) {
    long long res = 0;
    int bl = l / K, br = r / K;
    if (bl == br) {
        for (int i = l; i <= r; ++i) res += a[i] + blockAdd[bl];
        return res;
    }
    for (int i = l; i < (bl + 1) * K; ++i) res += a[i] + blockAdd[bl];
    for (int b = bl + 1; b < br; ++b) res += blockSum[b] + blockAdd[b] * K;
    for (int i = br * K; i <= r; ++i) res += a[i] + blockAdd[br];
    return res;
}
Практика
Для массивов используют обычный int a[N] (быстрее вектора, меньше накладных расходов). Размер блока выносят в константу: компилятор заменяет деление на константу быстрыми операциями. При $n$ до $\sim 300$–$500$ тысяч корневая на массиве работает; при $10^6$ — уже на грани.

3. Тяжёлые и лёгкие вершины 0:22:00

Задача. В графе с $n$ вершинами и $m$ рёбрами посчитать количество треугольников (троек попарно соединённых вершин).

Идея. Назовём вершину лёгкой, если её степень $\le K$ (порядка $\sqrt m$), иначе тяжёлой.

Тяжёлых вершин не больше $\dfrac{2m}{K}$: каждая имеет степень $> K$, а сумма степеней $= 2m$. При $K=\sqrt m$ тяжёлых $O(\sqrt m)$.

Разделим треугольники на два случая:

Второй способ (переориентация рёбер) 0:31:00

Отсортируем вершины по возрастанию степени. Направим каждое ребро от вершины с меньшим индексом в сортировке к большей (то есть «вперёд»). Тогда:

Теперь у каждой вершины исходящих рёбер $O(\sqrt m)$. Считаем треугольники: для $v$ перебираем $u \in g[v]$, $w \in g[u]$ (оба по ориентированным рёбрам) и проверяем ребро $v$–$w$. Сложность $O(m\sqrt m)$.

Идея приёма
Сортировка вершин по степени и ориентация рёбер «вперёд» делают исходящие степени всех вершин маленькими ($O(\sqrt m)$) — этот трюк часто встречается в задачах на графы.

4. Прибавление к соседям вершины 0:42:00

Задача. Дан граф, во всех вершинах нули. Запросы:

  1. add(v, x) — прибавить $x$ ко всем соседям вершины $v$;
  2. get(v) — узнать значение в вершине $v$.

Решение (тяжёлые/лёгкие).

При get(v) нужно учесть два вклада:

$value[v] = base[v] + \sum_{\substack{h \text{ тяжёлая},\\ (h,v)\in E}} lazy[h]$.

Тяжёлых вершин $O(\sqrt m)$, поэтому суммирование по ним — быстрое.

текст
  add(v, x):
    если v лёгкая:   для каждого соседа u: base[u] += x
    если v тяжёлая:  lazy[v] += x

  get(v):
    ans = base[v]
    для каждой тяжёлой h, соседней с v: ans += lazy[h]
    return ans

5. Строки: немного различных длин 0:45:00

Задача. Дана строка $T$ и запросы-строки $S_i$. Для каждой $S_i$ посчитать, сколько раз она встречается в $T$ как подстрока. Суммарная длина всех $S_i$ равна $L$.

Ключевое наблюдение. Различных длин среди строк $S_i$ немного:

Если есть $k$ различных длин, то их сумма $\ge 1+2+\dots+k = \tfrac{k(k+1)}{2} \le L$, откуда $k = O(\sqrt L)$.

Способ 1 (онлайн, хэши). Когда приходит строка длины $\ell$, которой ещё не было: выписываем хэши всех подстрок $T$ длины $\ell$ и кладём в map (или бор). Если длина уже была — просто смотрим в map. Суммарно получается $O(|T|\sqrt L)$ хэшей подстрок (различных длин $O(\sqrt L)$, для каждой — $O(|T|)$ подстрок), плюс $O(1)$ амортизированно на каждый запрос.

Способ 2 (бор). Для коротких строк ($\le \sqrt L$) можно построить бор всех их и искать; длинные строки ($> \sqrt L$) немногочисленны — их обрабатываем честно (например, префикс-функцией за $O(|S|+|T|)$).

6. Разноцветные рёбра (удаление) 0:56:00

Задача. Дан граф, вершины покрашены в белый/чёрный. Запросы: перекрасить вершину; найти и удалить любое ребро с концами разного цвета.

Это сложная задача. Основная идея — опять тяжёлые/лёгкие вершины:

Второе (безыдейное) решение — корневая на массиве. Перенумеруем рёбра так, чтобы почти все рёбра тяжёлой вершины шли подряд. Для каждого ребра храним бит «разноцветное». При перекраске вершины обновляем этот бит (для тяжёлой — отрезковым присваиванием, для лёгкой — точечно). Чтобы найти любое разноцветное ребро — держим по блокам бит «есть ли единичка в блоке» и ищем первый блок с единичкой, внутри — первое ребро. Всё за $O(\sqrt m)$.

7. Корневая по запросам 1:23:00

Задача. Есть запросы «изменить значение в точке» и «сумма на отрезке». Все запросы известны заранее (оффлайн).

Идея. Разобьём последовательность запросов на блоки по $K$ штук. Обрабатываем блоки независимо:

Перед блоком применяем все изменения предыдущих блоков и строим «актуальный» массив; по нему строим префиксные суммы.
Для запроса суммы внутри блока: берём «догадку» из префиксных сумм и уточняем её, перебрав $\le K$ изменений текущего блока, которые затрагивают отрезок.
В конце блока откатываем временные изменения.

Сложность: $O\!\left(\dfrac{q}{K} \cdot n + q \cdot K\right)$; при $K = \sqrt n$ — $O(q\sqrt n)$.

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

8. Ближайшая чёрная вершина (корневая по запросам + BFS) 1:33:00

Задача. Дано дерево, все вершины белые. Запросы: покрасить вершину в чёрный; для вершины $v$ узнать расстояние до ближайшей чёрной.

Решение. Делим запросы на блоки по $K$:

Сложность: $\dfrac{q}{K}\cdot n + q \cdot K$. Чтобы выбрать $K$, приравниваем слагаемые:

$\dfrac{q}{K}\cdot n = q\cdot K \;\Rightarrow\; K = \sqrt n$, итого $O(q\sqrt n)$.

Почему именно $K=\sqrt n$, а не $\sqrt q$ 1:44:00

Это следует из неравенства о средних: $x + y \ge 2\sqrt{xy}$. Для двух слагаемых $\dfrac{qn}{K}$ и $qK$:

$\dfrac{qn}{K} + qK \ge 2\sqrt{q^2 n} = 2q\sqrt n$, и равенство при $K = \sqrt n$.

То есть минимум суммы достигается, когда слагаемые равны, и правильный выбор — $K = \sqrt n$. Это работает при любом соотношении $n$ и $q$.

Бонус
Если разрешить перекрашивать чёрные обратно в белые, нужно в начале блока исключить из BFS вершины, которые изменят цвет внутри блока (они станут «новыми» вершинами, которые перебираем по одной). Это требует заглянуть в будущее — поэтому работает только оффлайн.

9. Алгоритм Мо 1:58:00

Задача. Дан массив и $q$ запросов «mex на подотрезке $[l,r]$». MEX — минимальное неотрицательное число, не встречающееся на отрезке.

Идея Мо

Пусть есть структура данных $S$, которая хранит множество и умеет: (1) вычислять ответ, (2) добавлять/удалять элемент. Для mex можно поддерживать cnt[x] (сколько раз $x$ в множестве) и set отсутствующих чисел; mex — минимум этого set.

Расположим запросы (отрезки $[l_i, r_i]$) в некотором порядке. От запроса к запросу будем двигать границы $l$ и $r$, добавляя/удаляя элементы:

C++
while (L > l) add(--L);
while (R < r) add(++R);
while (L < l) remove(L++);
while (R > r) remove(R--);
// ответ = текущее значение в структуре S

Стоимость перехода от отрезка $[l_1,r_1]$ к $[l_2,r_2]$ равна $|l_2-l_1| + |r_2-r_1|$ — это манхэттенское расстояние между точками $(l_i, r_i)$ на плоскости. Нужно упорядочить точки так, чтобы суммарное расстояние было мало (задача коммивояжёра в манхэттенской метрике).

Порядок запросов (классический Мо)

Возьмём $B$ и отсортируем запросы по паре $\left(\lfloor l/B \rfloor, r\right)$: сначала по номеру «полосы» по $l$, внутри полосы — по возрастанию $r$.

Порядок Мо: полосы по l, внутри полосы — по rКаждая точка — запрос (l, r). Линия показывает, в каком порядке их обработать.полоса 0полоса 1полоса 2полоса 3l →r↑04812123456789101112131415Внутри полосыr идёт только вверхили только вниз:O(n) на полосу.l прыгает лишьвнутри полосы:≤ B на запрос.«Змейка»чётные полосы — вверх,нечётные — вниз,r не бегает назад.
Сортировка по $(\lfloor l/B\rfloor, r)$ «змейкой»: $r$ монотонен внутри полосы, $l$ двигается не дальше $B$.

Оценка. Движение по $r$: внутри полосы суммарно $O(n)$, полос $n/B$, итого $O(n^2/B)$; между полосами ещё $O(n/B \cdot n)$. Движение по $l$: внутри полосы $\le B$ на переход, итого $O(qB)$.

$O\!\left(qB + \dfrac{n^2}{B}\right)$; при $B = \dfrac{n}{\sqrt q}$ — $O(n\sqrt q)$.

«Змейка» 2:34:00

Чтобы не возвращаться в начало каждой полосы, идём по нечётным полосам сверху вниз. Компаратор: домножаем $r$ на $(-1)^{\lfloor l/B \rfloor}$. Асимптотика та же, но константа сильно лучше.

Кривая Гильберта 2:36:00

Ещё более эффективный порядок — кривая Гильберта (гамильтонов путь по квадрату $2^k\times2^k$, обходящий все клетки). Есть функция hilbertOrder(x, y), вычисляющая номер клетки по координатам за $O(\log)$ битовыми операциями (рекурсивно по 4 подквадратам). Точки, соседние по номерам Гильберта, близки и по манхэттенскому расстоянию — константа ещё лучше (в 2–3 раза быстрее обычного Мо).

Пункт Б: ускоряем структуру данных 2:49:00

В алгоритме Мо запросов первого типа (вычислить ответ) — $q$, а второго (добавить/удалить) — $O(n\sqrt q)$. Значит, вычисление ответа можно позволить себе делать медленнее, чем добавление.

Для mex: разобьём массив cnt[0..n] на блоки размера $B$; для каждого блока храним, сколько в нём нулей. Добавление/удаление — $O(1)$; поиск минимального $x$ с cnt[x]==0 — находим первый блок с ненулевым числом нулей ($O(n/B)$), внутри блока — $O(B)$. Взяв $B=\sqrt n$, получаем mex за $O(\sqrt n)$, добавление за $O(1)$, итого $O(n\sqrt q)$.

10. Mex на пути: Мо на дереве 2:59:00

Задача. На рёбрах дерева записаны числа; нужно отвечать на mex на пути между двумя вершинами.

Эйлеров обход. Делаем DFS, записывая вершину каждый раз, когда в неё заходим. Получится путь длины $2n-1$. Каждое ребро встречается в нём ровно два раза (туда и обратно).

Мо на дереве: путь 3 → 8 из эйлерова обходаБерём кусок обхода от первого входа в 3 до первого входа в 8 и смотрим на рёбра-переходы.12534678sfэйлеров обход:1232421567686513–22–44–22–11–55–66–77–66–8↑ первый вход в 3первый вход в 8 ↑Рёбра 2–4 и 6–7 встретились дважды (туда и обратно) — они сокращаются.Рёбра, встретившиеся нечётное число раз, — это ровно путь 3 → 2 → 1 → 5 → 6 → 8.
На отрезке обхода между первыми входами в $s$ и $f$ рёбра пути встречаются нечётно, остальные — чётно.

Трюк с чётностью. Для пути от $s$ до $f$ возьмём первое вхождение $s$ и первое вхождение $f$ в Эйлеровом обходе. На получившемся подотрезке рёбра пути встречаются нечётное число раз, а все остальные — чётное (2 или 0). Поэтому, поддерживая в структуре только рёбра с нечётным количеством вхождений (добавление/удаление ребра меняет его «чётность»), мы получаем ровно рёбра пути.

Дальше — обычный алгоритм Мо по этому Эйлерову массиву длины $2n$.

11. Инверсии на пути: общий метод 3:12:00

Задача. В вершинах дерева записаны числа; для пути $s$–$f$ нужно посчитать количество инверсий в выписанной последовательности.

Сначала на массиве: инверсии на подотрезке считаем, поддерживая структуру (Фенвик/дерево отрезков) и добавляя/удаляя элементы только по краям. При добавлении элемента справа к ответу прибавляем количество чисел в множестве, больших его; слева — меньших.

Общий метод для дерева. Проблемы трюка с чётностью: числа на вершинах, а не рёбрах, и обновление «в середину» пути (не по краям). Решение — честно поддерживать текущий путь в деке:

Преимущество
Этот способ универсален: работает и для чисел на вершинах, и для чисел на рёбрах, в отличие от трюка с чётностью (который — частный случай). Асимптотика та же $O(n\sqrt q)$.

12. Мо с откатами и 3D-Мо 3:52:00

12.1. Количество префиксных максимумов и Мо с откатами 3:52:00

Задача. Для каждого запроса $[l,r]$ посчитать, сколько элементов на отрезке являются префиксными максимумами (строго больше всех предыдущих).

Добавление справа — просто (сравниваем с текущим максимумом); удаление справа — тоже (стек максимумов). Но добавление/удаление слева требует удаления префикса стека — амортизированно, а в обычном Мо амортизация недопустима (откаты ломают оценку).

Мо с откатами. Обрабатываем блоки по $l$ по-отдельности. Для блока:

  1. Стартуем из правой границы блока, двигаем $R$ только вправо (сканлайн), поддерживая стек максимумов для «синей» части.
  2. Для запросов с этим $R$ временно двигаем $L$ влево внутри блока («красная» часть), считаем ответ, затем откатываем красную часть.
  3. Объединяем красную и синюю части бинпоиском (отрезаем из синей префикс, меньший максимума красной).

Сложность: $O\!\left(\dfrac{n}{B}\cdot n + qB\right)$; при $B=\sqrt n$ — $O((n+q)\sqrt n)$.

Почему обычный Мо нельзя
Если структура данных амортизированная (отдельный запрос может стоить $O(n)$), то обычный Мо может деградировать до $O(q n)$. Мо с откатами явно откатывает временные изменения, сохраняя оценку.

12.2. 3D-Мо (с изменениями) 4:11:00

Если к запросам mex на подотрезке добавляются изменения в точке, вводим третью ось — «время». Каждый запрос кодируется тройкой $(l, r, t)$, где $t$ — номер версии массива (после $t$ изменений). Переход между версиями — это применение/откат одного точечного изменения (удалить старое значение, добавить новое) за $O(1)$.

Сортируем запросы по тройке $\left(\lfloor l/B \rfloor, \lfloor r/B \rfloor, t\right)$, где $B = n^{2/3}$:

Сложность: $O\!\left(qB + \dfrac{U n^2}{B^2}\right)$, где $U$ — число изменений; при $B = n^{2/3}$ и $U\approx q$ — $O(q\, n^{2/3})$.

Здесь плоскость $(l, r)$ разбивается на квадраты $B\times B$ («стаканы»); внутри стакана идём по возрастанию времени $t$. На практике полезна «змейка» по чётности координат.