2.3.3.2 — один параметр, который двигает баланс между вставкой и извлечением2.3.3.2 — one parameter that shifts the balance between insertion and extraction

d-арная Кучаary Heap

Бинарная куча (2.3.3.1) — частный случай d-арной кучи при d=2. Ничто в идее полного дерева в массиве не требует ровно двух детей: у узла с индексом i дети — d·i+1 .. d·i+d, родитель — (i-1)/d (целочисленно). Увеличение d уменьшает высоту дерева (log_d n вместо log₂ n) — значит, sift-up (а вместе с ним и decrease-key) становится дешевле, потому что путь до корня короче. Но sift-down теперь должен сравнивать узел с d детьми вместо двух на каждом уровне — извлечение минимума становится дороже. Это не улучшение, а сдвиг компромисса под конкретную нагрузку.A binary heap (2.3.3.1) is the special case of a d-ary heap at d=2. Nothing about the idea of a complete tree in an array requires exactly two children: the node at index i has children d·i+1 .. d·i+d, and a parent at (i-1)/d (integer division). Increasing d shrinks the tree's height (log_d n instead of log₂ n) — so sift-up (and with it, decrease-key) becomes cheaper, because the path to the root is shorter. But sift-down now has to compare a node against d children instead of two at every level — extracting the minimum becomes pricier. This isn't an improvement, it's a shift in the trade-off for a specific workload.

insert/decrease-key O(logd n) extract-min O(d logd n) классика — Dijkstra, d=4classic — Dijkstra, d=4

Идея и инвариантIdea & invariant

Инвариант — тот же heap-order, применённый ко всем d детям каждого узла: ключ родителя ≤ ключей всех d его детей. Индексная арифметика обобщается напрямую: parent(i) = (i-1)/d, children(i) = d·i+1, d·i+2, ..., d·i+d. При d=2 формулы в точности совпадают с бинарной кучей — это не отдельная структура, а параметризованное семейство.The invariant is the same heap-order, applied to all d children of every node: a parent's key is ≤ all d of its children's keys. The index arithmetic generalizes directly: parent(i) = (i-1)/d, children(i) = d·i+1, d·i+2, ..., d·i+d. At d=2 the formulas exactly match the binary heap — this isn't a separate structure, it's a parameterized family.

Высота полного d-арного дерева с n узлами — Θ(log_d n) = Θ(log n / log d). Sift-up делает не более одного сравнения на уровень — O(log_d n) сравнений всего, то есть растёт медленнее с увеличением d. Sift-down на каждом уровне должен найти минимум среди d детей — до d-1 сравнений на уровень, итого O(d · log_d n) в худшем случае. При больших d высота падает быстрее, чем растёт цена одного уровня, но не бесконечно: слишком большое d делает каждый уровень настолько "широким", что sift-down начинает доминировать. На практике d=4 — частый выбор: заметно короче дерево, чем при d=2, а стоимость сравнения 4 детей всё ещё мала. Дополнительный практический бонус — cache locality: при вставочно-тяжёлой нагрузке более широкий и низкий узел укладывается в меньше кэш-линий на пути от листа к корню.The height of a complete d-ary tree with n nodes is Θ(log_d n) = Θ(log n / log d). Sift-up makes at most one comparison per level — O(log_d n) comparisons total, growing more slowly as d increases. Sift-down at each level has to find the minimum among d children — up to d-1 comparisons per level, giving O(d · log_d n) in the worst case. At larger d, height drops faster than the per-level cost grows, but not indefinitely: too large a d makes each level so "wide" that sift-down starts to dominate. In practice, d=4 is a common choice: noticeably shorter than d=2's tree, while the cost of comparing 4 children is still small. An extra practical bonus is cache locality: under an insertion-heavy workload, a wider and shorter tree fits into fewer cache lines along the path from a leaf to the root.

15 узлов: d=2 даёт высоту 3, d=4 даёт высоту 2 d=2 height=3 d=4 height=2

та же 15-элементная куча: у d=4 путь всплытия (insert/decrease-key) короче, но sift-down (extract-min) сравнивает 4 узла вместо 2 на каждом уровнеthe same 15-element heap: with d=4 the sift-up path (insert/decrease-key) is shorter, but sift-down (extract-min) compares 4 nodes instead of 2 at every level

struct DaryHeap {
    std::vector<int> a;
    int d;
    DaryHeap(int d_) : d(d_) {}

