Рассмотрим немаркированные, укоренившиеся двоичные деревья. Мы можем сжать такие деревья: всякий раз , когда есть указатели на поддерева и с (интерпретируя как структурное равенство), мы сохраняем (без потери общности) и заменить все указатели на с указателями на . См . Ответ Ули для примера.T ′ T = T ′ = T T ′ T
Дайте алгоритм, который принимает дерево в указанном выше смысле в качестве входных данных и вычисляет (минимальное) количество узлов, которые остаются после сжатия. Алгоритм должен выполняться за время (в модели с равномерной стоимостью) с числом узлов на входе.n
Это был экзаменационный вопрос, и мне не удалось найти хорошее решение, и я его не видел.
Ответы:
Да, вы можете выполнить это сжатие за , но это не легко :) Сначала мы сделаем некоторые наблюдения, а затем представим алгоритм. Мы предполагаем, что дерево изначально не сжато - это на самом деле не нужно, но облегчает анализ.O(nlogn)
Во-первых, мы характеризуем «структурное равенство» индуктивно. Пусть и T ′ два (под) дерева. Если Т и Т ' являются нулевые деревья (не имеющие вершин на всех), они структурно эквивалентны. Если T и T ′ не являются нулевыми деревьями, то они структурно эквивалентны, если их левые потомки структурно эквивалентны, а их правые потомки структурно эквивалентны. «Структурная эквивалентность» является минимальной фиксированной точкой над этими определениями.T T′ T T′ T T′
Например, любые два конечных узла являются структурно эквивалентными, так как оба имеют нулевые деревья в качестве своих дочерних элементов, которые являются структурно эквивалентными.
Поскольку довольно неприятно говорить, что «их левые дети структурно эквивалентны, как и их правые дети», мы часто будем говорить, что «их дети структурно эквивалентны» и намерены делать то же самое. Также обратите внимание, что мы иногда говорим «эта вершина», когда имеем в виду «поддерево, корни которого находятся в этой вершине».
Приведенное выше определение немедленно дает нам подсказку, как выполнить сжатие: если мы знаем структурную эквивалентность всех поддеревьев с глубиной не более , то мы можем легко вычислить структурную эквивалентность поддеревьев с глубиной d + 1 . Мы должны сделать это вычисление разумно, чтобы избежать времени выполнения O ( n 2 ) .d d+1 O(n2)
Алгоритм будет присваивать идентификаторы каждой вершине во время ее выполнения. Идентификатор - это число в наборе . Идентификаторы уникальны и никогда не меняются: поэтому мы предполагаем, что мы устанавливаем некоторую (глобальную) переменную равной 1 в начале алгоритма, и каждый раз, когда мы присваиваем идентификатор какой-либо вершине, мы присваиваем текущее значение этой переменной вершине и приращению значение этой переменной.{ 1 , 2 , 3 , … , n }
Сначала мы преобразуем входное дерево в (не более ) списков, содержащих вершины одинаковой глубины вместе с указателем на их родителя. Это легко сделать за O ( n ) раз.N O ( n )
Сначала мы сжимаем все листья (мы можем найти эти листья в списке с вершинами глубины 0) в одну вершину. Мы присваиваем этой вершине идентификатор. Сжатие двух вершин выполняется путем перенаправления родителя любой вершины, чтобы указывать на другую вершину.
Мы делаем два наблюдения: во-первых, у любой вершины есть дети строго меньшей глубины, а во-вторых, если мы выполнили сжатие на всех вершинах глубины, меньшей (и дали им идентификаторы), то две вершины глубины d структурно эквивалентны и может быть сжат, если идентификаторы их дочерних элементов совпадают. Последнее наблюдение следует из следующего аргумента: две вершины структурно эквивалентны, если их потомки структурно эквивалентны, и после сжатия это означает, что их указатели указывают на одинаковых потомков, что, в свою очередь, означает, что идентификаторы их потомков равны.d d
Мы перебираем все списки с узлами одинаковой глубины от малой до большой глубины. Для каждого уровня мы создаем список целочисленных пар, где каждая пара соответствует идентификаторам потомков некоторой вершины на этом уровне. Мы имеем, что две вершины на этом уровне структурно эквивалентны, если их соответствующие целочисленные пары равны. Используя лексикографический порядок, мы можем отсортировать их и получить наборы целочисленных пар, которые равны. Мы сжимаем эти множества в отдельные вершины, как указано выше, и присваиваем им идентификаторы.
Приведенные выше наблюдения доказывают, что этот подход работает и приводит к сжатому дереву. Общее время выполнения плюс время, необходимое для сортировки списков, которые мы создаем. Поскольку общее число целочисленных пар, которые мы создаем, равно n , это дает нам общее время работы O ( n log n ) , как требуется. Подсчет количества узлов, которые мы оставили в конце процедуры, тривиален (просто посмотрите, сколько идентификаторов мы раздали).O ( n ) N O ( n logн )
источник
degree
» степени должны бытьdepth
? И несмотря на то, что CS-деревья растут вниз, я нахожу «высоту дерева» менее запутанной, чем «глубину дерева».Сжатие неизменяемой структуры данных таким образом, чтобы она не дублировала какую-либо структурно равную подтерму, называется согласованием хэшей . Это важный метод управления памятью в функциональном программировании. Хеширование - это своего рода систематическое запоминание структур данных.
Мы собираемся хешировать дерево и считать узлы после хэширования. Хеширование структуры данных размера всегда можно сделать в O ( nN операции; Подсчет количества узлов в конце линейный по количеству узлов.O ( nl g (n))
Я буду рассматривать деревья как имеющие следующую структуру (написанную здесь в синтаксисе Haskell):
Для каждого конструктора нам нужно поддерживать соответствие его возможных аргументов результату применения конструктора к этим аргументам. Листья тривиальны. Для узлов мы сохраняем конечное частичное отображение где T - набор идентификаторов деревьев, а N - набор идентификаторов узлов; T = N ⊎ { ℓ } где ℓ - единственный листовой идентификатор. (В конкретных терминах идентификатор - это указатель на блок памяти.)п о д е с :Т× Т→ N T N T= N⊎ { ℓ } ℓ
Мы можем использовать структуру данных логарифмического времени
nodes
, например сбалансированное двоичное дерево поиска. Ниже я вызовуlookup nodes
операцию, которая ищет ключ вnodes
структуре данных, иinsert nodes
операцию, которая добавляет значение в новый ключ и возвращает этот ключ.Теперь мы пересекаем дерево и добавляем узлы по мере продвижения. Хотя я пишу в псевдокоде, похожем на Haskell, я буду воспринимать его
nodes
как глобальную переменную; мы только добавим к нему, но вставки должны быть пронизаны.add
Функция рекурсивно на дереве, добавив его поддеревьев кnodes
карте, и возвращает идентификатор корня.Количество
insert
вызовов, которое также является окончательным размеромnodes
структуры данных, представляет собой количество узлов после максимального сжатия. (Добавьте один для пустого дерева, если необходимо.)источник
nodes
insert
иadd
должно быть сделано явным, и должна быть дана функция, которая фактически решает проблему, imho.nodes
Для удобства я делаю изменяемую переменную, но вы можете использовать ее во всех случаях. Я не собираюсь давать полный код, это не так.Вот еще одна идея, которая направлена на (инъективное) кодирование структуры деревьев в числа, а не на их произвольную маркировку. Для этого мы используем, что первичная факторизация любого числа уникальна.
Для наших целей пусть обозначает пустую позицию в дереве, а N ( l , r ) узел с левым поддеревом l и правым поддеревом r .Е N( л , г ) L р будет листом. Теперь пустьN( E, E)
Это последнее предположение является натяжкой для реальных машин; в этом случае вместо возведения в степень предпочли бы использовать нечто похожее на функцию спаривания Кантора .
источник
Поскольку изображения не допускаются в комментариях:
вверху слева: дерево ввода
вверху справа: поддеревья с корнями в узлах 5 и 7 тоже изоморфны.
внизу слева и справа: сжатые деревья не определены однозначно.
источник
Изменить: я прочитал вопрос, как T и T ′ были детьми одного и того же родителя. Я взял определение сжатия также рекурсивным, то есть вы можете сжать два ранее сжатых поддерева. Если это не фактический вопрос, то мой ответ может не сработать.
где
hasSameStructure()
функция, которая сравнивает два уже сжатых поддерева за линейное время, чтобы увидеть, имеют ли они точно такую же структуру. Написание рекурсивной функции с линейным временем, которая обходит каждую и проверяет, есть ли у одного поддерева левый дочерний элемент каждый раз, когда у другого есть и т. Д., Не должно быть трудным.ПустьNℓ Nр
источник