Продвинутый модуль: сливаемые и Фибоначчиевы кучи

Продвинутый модуль · изучайте отдельно от основного маршрута. Его прохождение не требуется для результата базового курса.

Meld и decreaseKey: биномиальные, Фибоначчиевы и левосторонние кучи. Инварианты, переносы рангов, потенциал и выбор реализации.

01

Более богатый интерфейс кучи

Это продвинутый модуль после двоичной кучи. Для основного маршрута можно сразу перейти к динамическому программированию. Начнём с факультативного сценария: у команды A задачи с приоритетами [2,7,9], у B — [1,5]. После meld все пять задач находятся в одной очереди, ближайшая имеет приоритет 1. Вставлять их по одной не обязательно: объединённый массив обычных куч можно обработать heapify за O(n+m).

Теперь задача с id=17 стала срочной: её приоритет уменьшаем с 7 до 0 через decreaseKey(handle,0). Handle — сохранённая ссылка или устойчивый id именно этой задачи; одинаковые приоритеты не делают задачи одинаковыми. Приоритет меньше — задача раньше. Специализированный meld полезен, когда большие очереди приходится объединять многократно.

Для одного слияния heapify часто достаточно. Чтобы выбирать структуру для множества запросов, сначала построим несколько вариантов кучи и их инварианты, затем сравним оценки в конце. В разминке meld_values вы проверите сохранение содержимого, в профильной задаче — устройство самого meld.

Meld потребляет две независимые очереди и возвращает общую; после него договоритесь, какая переменная владеет новой кучей. Поиск задачи по одному значению приоритета не входит в decreaseKey: нужен handle. Проверку этого условия нельзя заменять обещанием быстрой операции.

При уменьшении ключа новое значение не превосходит прежнего, так что свойство min-кучи может нарушиться только по отношению к родителю.

Важное условиеУ decreaseKey предполагается существующий handle на узел; поиск нужного значения с нуля — отдельная задача. Время Fibonacci heap указано амортизированно.

Handle — ссылка или устойчивый идентификатор уже найденного узла. Если его нет, поиск элемента в куче может стоить O(n), и оценку decreaseKey нельзя считать полной ценой запроса. Meld обычно разрушает независимость исходных куч: после него владельцы и время жизни узлов должны быть определены. Для двоичной кучи два массива можно объединить и построить заново за O(n+m), а не обязательно за O(m log(n+m)).

Уменьшение приоритета с ленивым удалением

Практическая альтернатива decreaseKey: кладём новую запись (приоритет,id), а устаревшие пропускаем. Приоритеты только уменьшаются; актуальная запись после извлечения удаляется из словаря. Это не Фибоначчиева куча: q обновлений требуют O(q log(q+1)) времени и могут накопить O(q) записей.

Уменьшение приоритета с ленивым удалением / Python
import heapq

def updated_priorities(initial, updates):
    current = dict(enumerate(initial))
    heap = [(key, i) for i, key in current.items()]
    heapq.heapify(heap)
    for i, key in updates:
        if key < current[i]:
            current[i] = key
            heapq.heappush(heap, (key, i))
    result = []
    while heap:
        key, i = heapq.heappop(heap)
        if current.get(i) == key:
            result.append(key)
            del current[i]
    return result

print(*updated_priorities([9, 5], [(0, 2)]))
Ожидаемый вывод
2 5

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Изменение приоритета события

Если событие названо id, храните id → handle либо добавляйте новую запись с ленивым удалением старой. Линейный поиск по куче не исчезает только потому, что следующий decreaseKey быстрый.

Требования к операциям ясны. Построим семейство деревьев, которое превращает слияние в переносы между рангами.

Примените meld и handle к очередям задач

Задания раздела

Очереди A=[2,7,9], B=[1,5]. После meld задача с ключом 7 получила ключ 0. Каков новый минимум?

Две задачи имеют ключ 7. Что нужно сохранить для decreaseKey одной из них?

Попыток: 0
02

Биномиальные деревья

Этот раздел откроется после предыдущих проверок.

Продолжить: Более богатый интерфейс кучи →

