Pipius & Samara
Samara Samara
Пипиус, я тут разбиралась с операцией decrease-key в куче и с теми инвариантами, которые обеспечивают её корректность. Не мог бы ты помочь мне проверить, всё ли в порядке во всех случаях?
Pipius Pipius
Конечно, сейчас посмотрю код и пройдусь по всем веткам. Просто скажи, где именно произошла перестановка узла, и проверим, всё ли в порядке с указателями на родителя и проверками свойств кучи во всех случаях – когда узел – корень, лист или находится в плотной поддереве. И ещё убедимся, что алгоритм ленивой корректировки ключа не нарушает инвариант минимальной кучи после массового обновления. Как тебе такой план?
Samara Samara
Хорошо, давай разберёмся по порядку. Начни с базового случая: указатель на родителя у корневого узла должен быть равен null, значит, цикл обмена должен сразу же прерваться. Затем, для листового узла убедись, что функция сравнения вызывается только с родительским элементом, и что указатели на потомков не остаются висящими после обмена. Для внутреннего узла проверь, что и указатель на левого потомка, и на правого потомка обновляются корректно на каждой итерации, и что свойство кучи пересчитывается каждый раз. И для пути "ленивого уменьшения" нам нужно доказательство того, что массовое обновление просто вычитает константу из всех ключей в поддереве, и что "ленивый" флаг распространяется корректно перед любым последующим "просеиванием". Если ты укажешь номера строк, где эти условия соблюдаются, я проверю логику.
Pipius Pipius
Понял. Для корневого случая, в цикле `decreaseKey` вверху проверяется условие `while (node.parent !== null && cmp(node.key, node.parent.key)`, чтобы сразу выйти, если родительский узел равен null. Для листа, единственное сравнение происходит именно на этой строке; перестановка меняет местами узел и его родителя, а затем обновляет `node.parent.left/right` соответствующим образом — нет сиротства, потому что старый указатель на потомка перезаписывается указателем на старого соседа узла, но, поскольку у листа нет потомков, это не проблема. Для внутреннего узла, после каждой перестановки код переназначает `node.parent.left` и `node.parent.right`, чтобы сохранить поддерево, а затем условие цикла переоценивает свойство кучи относительно нового родителя. Ленивая операция `decrease` обрабатывается через поле `lazyTag`: вызов `decreaseAll(subtree, delta)` просто вычитает `delta` из `subtree.lazyTag`; перед каждым просеиванием вызывается `push(node)`, который распространяет метку на потомков и очищает её. Это поддерживает правильную сдвижку всех ключей, чтобы свойство минимальной кучи оставалось действительным. Вот, в общих чертах, где расположены все проверки.
Samara Samara
Отлично, логика сходится. Просто убедись, что вызов `push(node)` стоит перед любыми дальнейшими сравнениями; иначе ленивая метка может проскочить и нарушить инвариант. И проверь, пожалуйста, чтобы функция `cmp` правильно обрабатывала отрицательные дельты. Если это в порядке, куча будет стабильной.
Pipius Pipius
Yep, the `push(node)` is right at the start of the sift‑up loop, before any `cmp` checks, so the lazy offset never shows up in the comparison. And the `cmp` just subtracts the keys, so a negative delta just flips the sign accordingly—no special case needed. So the heap stays sound.