The binary heap is the default priority queue. The other heaps exist for meld, global add, monotone keys or decrease-key. Several classic tricks (lazy deletion, two heaps, top- sums) handle what priority_queue can’t do directly.

Binary heap facts

  • Array layout: children of are and (0-indexed).
  • Push and pop are . Building from an array is , because .
  • priority_queue puts the element the comparator ranks largest on top, so greater<> gives a min-heap.
  • No decrease-key: push the new key and skip stale entries when popped (lazy deletion). The heap grows to but the complexity is unchanged.

Erasable heap (lazy deletion of arbitrary values): keep a second heap of “to delete” values and pop both while their tops match.

struct ErasableHeap {                         // max-heap with erase(x) for values that are present
    priority_queue<ll> a, d;
    void push(ll x) { a.push(x); }
    void erase(ll x) { d.push(x); }
    ll top() { while (!d.empty() && a.top() == d.top()) a.pop(), d.pop(); return a.top(); }
};

The heap family

Heappushpopdecrease-keymeldUse
binary with index mapdefault
leftist— worstmeld, persistent heaps (Eppstein)
skew am. am.— am.shortest meldable code
randomized meldable exp. exp.— exp.10 lines
pairing (PBDS) am. am.meld + decrease-key ready-made
Fibonacci am. am. am.theory only; slower in practice
binomial am. worst-case meld without randomisation
Brodal queuetheory only — the provably-optimal bounds, never implemented in practice
hollow heap am. am.theory only
soft heap am. am.—corrupts an -fraction of keys, see below
radix heap am.monotone integer keys (Dijkstra)
// leftist heap, persistent-friendly (copy on merge for persistence)
struct LNode { ll key; int dist = 1; LNode *l = nullptr, *r = nullptr; };
LNode* meld(LNode* a, LNode* b) {
    if (!a || !b) return a ? a : b;
    if (a->key > b->key) swap(a, b);                 // min-heap
    a->r = meld(a->r, b);
    if (!a->l || a->l->dist < a->r->dist) swap(a->l, a->r);
    a->dist = (a->r ? a->r->dist : 0) + 1;
    return a;
}

Making it persistent means allocating a copy of a before modifying it. That’s exactly what Eppstein’s shortest paths needs (K Shortest Paths).

Skew heap: drop the null-path-length bookkeeping entirely and unconditionally swap children after every merge (still always merging into the right child first). Shorter code than leftist, same asymptotics, but only amortized rather than worst-case.

Meldable heaps (or small-to-large with priority_queue, ) matter for: merging children’s heaps in tree greedy problems, the directed MST (Chu-Liu/Edmonds with melded incoming edges), and slope trick on trees.

Why Fibonacci heaps are a trap: the amortized decrease-key improves Dijkstra from to on paper, but the constant factor and pointer-chasing make it slower than a binary heap for every graph size that fits in memory — nobody uses it in contests, few use it in production. The lesson generalises: an asymptotic win bought with a large constant and poor cache behaviour is usually not a win. Soft heaps (Chazelle) go the other way: allow amortized everything by deliberately corrupting at most an fraction of keys (their stored value may be increased), used to break the sorting-based barrier for MST and for a clean linear-time selection algorithm — a genuinely surprising idea, giving up exactness for constant-time operations.

Global add (offset trick)

Store values relative to a global offset, so “add to every element” is . Combined with small-to-large merging (move the smaller heap’s elements, adjusting for both offsets), you get mergeable heaps with global add in total. That’s the core of slope trick on trees and many tree-greedy problems.

Radix heap

For Dijkstra with non-negative integer weights: keys are pushed in non-decreasing order relative to the last popped key. Bucket holds keys whose highest bit differing from last is . Popping redistributes the first non-empty bucket around its minimum, and each element moves down at most times.

template<class V> struct RadixHeap {
    array<vector<pair<ull, V>>, 65> b; ull last = 0; size_t cnt = 0;
    static int bucket(ull x, ull l) { return x == l ? 0 : 64 - __builtin_clzll(x ^ l); }
    void push(ull k, V v) { cnt++; b[bucket(k, last)].push_back({k, v}); }
    pair<ull, V> pop() {
        if (b[0].empty()) {
            int i = 1; while (b[i].empty()) i++;
            last = min_element(all(b[i]))->first;
            for (auto& e : b[i]) b[bucket(e.first, last)].push_back(e);
            b[i].clear();
        }
        cnt--; auto e = b[0].back(); b[0].pop_back(); return e;
    }
};

When is small, Dial’s algorithm (buckets indexed directly by distance) is even simpler (Shortest Paths).

Two-heap patterns

Running median: lo (max-heap, smaller half) and hi (min-heap, larger half), with sizes balanced so that . The median is lo.top(). With sums of each half, the minimum of over is also : = median, cost = sum(hi) - sum(lo) adjusted by sizes.

Sliding window median: the same with two multisets (deletion by value), or lazy-deletion heaps with explicit size counters.

Sum of the largest under insert/erase/change of : two multisets, top (the best) and rest, with a running sum of top. After each change, move border elements until top has exactly elements. per operation. Uses: “sum of the largest in every sliding window”, “best items as items arrive and leave”, DP transitions that take the best options.