Построим форму по шагам. B₀ — один узел. Два одноузловых дерева соединяем под меньшим корнем: B₁ содержит 2 узла. Два B₁ дают B₂ с 4 узлами; два B₂ — B₃ с 8. Ранг k просто называет этот шаг построения. Только после этих четырёх картинок обобщим правило до 2ᵏ узлов.

Биномиальное дерево Bₖ (Binomial tree) — Дерево, построенное связыванием двух деревьев Bₖ₋₁, одно становится ребёнком другого.

В дереве Bₖ содержится 2ᵏ узлов; у корня k детей, являющихся корнями B₀, B₁, …, Bₖ₋₁. Для min-кучи каждый родитель не превосходит своих детей.

Размеры биномиального дерева

B₀ — один узел. Два B₀ дают B₁ (2 узла); два B₁ — B₂ (4 узла); два B₂ — B₃ (8 узлов). Меньший ключ остаётся корнем.

B0: 1 вершин, высота 0, детей корня 0.

Сливаем два B₂ с корнями 3 и 7. Каким будет результат?

Пустой массив

Дети корня плюс сам корень: 1 + 0 = 1.

|Bₖ| = 2ᵏ; height(Bₖ) = k

Следует по индукции из рекурсивного определения дерева.

Почему деревья называют биномиальными
Число узлов на уровнях Bₖ выражается биномиальными коэффициентами. Для базовых операций лекции главное свойство — размер 2ᵏ.

Дерево B₀ состоит из одной вершины. Связав два B₂ под меньшим корнем, получим B₃ с восемью вершинами и тремя детьми у корня. Операция link сохраняет порядок min-кучи и занимает O(1), если список детей позволяет добавить ссылку без копирования.

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Группы размеров — степени двойки

При объединении очередей дерево Bₖ удобно считать блоком из 2ᵏ элементов. Связать два таких блока можно без обхода их внутренних узлов; важны корни и ранг.

Форма одного дерева определена. Теперь ограничим число деревьев каждого ранга и получим полноценную кучу.

Постройте следующее биномиальное дерево

Задания раздела

Два B₂ (по 4 узла) связали под меньшим корнем. Сколько узлов в результате?

Корни исходных деревьев имеют ключи 3 и 7. Как сохранить min-порядок?

Попыток: 0
03

Биномиальная куча и двоичная запись

Этот раздел откроется после предыдущих проверок.

Продолжить: Более богатый интерфейс кучи →

У 13 элементов естественное разложение 13=8+4+1: берём B₃, B₂ и B₀. В двоичной записи 13=1101₂ единицы стоят ровно в разрядах 3, 2, 0. Биномиальная куча хранит не больше одного дерева каждого ранга. Слияние — сложение таких записей с переносами.

Биномиальная куча (Binomial heap) — Лес min-деревьев разных рангов: на каждый ранг допускается не более одного дерева.

Для n элементов набор рангов определяется единичными разрядами двоичной записи n. Поэтому деревьев не более O(log n).

Слияние как двоичные переносы

A: 13 = 1101₂; B: 7 = 111₂. Сумма: 20 = 10100₂.

B0: 1[0]
B0: 1[1]
B1: 2[2]
B2: 4[3]
B2: 4[4]
B3: 8[5]

По разрядам от старшего к младшему: 3:1 · 2:2 · 1:1 · 0:2. Два дерева в разряде дают перенос. Всего 20 элементов, 6 корней.

До слияния: 13 + 7 элементов.
Шаг 1 / 5

Операция meld действует как сложение двоичных чисел: два дерева одинакового ранга связывают в одно дерево следующего ранга. Минимум находится среди корней.

Слить два дерева одного ранга
link(a, b):
    assert a.rank == b.rank
    if a.key > b.key: swap(a,b)
    make b a child of a
    a.rank += 1
    return a

Свойство min-кучи сохраняется: меньший корень остаётся выше.

Операция биномиальной кучиСтоимость
insertO(log n)
meldO(log(n+m))
getMin при сохранённом указателеO(1)
extractMinO(log n)
decreaseKeyO(log n)

Например, 13 = 8 + 4 + 1: куча содержит ранги 3, 2 и 0. При добавлении одного узла ранг 0 переносится в 1; если он занят, перенос идёт дальше. Без отдельного указателя минимум ищется среди O(log n) корней. Отдельный указатель ускоряет чтение минимума, но его нужно обновлять после изменений.