    int parent(int i) { return (i - 1) / d; }
    int child(int i, int k) { return d * i + k + 1; } // k = 0..d-1// k = 0..d-1

    void push(int key) {
        a.push_back(key);
        int i = a.size() - 1;
        while (i > 0 && a[parent(i)] > a[i]) {
            std::swap(a[parent(i)], a[i]);
            i = parent(i);
        }
    }

    int pop_min() {
        int top = a[0];
        a[0] = a.back();
        a.pop_back();
        int i = 0, n = a.size();
        while (true) {
            int smallest = i;
            for (int k = 0; k < d; k++) {          // до d сравнений на уровень// up to d comparisons per level
                int c = child(i, k);
                if (c < n && a[c] < a[smallest]) smallest = c;
            }
            if (smallest == i) break;
            std::swap(a[i], a[smallest]);
            i = smallest;
        }
        return top;
    }
};

Демонстрация вживуюLive demonstration

Выберите d — куча перестраивается заново из одного и того же набора ключей, и высота дерева видна сразу. insert проигрывает sift-up, extract-min — sift-down со сравнением всех d детей на каждом уровне (подсвечены все сравниваемые узлы, выбранный — акцентом).Pick a d — the heap is rebuilt from the same key set, and the tree's height is immediately visible. insert auto-plays sift-up, extract-min auto-plays sift-down comparing all d children at each level (all compared nodes are highlighted, the chosen one gets the accent color).

выберите d — высота дерева пересчитывается сразу для тех же ключейpick a d — the tree height is recomputed instantly for the same keys

Когда именно менять d — а когда не стоитWhen it actually pays to change d — and when it doesn't

d-арная куча выигрывает там, где insert и decrease-key случаются намного чаще extract-min — классический пример: алгоритм Дейкстры на плотном графе, где decrease-key вызывается для каждого ребра (их может быть O(V²)), а extract-min — только один раз на вершину (O(V) раз). Увеличение d с 2 до 4 в такой нагрузке снижает суммарную стоимость decrease-key заметнее, чем растёт стоимость extract-min, потому что decrease-key вызывается на порядки чаще. В нагрузке с противоположным профилем (например, heapsort, где нет decrease-key вовсе, только n извлечений подряд) d-арная куча с d>2 обычно проигрывает бинарной — sift-down доминирует, и раздувать d нет смысла.A d-ary heap wins where insert and decrease-key happen far more often than extract-min — the classic example: Dijkstra's algorithm on a dense graph, where decrease-key gets called for every edge (there can be O(V²) of them), while extract-min happens only once per vertex (O(V) times). Raising d from 2 to 4 under such a workload cuts the total decrease-key cost more noticeably than it grows the extract-min cost, because decrease-key is called orders of magnitude more often. In a workload with the opposite profile (e.g. heapsort, which has no decrease-key at all, only n extractions in a row), a d-ary heap with d>2 usually loses to a binary one — sift-down dominates, and there's no point inflating d.

Где встречаетсяWhere it shows up

01

Алгоритм Дейкстры на плотных графах с d=4Dijkstra's algorithm on dense graphs with d=4

Реализации кратчайших путей для плотных графов (много рёбер на вершину) часто используют d-арную кучу с d=4 именно потому, что decrease-key вызывается для каждого ребра при релаксации, а извлечение минимума — только один раз на вершину: суммарный выигрыш от более дешёвого decrease-key перевешивает подорожавший extract-min.Shortest-path implementations for dense graphs (many edges per vertex) often use a d-ary heap with d=4 precisely because decrease-key gets called on every edge relaxation, while extracting the minimum happens only once per vertex: the aggregate win from cheaper decrease-key outweighs the pricier extract-min.

02

Кэш-дружественные очереди с приоритетом при insert-тяжёлой нагрузкеCache-friendly priority queues under insert-heavy workloads

При большом количестве вставок на одно извлечение более широкое и низкое дерево d-арной кучи означает меньше "прыжков" по памяти на пути от листа к корню — меньше промахов кэша на операцию, что на практике даёт заметный выигрыш даже при формально более дорогом sift-down.With many insertions per extraction, a d-ary heap's wider and shorter tree means fewer memory "jumps" along the path from a leaf to the root — fewer cache misses per operation, which in practice gives a noticeable win even though sift-down is formally pricier.