EduBrick

Устройство и построение

Разбиение массива на отрезки, почему массива размера 4n всегда хватает и как построить дерево за O(n).

2 мин

Разобьём массив пополам, каждую половину — пополам, и так до отдельных элементов. Получится дерево, в каждом узле которого хранится ответ для его отрезка.

                 [1..8]
         [1..4]           [5..8]
     [1..2]  [3..4]   [5..6]  [7..8]
     1  2    3  4     5  6    7  8

Всего узлов меньше 2n2n: листьев nn, внутренних — на один меньше. Высота дерева — log2n\lceil \log_2 n \rceil.

Нумерация

Корень — вершина 1, дети вершины vv — это 2v2v и 2v+12v + 1. Такая нумерация избавляет от указателей: всё дерево живёт в одном массиве.

std::vector<long long> tree(4 * n);

void build(int v, int tl, int tr) {
    if (tl == tr) { tree[v] = a[tl]; return; }
    int tm = (tl + tr) / 2;
    build(2 * v, tl, tm);
    build(2 * v + 1, tm + 1, tr);
    tree[v] = tree[2 * v] + tree[2 * v + 1];
}

Построение обходит каждый узел один раз: O(n)O(n).

Почему 4n

Узлов меньше 2n2n, но при такой нумерации номера не идут подряд: если nn не степень двойки, дерево получается неровным, и максимальный номер может превысить 2n2n.

Оценка сверху такая. Достроим nn до ближайшей степени двойки ss; тогда дерево полное, узлов ровно 2s12s - 1, и все номера меньше 2s2s. Поскольку s<2ns < 2n, все номера меньше 4n4n.

Отсюда правило: выделяйте 4n4n и не думайте. Попытка сэкономить и выделить 2n2n приводит к выходу за границы массива: уже при n=6n = 6 максимальный использованный номер равен 13, а 2n=122n = 12.

Оценка 4n4n не запас «на всякий случай», а почти точная граница. Перебор по всем nn до 20 000 даёт наибольшее отношение 3,9543{,}954 — при n=16512n = 16512 максимальный номер равен 65 281. Урезать до 3n3n тоже нельзя: впервые не хватает уже при n=36n = 36, где номер доходит до 113.

Альтернатива: дополнить до степени двойки

Второй способ — сразу дополнить массив нейтральными элементами до длины s=2ks = 2^k. Тогда дерево полное, хватает ровно 2s2s ячеек, а листья лежат в [s,s+n)[s, s + n) и адресуются напрямую.

Это основа итеративной реализации, которая короче и быстрее рекурсивной.

Плата — до вдвое большая память в худшем случае: при n=2k+1n = 2^k + 1 дополнение почти удваивает длину.