Какие деревья входят в биномиальную кучу

Единичный бит k означает дерево Bₖ размером 2ᵏ. Для 13 это ранги 0, 2 и 3. Это вычисление формы леса, не полноценная реализация кучи. Время O(log(n+1)), память O(log(n+1)).

Какие деревья входят в биномиальную кучу / Python
def binomial_ranks(n):
    ranks = []
    rank = 0
    while n:
        if n & 1:
            ranks.append(rank)
        n >>= 1
        rank += 1
    return ranks

print(*binomial_ranks(13))
Ожидаемый вывод
0 2 3

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Много слияний очередей

При s независимых очередях поэлементная вставка повторно обрабатывает уже готовые структуры. Биномиальная куча связывает деревья одинаковых рангов и ограничивает число корней логарифмом общего размера.

Немедленные переносы дают логарифмическое слияние. Посмотрим, что можно отложить до удаления минимума.

Верните возрастающий список рангов деревьев биномиальной кучи размера n. Например, 13=1+4+8 даёт [0,2,3]. n — неотрицательное целое.

Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

Открыто 0 из 3

    5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

    Тест 1 binomial_ranks(13)Ожидается: [0, 2, 3]
    Тест 2 binomial_ranks(0)Ожидается: []

    Дополнительные тесты: 4. Проверяются границы и повторы.

    Попыток: 0
    04

    Зачем нужна Фибоначчиева куча

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Более богатый интерфейс кучи →

    Если часто объединять большие очереди и уменьшать приоритеты уже известных задач, немедленные переносы биномиальной кучи повторяют работу. Фибоначчиева куча откладывает её: добавление и слияние увеличивают число корней, но не требуют немедленной консолидации. Это улучшает теоретический бюджет некоторых последовательностей операций; указатели, память и константы не обещают практического выигрыша на каждом входе.

    Фибоначчиева куча (Fibonacci heap) — Лес деревьев min-кучи с ленивым объединением и амортизированными гарантиями для ключевых операций.

    Фибоначчиева куча ослабляет жёсткую форму биномиальных деревьев. При insert новый элемент можно просто сделать отдельным корнем; при meld соединить корневые списки.

    Отложенная консолидация
    7: степень 0[0]
    2: степень 0[1]
    9: степень 0[2]
    4: степень 0[3]

    Вставки оставляют одноузловые деревья в корневом списке.

    Элементов: 4; корней: 4. Модель показывает insert, meld и extractMin; каскадные разрезания разобраны отдельно ниже.

    Указатель на минимальный корень позволяет отвечать getMin за O(1). Дорогую нормализацию рангов откладывают до extractMin.

    Разница гарантийПри готовых циклических списках insert и meld меняют постоянное число ссылок и занимают O(1) в худшем случае. Для extractMin и decreaseKey стандартные оценки O(log n) и O(1) относятся к амортизированному времени: один вызов может быть дороже.

    Нужны циклические двусвязные списки корней и детей, parent, степень и отметка у узла, а также указатель min. Операции insert и meld требуют O(1) реальных переназначений; одинаковые степени корней до консолидации разрешены. Корни не помечаем, а структура каждого дерева сохраняет порядок min-кучи.

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Много уменьшений расстояний

    В теоретическом анализе алгоритмов кратчайшего пути число decreaseKey может быть большим относительно extractMin. Фибоначчиева куча улучшает соответствующий член оценки; на практике сложность указателей и константы часто делают двоичную кучу удобнее.

    Дешёвое добавление накапливает корни. Разберём операцию, которая приводит их к управляемому виду.

    Посчитайте отложенную работу

    Задания раздела

    В куче 4 корня. Выполнили 3 insert, затем meld с кучей из 2 корней, без extractMin. Сколько корней?

    В корневом списке оказались три дерева степени 0. Это ошибка?

    Попыток: 0
    05

    Удаление минимума и консолидация

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Более богатый интерфейс кучи →
    Консолидация своими руками

    Четыре этапа: убрать минимальный корень → поднять его детей → связать равные степени → найти новый минимум.

    Корней сейчас: 4.

    Отложенная работа всё же должна когда-то выполняться. Извлечение минимума удаляет корень, выводит его детей в корневой список и объединяет деревья одинаковой степени.

    Для extractMin удаляют минимальный корень и делают его детей корнями. Затем проходят по списку корней и связывают пары деревьев одинакового ранга, пока каждый ранг не встречается более одного раза.

    После консолидации заново выбирается минимальный корень. Чтобы время было логарифмическим, надо доказать, что ранг узла ограничен O(log n), несмотря на возможные удаления детей.

    extractMin = O(log n) амортизированно

    Число различных рангов логарифмическое; дорогая консолидация оплачивается накопленным потенциалом.

    Дети удаляемого корня
    В подлинной структуре нужно корректно извлечь и вставить целые связанные списки детей, обновить их parent и mark и продолжить работу с таблицей рангов.

    После n последовательных вставок корней может быть n, поэтому одно extractMin действительно может стоить Θ(n). Логарифмической является амортизированная цена, а не худший случай этого вызова. Ограничение степени получают из того, что некорневой узел может потерять не более одного ребёнка, оставаясь на месте: минимальное число потомков растёт как числа Фибоначчи.

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Редкое удаление после большой пачки вставок

    Пачка из n insert может оставить n корней. Следующее extractMin будет долгим; нельзя обещать ему худший случай O(log n). Для общего лимита смотрите сумму всей пачки и удаления.

    Для логарифмической степени нужен контроль потерь детей. Он определит правила уменьшения ключа.

    Восстановите extractMin по шагам

    Задания раздела

    Упорядочите этапы удаления минимума.

    1. Связать пары равных степеней
    2. Убрать минимальный корень
    3. Найти новый минимальный корень
    4. Поднять его детей в корневой список

    У корней степени [0,1,0]. Какую пару можно связать первой?

    Попыток: 0
    06

    Уменьшение ключа и каскадные разрезания

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Более богатый интерфейс кучи →

    Теперь рассмотрим decreaseKey для известного узла. Если новое значение нарушает порядок только на ребре к родителю, достаточно отрезать это ребро. Но без контроля потерь дерево могло бы потерять связь между степенью и количеством потомков.

    Если после decreaseKey ключ узла стал меньше родительского, узел отрезают от родителя и переводят в корневой список. Чтобы дерево не слишком сильно деградировало, вводят пометки потерь ребёнка.

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

    Разрезание по цепочке помеченных предков

    Сначала один разрез: если ключ ребёнка стал меньше ключа родителя, переносим ребёнка в корни. Затем смотрим на родителя. Для некорневого родителя первая потеря означает пометку; вторая — разрезание самого родителя. У корня проверка заканчивается.

    Некорневой узел p без пометки впервые потерял ребёнка. Что изменить?

    Тот же некорневой p уже помечен и теряет второго ребёнка. Что дальше?

    Корней t = 1
    Пометок m = 3
    Φ = 7
    Цепочка: корень, 3 помеченных предков, затем уменьшаемый лист.
    Шаг 1 / 5

    У корня каскад прекращается. Если встретился бы непомеченный некорневой предок, он получил бы пометку, и каскад тоже завершился бы.

    Техническое уточнениеПотенциальный анализ требует различать корневые и некорневые вершины и аккуратно обновлять отметки. У корней метка не нужна.

    При первой потере ребёнка некорневой узел помечается. При второй он сам становится корнем, пометка снимается, и проверка продолжается у его родителя. Если уменьшенный ключ всё ещё не меньше родительского, разрезание не нужно. При переносе ребёнка в корни у него сбрасываются parent и mark.

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Уменьшение по известному узлу

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

    Правила пометок заданы. Осталось объяснить, кто оплачивает длинное каскадное разрезание.

    Различите первую и повторную потерю

    Задания раздела

    Выберите действие после потери ребёнка.

    После decreaseKey новый ключ 8, родитель имеет ключ 5. Нужно ли разрезание?

    Попыток: 0
    07

    Левосторонняя куча

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Более богатый интерфейс кучи →

    Фибоначчиева куча даёт сильные теоретические оценки, но её реализация сложна. Для многих контестных задач со слиянием подходит более простой вариант — левосторонняя куча с коротким правым путём.

    Левосторонняя куча (Leftist heap) — Двоичная min-куча с ограничением на длины кратчайших путей к пустым детям: npl(left) ≥ npl(right).

    Корень min-кучи содержит минимум. Операция meld сравнивает корни, сохраняет меньший, рекурсивно сливает правое поддерево с другой кучей и при необходимости меняет детей местами.

    Настоящее дерево левосторонней кучи
    КучаПутьКлючnplЛевый ребёнокПравый ребёнок
    Aкорень3185
    AкореньL80——
    AкореньR50——
    Bкорень1194
    BкореньL90——
    BкореньR40——

    У пустого ребёнка npl = −1. В каждой строке npl слева не меньше npl справа. Слияние проходит по правым путям.

    merge = O(log n)

    Правый путь имеет логарифмическую длину из-за левостороннего инварианта.

    Схема meld левосторонних куч
    meld(a,b):
        if a == null: return b
        if b == null: return a
        if a.key > b.key: swap(a,b)
        a.right = meld(a.right,b)
        if npl(a.left) < npl(a.right): swap(a.left,a.right)
        update_npl(a)
        return a

    Слияние меняет ссылки и восстанавливает npl после возврата из рекурсии. Демонстрация строит настоящее дерево.

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

    Примем npl(null) = −1, npl(листа) = 0 и npl(v) = 1 + min(npl(left), npl(right)). Инвариант npl(left) ≥ npl(right) делает кратчайший путь к пустому ребёнку правым. При слиянии рекурсивно обрабатываются только правые пути; затем дети при необходимости меняются местами. Высота всего дерева может быть линейной, а правый путь остаётся O(log n). Для insert сливаем с одноузловой кучей, для extractMin — двух детей корня.

    КучаГлавный инвариантМинДобавитьИзвлечь минMeldУменьшить ключ по handle
    ДвоичнаяПолное дерево; родитель ≤ дети.O(1)O(log n)*O(log n)O(n+m) через общий heapifyO(log n), если известен индекс
    БиномиальнаяНе больше одного Bₖ каждого ранга; порядок min-кучи.O(log n) без указателя minO(log n)O(log n)O(log(n+m+1))O(log n), с корректным учётом handle
    ФибоначчиеваПорядок min-кучи; некорневой узел остаётся на месте после максимум одной потери ребёнка.O(1)O(1)O(log n) аморт.O(1)O(1) аморт.
    ЛевосторонняяПорядок min-кучи; npl(left) ≥ npl(right).O(1)O(log n)O(log n)O(log(n+m+1))В базовый интерфейс не входит
    ОперацияБинарная кучаФибоначчиева (амортиз.)
    insertO(log n)O(1)
    getMinO(1)O(1)
    extractMinO(log n)O(log n)
    meldO(n+m) стандартноO(1)
    decreaseKeyO(log n)O(1)

    В сравнении n и m — размеры двух сливаемых куч. Для динамического массива рост ёмкости учитывается амортизированно и отмечен звёздочкой. Meld потребляет две независимые кучи. Поиск произвольного значения и чтение максимума в min-куче обычно стоят O(n); вставка в заданную позицию не предусмотрена. Для биномиальной кучи отдельный обновляемый указатель min даёт O(1) чтение минимума.

    Полная левосторонняя куча: meld, insert, pop

    Обе кучи владеют разными узлами; meld потребляет их и возвращает один корень. После рекурсивного слияния восстанавливаем npl(left) ≥ npl(right). Meld и pop — O(log n) по правым путям, минимум в корне — O(1). Дополнительный стек рекурсии при meld логарифмический; высота самого дерева может быть линейной.

    Полная левосторонняя куча: meld, insert, pop / Python
    class Node:
        def __init__(self, key):
            self.key, self.left, self.right, self.rank = key, None, None, 0
    
    def rank(node):
        return node.rank if node is not None else -1
    
    def meld(a, b):
        if a is None: return b
        if b is None: return a
        if a.key > b.key: a, b = b, a
        a.right = meld(a.right, b)
        if rank(a.left) < rank(a.right): a.left, a.right = a.right, a.left
        a.rank = rank(a.right) + 1
        return a
    
    def make_heap(values):
        root = None
        for x in values: root = meld(root, Node(x))
        return root
    
    def drain(root):
        result = []
        while root is not None:
            result.append(root.key)
            root = meld(root.left, root.right)
        return result
    
    print(*drain(meld(make_heap([3, 8, 7]), make_heap([1, 9, 4]))))
    Ожидаемый вывод
    1 3 4 7 8 9

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Luogu P3377: слияние и удаление минимума

    Здесь действительно нужен meld разных куч, а команды обращаются к элементам по исходным id. Левосторонняя куча поддерживает слияние, а отдельный учёт представителей и удалённых элементов сохраняет смысл команд.

    Выбор кучи зависит от набора запросов и констант реализации. Закрепим оценки по ограничениям; в следующей лекции будем ускорять не хранение, а повторные вычисления.

    Две min-кучи заданы списками значений a и b. Смоделируйте meld и последовательное извлечение всех минимумов: верните общий отсортированный список с сохранением повторов. Можно использовать heapq. Это проверка содержимого слияния; инвариант левостороннего дерева разобран в примере выше.

    Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

    Открыто 0 из 3

      5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

      Тест 1 meld_values([3, 8, 7], [1, 9, 4])Ожидается: [1, 3, 4, 7, 8, 9]
      Тест 2 meld_values([], [2, 2])Ожидается: [2, 2]

      Дополнительные тесты: 3. Проверяются границы и повторы.

      Попыток: 0
      А—Я

      Справочник терминов

      Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.

      Общий справочник всех лекций ↗

      Найдено: 13 из 13

      meld / MeldОбъединение двух куч.
      decreaseKey / Decrease keyУменьшение ключа известного узла.
      extractMin / Extract minimumИзвлечь и удалить минимальный элемент.
      Биномиальное дерево / Binomial treeРекурсивное дерево из 2ᵏ узлов.
      Биномиальная куча / Binomial heapЛес биномиальных деревьев разных рангов.
      Фибоначчиева куча / Fibonacci heapКуча с ленивым связыванием деревьев и амортизированными гарантиями.
      Ранг дерева / Degree / rankКоличество непосредственных детей вершины в рассматриваемой куче.
      Консолидация / ConsolidationСлияние корней одинакового ранга после extractMin.
      Каскадное разрезание / Cascading cutСерия отделений помеченных вершин при decreaseKey.
      Пометка / MarkФлаг потери ребёнка в Fibonacci heap.
      Потенциал / PotentialΦ = число корней + 2 × число помеченных вершин.
      Левосторонняя куча / Leftist heapДвоичная куча с ограниченным правым путём.
      Нулевая длина пути / Null path lengthКратчайшее расстояние до отсутствующего ребёнка с выбранной нормировкой.
      08

      Ограничения и выбор алгоритма

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Более богатый интерфейс кучи →

      Сначала подставьте максимальные размеры из условия в оценку времени и памяти. Оценка числа действий — ориентир: константы, язык и ввод проверяются отдельно.

      Сначала посчитайте типы операций

      Пусть I вставок, M слияний, D уменьшений и E извлечений. Сравнивайте сумму цен, а не одну строку таблицы. У Фибоначчиевой кучи I+M+D оплачиваются амортизированно за O(I+M+D), а извлечения добавляют O(E log n). Нужны уже известные handle.

      Ленивая двоичная куча — другой бюджет

      При 200 000 обновлений расстояний можно хранить до O(q) записей, включая устаревшие. Время зависит от log q, память — от числа обновлений. Сравните это с лимитом, прежде чем писать сложную указательную кучу.

      Две двоичные кучи можно объединить линейным heapify объединённого массива. Это полезно для одного крупного слияния; много повторных слияний больших контейнеров могут сделать суммарную линейную по размеру работу квадратичной.

      Ранги, правый путь и память

      У левосторонней кучи O(log n) относится к правому пути, а не к высоте всего дерева. Не используйте обычный рекурсивный обход всего дерева, считая его глубину логарифмической. Для освобождения или проверки глубокой структуры может понадобиться явный стек.

      При n,m ≤ 100 000 в P3377 нельзя каждый раз копировать все значения объединяемых куч. Храните узлы и ссылки, проверяйте удалённые id, повторы значений и попытку слить кучу саму с собой. При равных ключах условие может требовать сравнения пары (ключ, исходный id).

      Выберите кучу по операциям

      Задания раздела

      Сопоставьте сценарий и разумный первый вариант.

      O(log n) у левосторонней кучи ограничивает…

      Попыток: 0
      09

      Практика: 3 уровня

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Более богатый интерфейс кучи →

      Разминка: содержимое слияния

      Сначала решите meld_values: это упражнение на сохранение всех значений и повторов. Оно не проверяет дерево. Основной результат модуля — следующий meld, в котором тесты проверяют min-порядок и npl после каждой операции.

      Две min-кучи заданы списками значений a и b. Смоделируйте meld и последовательное извлечение всех минимумов: верните общий отсортированный список с сохранением повторов. Можно использовать heapq. Это проверка содержимого слияния; инвариант левостороннего дерева разобран в примере выше.

      Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

      Открыто 0 из 3

        5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

        Тест 1 meld_values([3, 8, 7], [1, 9, 4])Ожидается: [1, 3, 4, 7, 8, 9]
        Тест 2 meld_values([], [2, 2])Ожидается: [2, 2]

        Дополнительные тесты: 3. Проверяются границы и повторы.

        Попыток: 0

        Основное применение: настоящее левостороннее дерево

        Реализуйте meld(a,b) для готового класса Node. Верните корень, сохраните все узлы и повторы, min-порядок и npl(left)≥npl(right). npl(None)=−1, лист имеет npl=0. Готовый адаптер проверяет инварианты после meld и каждого extractMin; форма дерева может быть любой корректной. Не используйте сортировку и heapq.

        Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

        Открыто 0 из 3

          5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

          Тест 1 leftist_probe([3, [8, None, None], [5, None, None]], [1, [9, None, None], [4, None, None]])Ожидается: {"valid": True, "values": [1, 3, 4, 5, 8, 9]}
          Тест 2 leftist_probe(None, [2, None, None])Ожидается: {"valid": True, "values": [2]}

          Дополнительные тесты: 3. Проверяются границы и повторы.

          Попыток: 0
          Повторение обычной двоичной кучи

          Эти задачи закрепляют insert и extractMin из лекции 02. Они полезны для повторения, но не требуют специализированного meld.

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

          Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.

          01

          Лёгкие

          Три задачи для закрепления основных приёмов.

          1. Поддерживать инвариант k крупнейших значений.

            Ограничения. Начальный массив и число add до 10⁴; k ≤ initial.length+1.

            Идея и оценка сложности

            Храните min-кучу размера не больше k. После добавления лишний минимум удаляется, а корень даёт k-й максимум. O(log(k+1)) на обновление, O(k) памяти.

          2. LeetCode · 1046

            Last Stone Weight

            Выбрать правильное направление кучи.

            Ограничения. До 30 камней весом до 1000.

            Идея и оценка сложности

            Нужны два максимума, поэтому используйте max-кучу. В Python можно хранить отрицательные веса. Возвращайте разность обратно только если она ненулевая. O(n log(n+1)).

          3. Codeforces · 1353D

            Constructing the Array

            Сравнивать составные приоритеты отрезков.

            Ограничения. Сумма n по тестам до 2·10⁵.

            Идея и оценка сложности

            Храните пустые отрезки по убыванию длины и возрастанию левой границы. После выбора середины добавляйте непустые половины. Это базовая задача курса на приоритет пары; O(n log n).

          02

          Средние

          Две задачи на сочетание идей и анализ ограничений.

          1. Возвращать освобождённые минимальные номера в очередь.

            Ограничения. До 10⁵ мест и суммарных операций.

            Идея и оценка сложности

            Min-куча хранит свободные номера. Можно отдельно хранить следующий ещё не выданный номер и кучу только освобождённых. Оцените инициализацию и O(log n) на возврат и повторную выдачу.

          2. Переопределять распределение ресурса через минимум.

            Ограничения. До 10⁵ зданий, высоты до 10⁶, кирпичей до 10⁹.

            Идея и оценка сложности

            Лестницы отдавайте крупнейшим положительным подъёмам. Держите их в min-куче; если кандидатов больше лестниц, минимальный подъём оплачивайте кирпичами. Сумму расходов храните широко. O(n log(ladders+1)) с отдельной обработкой нулевого числа лестниц.

          Профильное испытание

          P3377 добавляет доступ по исходным id и удаление. Перед ней проверьте свою реализацию на пустой куче, равных ключах и чередовании meld/extractMin.

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

          Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.

          01

          Тяжёлая

          Одна задача для переноса идеи на новую постановку.

          1. Luogu · P3377

            Meldable Heap 1

            Реализовать настоящее слияние куч и доступ через исходные id.

            Ограничения. n,m ≤ 10⁵; ключи помещаются в int. Условие на китайском.

            Идея и оценка сложности

            Левосторонняя куча выполняет meld и извлечение минимума. Дополнительно храните принадлежность исходных id кучам и отметку удаления; запрос к удалённому id имеет особый смысл. При равных ключах первым удаляется меньший исходный id. Не объединяйте кучу саму с собой.

          Графы с частым уменьшением расстояний используют decreaseKey; сравнение с ленивой двоичной кучей теперь можно проводить по конкретному бюджету операций.

          Проверьте инварианты настоящего meld

          Задания раздела

          После рекурсивного meld у узла npl(left)=0, npl(right)=2. Что сделать?

          Чему равен meld(None,b)?

          Попыток: 0
          Дополнительные материалыДля углублённого разбора · не влияют на прохождение
          ДОП.

          Потенциал Фибоначчиевой кучи

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

          Каскад иногда проходит много уровней. Как в очереди на двух стеках, амортизированная гарантия требует запаса, накопленного предыдущими операциями. Запишем его через число корней и пометок.

          Потенциал (Potential function) — Неотрицательная величина, отражающая запас «отложенной» работы в текущем лесу.

          Φ = t + 2m

          t — число деревьев-корней, m — число помеченных узлов.

          Добавление корня увеличивает потенциал на 1. Консолидация уменьшает число корней. Каскадные разрезания могут быть многочисленными, но снимают отметки — тем самым оплачивается фактическая работа.

          Кто оплачивает каскад

          На цепочке до корня отрезаем лист и 4 уже помеченных предков.

          Фактические разрезания5
          Потенциал до9
          Потенциал после6
          Разрезания + ΔΦ2

          В условной модели одна единица на разрезание даёт постоянный итог 2 независимо от длины каскада. Полное доказательство учитывает остальные постоянные действия.

          Амортизированная стоимость = фактическая стоимость + ΔΦ. Именно эта оценка объясняет O(1) для decreaseKey при возможной длинной каскадной цепочке.

          Почему коэффициент при m равен 2
          Уменьшение числа помеченных узлов компенсирует стоимость дополнительного отсечения и появления нового корня. Выбранный коэффициент делает суммарную амортизированную цену одного decreaseKey ограниченной константой.

          Если выполнено c разрезаний по уже помеченным предкам, работа O(c) сопровождается снятием примерно c пометок. Добавленные корни повышают t, но снятые пометки понижают 2m и оплачивают каскад. Отсюда O(1) амортизированно для decreaseKey и O(log n) для extractMin. Точное доказательство также учитывает константу в единице стоимости и логарифмическую степень.

          В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

          Проверка амортизированной гарантии

          Сравните фактические разрезания с изменением Φ. Длинный каскад снимает много пометок; последовательность из многих дорогих каскадов требует предварительно создать их запас.

          Потенциал объяснил теоретическую выгоду. Теперь сравним её с более короткой реализацией слияния.

          Предскажите изменение запаса работы

          Задания раздела

          В Φ=t+2m корней стало на 1 больше, пометок на 1 меньше. Чему равно ΔΦ?

          Попыток: 0

          ПРОДОЛЖИТЬ ОБСУЖДЕНИЕ

          Промпт для ИИ

          Скопируйте промпт с текстом лекции в чат с ИИ, чтобы обсудить непонятные места. В него входят дополнительные материалы, псевдокод и примеры на обоих языках.