struct TopK {                                  // sum of the k largest
    int k; ll s = 0; multiset<ll> top, rest;
    void fix() {
        while (sz(top) > k) { auto it = top.begin(); s -= *it; rest.insert(*it); top.erase(it); }
        while (sz(top) < k && !rest.empty()) { auto it = prev(rest.end()); s += *it; top.insert(*it); rest.erase(it); }
        while (!top.empty() && !rest.empty() && *prev(rest.end()) > *top.begin()) {
            ll a = *top.begin(), b = *prev(rest.end());
            top.erase(top.begin()); rest.erase(prev(rest.end()));
            top.insert(b); rest.insert(a); s += b - a;
        }
    }
    void insert(ll x) { rest.insert(x); fix(); }
    void erase(ll x) { auto it = top.find(x); if (it != top.end()) s -= x, top.erase(it); else rest.erase(rest.find(x)); fix(); }
};

Other uses

  • Merging sorted lists: heap of list heads, .
  • -th smallest pair sum of two sorted arrays: start from and expand and with a visited set, or push for all and advance only. .
  • best subsets, or -th smallest subset sum: a heap of states where each state generates two successors (the “Frederickson-style” or “replace the last element / extend” scheme), so the heap never holds more than states.
  • Event simulation and sweep lines, A*, Huffman coding, slope trick (Slope Trick).
  • Priority-queue undo trick: the heap analogue of the DSU queue-undo trick, for a divide-and-conquer-over-time recursion that needs “push, and later undo the push” on a max/min-heap. A plain heap has no efficient arbitrary-element deletion, so instead of deleting, record every push on a stack and, to undo the last pushes, pop them off the stack and re-insert everything else still logically present — or, more simply, keep a lazily-deleted heap (push the value with a version tag, and on pop skip anything tagged as undone) so “undo” is marking and the heap only ever grows, paid for once per real push, same amortized bound as the DSU version.

Binomial heap

A forest of binomial trees ( = two linked root to root, nodes), at most one of each order, like the binary representation of . Push, pop-min and meld are (push is amortized, exactly binary-counter increments). Historically the base of Fibonacci heaps; in contests a pairing heap or __gnu_pbds meldable heap is simpler, but meldability in worst case is the point.

template <class T> struct BinomialHeap {                  // min-heap with meld
    struct Node { T key; int deg = 0; Node *child = nullptr, *sib = nullptr; };
    vector<Node*> roots;                                  // roots[k]: tree of order k or nullptr
    static Node* link(Node* a, Node* b) { if (b->key < a->key) swap(a, b); b->sib = a->child; a->child = b; a->deg++; return a; }
    void insertTree(Node* t) {                            // binary-counter carry
        for (int k = t->deg;; k++) {
            if (k >= sz(roots)) roots.resize(k + 1, nullptr);
            if (!roots[k]) { roots[k] = t; return; }
            t = link(roots[k], t); roots[k] = nullptr;
        }
    }
    void push(T x) { insertTree(new Node{x}); }
    void meld(BinomialHeap& o) { for (Node* t : o.roots) if (t) insertTree(t); o.roots.clear(); }
    int minIdx() const { int b = -1; for (int i = 0; i < sz(roots); i++) if (roots[i] && (b < 0 || roots[i]->key < roots[b]->key)) b = i; return b; }
    bool empty() const { return minIdx() < 0; }
    T top() const { return roots[minIdx()]->key; }
    void pop() {
        int b = minIdx(); Node* t = roots[b]; roots[b] = nullptr;
        for (Node* c = t->child; c;) { Node* nx = c->sib; c->sib = nullptr; insertTree(c); c = nx; }   // children: orders deg-1..0
        delete t;
    }
};

Venice Heap

Min-heap that supports “add x to every element” in O(1) and merging heaps by moving the smaller one.

VeniceHeap<i64> h;
h.push(v); h.top(); h.pop(); h.size();
h.addAll(x);                         // every element += x
h.merge(other);                      // other becomes empty

Complexity: O(log n) push/pop, O(1) addAll, merges O(n log^2 n) in total (small to large).
The “Venice technique”: instead of changing every element, store values relative to a global offset, the way water rises around buildings. Common in greedy problems on trees (merge children heaps and shift all keys) and in slope-trick style DP (see dp/slope-trick.cpp).

Implementation

template <class T>
struct VeniceHeap {
    T offset{};
    priority_queue<T, vector<T>, greater<T>> pq;
 
    void push(T v) { pq.push(v - offset); }
    T top() const { return pq.top() + offset; }
    void pop() { pq.pop(); }
    int size() const { return pq.size(); }
    bool empty() const { return pq.empty(); }
    void addAll(T x) { offset += x; }
 
    void merge(VeniceHeap& o) {
        if (size() < o.size()) {
            swap(pq, o.pq);
            swap(offset, o.offset);
        }
        while (!o.pq.empty()) {
            push(o.pq.top() + o.offset);
            o.pq.pop();
        }
        o.offset = T{};
    }
};