Ефективне стиснення не маркованих дерев


20

Розгляньте не марковані, вкорінені двійкові дерева. Ми можемо стиснути такі дерева: коли є покажчики на підтрубки і T з T = T (трактуючи = як структурна рівність), ми зберігаємо (wlog) T і замінюємо всі покажчики на T TTT=T=TT з покажчиками на . Див . Приклад відповіді uli .T

Наведіть алгоритм, який приймає дерево у наведеному вище значенні як вхідний та обчислює (мінімальне) число вузлів, які залишаються після стиснення. Алгоритм повинен працювати в часі (в єдиній моделі витрат) з n кількістю вузлів на вході.O(nlogn)n

Це було екзаменаційне питання, і я не зміг придумати гарного рішення, а також не бачив.


І що тут "вартість", "час", елементарна операція? Кількість відвідуваних вузлів? Кількість пройдених ребер? І як визначається розмір входу?
uli

Це стиснення дерева є примірником хешування . Не впевнений, чи це призводить до загального методу підрахунку.
Жил "ТАК - перестань бути злим"

@uli Я уточнив, що таке . Я думаю, що "час" досить конкретний. У налаштуваннях, що не супроводжуються, це еквівалентно рахунку операцій, що в ландауських термінах еквівалентно підрахунку елементарної операції, що відбувається найчастіше. n
Рафаель

@Raphael Звичайно, я можу здогадатися, якою повинна бути елементарна операція, і, ймовірно, виберуть те саме, що і всі. Але я знаю, що я тут педантичний, щоразу, коли даються «межі часу», важливо констатувати, що рахується. Це заміни, порівняння, доповнення, доступ до пам'яті, оглянуті вузли, пройдено ребра, ви називаєте це. Це як опускання одиниці вимірювання у фізиці. Чи або 1010kg ? І я вважаю, що доступ до пам'яті майже завжди є найчастішою операцією. 10ms
uli

@uli Це такі деталі, які має передаватися "єдиною моделлю витрат". Болісно точно визначити, які операції є елементарними, але в 99,99% випадків (включаючи цю) немає двозначності. Класи складності принципово не мають одиниць, вони не вимірюють час, необхідний для виконання одного екземпляра, але спосіб цього часу змінюється, коли вхід збільшується.
Жил "ТАК - перестань бути злим"

Відповіді:


10

Так, ви можете виконати це стиснення в час, але це непросто :) Спочатку робимо деякі спостереження, а потім представляємо алгоритм. Ми припускаємо, що дерево спочатку не стискається - це насправді не потрібно, але полегшує аналіз.O(nlogn)

По-перше, ми характеризуємо «структурну рівність» індуктивно. Нехай і Т ' - два (під) дерева. Якщо T і T ' обидва нульові дерева (взагалі не мають вершин), вони структурно еквівалентні. Якщо T і T ' обидва не нульові дерева, то вони структурно рівнозначні, якщо їхні ліві діти структурно рівнозначні, а їхні праві діти - структурно рівнозначні. "Структурна еквівалентність" є мінімальною фіксованою точкою в цих визначеннях.TTTTTT

Наприклад, будь-які два вузли листя є структурно еквівалентними, оскільки вони обидва мають нульові дерева, як і їхні діти, які структурно рівнозначні.

Оскільки досить прикро сказати, що "їхні ліві діти структурно рівнозначні, як і їхні праві діти", ми часто говоримо "їхні діти структурно рівнозначні" і мають намір те саме. Також зауважимо, що іноді ми говоримо "ця вершина", коли маємо на увазі "піддірення, що вкорінене в цій вершині".

Вищенаведене визначення відразу дає нам підказку, як виконати стиснення: якщо ми знаємо структурну еквівалентність усіх підтрубків з глибиною не більше , то ми можемо легко обчислити структурну еквівалентність підрядів з глибиною d + 1 . Нам потрібно робити це обчислення розумним способом, щоб уникнути часу запуску O ( n 2 ) .dd+1O(n2)

Алгоритм призначатиме ідентифікатори для кожної вершини під час її виконання. Ідентифікатор - це число у наборі {1,2,3,,n} . Ідентифікатори є унікальними і ніколи не змінюються: тому ми припускаємо, що ми встановлюємо певну (глобальну) змінну до 1 на початку алгоритму, і кожен раз, коли ми присвоюємо ідентифікатор якійсь вершині, ми присвоюємо поточне значення цієї змінної вершині та приросту значення цієї змінної.

Спочатку перетворюємо дерево вводу в (максимум n ) списки, що містять вершини однакової глибини, разом із вказівником на їх батьків. Це легко зробити за час.O(n)

Спочатку стискаємо все листя (їх можна знайти у списку з вершинами глибини 0) в одну вершину. Цю вершину ми присвоюємо ідентифікатору. Стиснення двох вершин виконується шляхом перенаправлення батьківського з вершин будь-якої вершини, щоб вказати на іншу вершину.

Ми робимо два спостереження: по-перше, у будь-якій вершині є діти суворо меншої глибини, по-друге, якщо ми виконали стиснення на всіх вершинах глибини меншої ніж (і дали їм ідентифікатори), то дві вершини глибини d структурно рівнозначні і можна стиснути, якщо ідентифікатори їхніх дітей збігаються. Останнє спостереження випливає з наступного аргументу: дві вершини структурно рівнозначні, якщо їхні діти структурно рівнозначні, і після стиснення це означає, що їхні покажчики вказують на тих самих дітей, що, в свою чергу, означає, що ідентифікатори їхніх дітей рівні.dd

Ми повторюємо всі списки з вузлами однакової глибини від малої до великої глибини. Для кожного рівня ми створюємо список цілих пар, де кожна пара відповідає ідентифікаторам дітей певної вершини на цьому рівні. Ми маємо, що дві вершини на цьому рівні структурно еквівалентні, якщо їх відповідні цілі пари рівні. Використовуючи лексикографічне впорядкування, ми можемо їх сортувати та отримати множини цілих пар, рівних. Ми стискаємо ці множини до одиничних вершин, як зазначено вище, і надаємо їм ідентифікатори.

Наведені вище спостереження доводять, що такий підхід працює і призводить до стиснення дерева. Загальний час роботи - плюс час, необхідний для сортування створених нами списків. Оскільки загальна кількість цілих пар, які ми створюємо, дорівнює n , це дає нам, що загальний час запуску дорівнює O ( n log nO(n)n , як потрібно. Підрахунок, скільки вузлів у нас залишилося в кінці процедури, є тривіальним (просто подивіться, скільки ідентифікаторів ми передали).O(nlogn)


Я не читав вашої відповіді докладно, але думаю, що ви більш-менш винаходили хеш-консингент із дивним способом пошуку вузлів.
Жил "ТАК - перестань бути злим"

@ Алекс "діти, які мають строго менший degreeступінь", мабуть, повинні бути depth? І попри CS-дерева, що ростуть вниз, я вважаю, що «висота дерева» менш заплутана, ніж «глибина дерева».
uli

Гарна відповідь. Я відчуваю, що повинен бути спосіб обійти сортування. Мій другий коментар до відповіді @Gilles також дійсний і тут.
Рафаель

@uli: так, ти маєш рацію, я це виправив (не знаю, чому я переплутав ці два слова). Висота та глибина - це два тонко різні поняття, і мені було потрібно останнє :) Я думав, що буду дотримуватися звичайної «глибини», а не плутати всіх, міняючи їх.
Олексій десять Бринк

4

Стиснення структури, що не змінюється, так, щоб вона не дублювала жодну структурно рівну підтермію, відоме як хеш-консинг . Це важливий прийом управління пам’яттю у функціональному програмуванні. Хеш-консинг - це свого роду систематичне запам'ятовування структур даних.

Ми збираємось хеш-мінуси дерева та підраховуємо вузли після конфіскації хешу. Хеш-консистенція структури даних розміром завжди може бути виконана в O ( nn операції; підрахунок кількості вузлів на кінці є лінійним у кількості вузлів.O(nlg(n))

Я вважатиму дерева, які мають таку структуру (написана тут у синтаксисі Haskell):

data Tree = Leaf
          | Node Tree Tree

Для кожного конструктора нам потрібно підтримувати відображення від його можливих аргументів до результату застосування конструктора до цих аргументів. Листя тривіальні. Для вузлів ми підтримуємо кінцеву часткову карту де T - набір ідентифікаторів дерев і N - набір ідентифікаторів вузлів; T = N { }, де - єдиний ідентифікатор листя. (Конкретно кажучи, ідентифікатор - це вказівник на блок пам'яті.)nodes:T×TNTNT=N{}

Ми можемо використовувати структуру даних логарифмічного часу nodes, наприклад, збалансоване дерево бінарного пошуку. Нижче я називаю lookup nodesоперацію, яка шукає ключ у nodesструктурі даних, і insert nodesоперацію, яка додає значення під свіжим ключем і повертає цей ключ.

Тепер обводимо дерево і додаємо вузли, коли ми йдемо далі. Хоча я пишу в псевдокоді, подібному до Haskell, я буду вважати nodesглобальну змінну змінну; ми лише коли-небудь додаватимемо його, але вставки потрібно прошивати по всій темі. addФункція рекурсивно на дереві, додавши його піддерев до nodesкарті, і повертає ідентифікатор кореня.

insert (p1,p2) =
add Leaf = $\ell$
add (Node t1 t2) =
    let p1 = add t1
    let p2 = add t2
    case lookup nodes (p1,p2) of
      Nothing -> insert nodes (p1,p2)
      Just p -> p

Кількість insertвикликів, що також є кінцевим розміром nodesструктури даних, - це кількість вузлів після максимального стиснення. (Якщо потрібно, додайте його для порожнього дерева.)


Чи можете ви дати посилання на "Hash coning структури даних розміру завжди можна виконати в операціях O ( n l g ( n ) ) "? Зауважте, що вам потрібні збалансовані дерева для того, щоб досягти бажаного часу виконання. nO(nlg(n))nodes
Рафаель

Я розглядав лише структурування хешування до чисел структурованим чином, щоб незалежне обчислення хешу для одного дерева завжди дало б однаковий результат. Ваше рішення також чудово, за умови, що у нас є змінні структури даних. Я думаю, що це може бути зачищене, але; переплетення insertі addповинно бути явним, і функція, яка фактично вирішує проблему, повинна бути надана, imho.
Рафаель

1
@Raphael Hash консистенція спирається на кінцеву структуру карти над кортежами покажчиків / ідентифікаторів, ви можете реалізувати це з логарифмічним часом для пошуку та додавання (наприклад, з збалансованим деревом двійкового пошуку). Моє рішення не потребує змінності; Я роблю nodesзмінну змінну для зручності, але ви можете вносити її в усі. Я не збираюся давати повний код, це не ТАК.
Жил "ТАК - перестань бути злим"

1
@Raphael HASHING структура, на відміну від присвоєння їм довільних чисел, трохи виверткий. У моделі єдиної вартості ви можете кодувати що-небудь у велике ціле число і робити операції на ньому постійного часу, що не реально. У реальному світі ви можете використовувати криптографічні хеші, щоб фактично відображати один на один від нескінченних множин до кінцевого діапазону цілих чисел, але вони повільні. Якщо ви використовуєте контрольну суму, не крипто, як хеш, вам потрібно подумати про зіткнення.
Жил "ТАК - перестань бути злим"

3

Ось ще одна ідея, яка спрямована на (ін’єкційне) кодування структури дерев у числа, а не просто маркування їх довільно. Для цього ми використовуємо, що просте множення будь-якого числа є унікальним.

Для наших цілей нехай Е позначають порожнє місце на дереві та N(л,r) вузол з лівим піддеревом л і праве піддерево r. N(Е,Е)був би листочок. А тепер нехай

f(Е)=0f(N(л,r))=2f(л)3f(r)

Використання f, ми можемо обчислити набір усіх підрядів, що містяться в дереві знизу вгору; у кожному вузлі ми зливаємо набори кодувань, отриманих від дітей, і додаємо нове число (яке можна обчислювати в постійному часі з кодування дітей).

Останнє припущення - це розтягнення на реальних машинах; у цьому випадку можна вважати за краще використовувати щось подібне до функції спарювання Кантора замість експоненції.

Час виконання цього алгоритму залежить від структури дерева (на збалансованих деревах, О(нжурналн)з будь-якою реалізацією набору, яка дозволяє об'єднати в лінійний час). Для загальних дерев нам потрібен логарифмічний часовий союз з простим аналізом. Можливо, все-таки може допомогти складний аналіз; зауважимо, що звичайне дерево найгіршого випадку, лінійний список, визнаєО(нжурналн) час тут, тож не зовсім зрозуміло, що може бути найгіршим.


1

Оскільки фотографії в коментарях заборонені:

enter image description here

ліворуч вгорі: дерево вводу

праворуч вгорі: підрядки, що укорінені у вузлах 5 та 7, теж ізоморфні.

ліворуч праворуч: стислі дерева не визначені однозначно.

Зауважте, що в цьому випадку розмір дерева знизився з 7+5|Т| теж 6+|Т|.


Це справді приклад бажаної операції, дякую. Зауважте, що ваші остаточні приклади ідентичні, якщо ви не розрізняєте оригінальні та додані посилання.
Рафаель

-1

Редагувати: Я читав питання, оскільки Т і Т 'були дітьми одного батька. Я вважав, що визначення стиснення також є рекурсивним, це означає, що ви можете стиснути дві раніше стиснуті підрядки. Якщо це не власне питання, то моя відповідь може не спрацювати.

О(нжурналн) благає про Т(н)=2Т(н/2)+cнрозділити і підкорити рішення. Рекурсивно стискають вузли та обчислюють кількість нащадків у кожному піддереві після стиснення. Ось кілька псевдокодів python-esque.

def Comp(T):
   if T == null:
     return 0
   leftCount = Comp(T.left)
   rightCount = Comp(T.right)
   if leftCount == rightCount:
     if hasSameStructure(T.left, T.right):
       T.right = T.left
       return leftCount + 1
     else
       return leftCount + rightCount + 1    

Де hasSameStructure()функція, яка порівнює два вже стиснуті підряди за лінійним часом, щоб побачити, чи мають вони таку саму структуру. Написання лінійної рекурсивної функції часу, яка проходить кожне і перевіряє, чи є одне піддерево лівою дитиною щоразу, коли інше робить і т.д., не повинно бути важким.

Дозволяти н і нrбути розмірами лівого та правого підкремлів відповідно (після стиснення). Тоді час роботи

Т(н)=Т(н1)+Т(н2)+О(1) якщо ннr
і
2Т(н/2)+О(н) інакше

What if the subtrees are not siblings? Care for ((T1,T1),(T2,T1)) T1 can be saved twice by using a pointer two the third occurence.
uli

@uli I'm not sure what you're saying. I read the question as T and Tбули дітьми одного батька. Якщо це не власне питання, то моя відповідь може не спрацювати. Я вважав, що визначення стиснення також є рекурсивним, це означає, що ви можете стиснути дві раніше стиснуті підрядки.
Джо

У запитаннях чітко зазначено, що два підмарини визначені як ізоморфні. Нічого не сказано про те, що вони мають одного батька. Якщо піддерево T1 з’являється тричі на дереві, як і в моєму попередньому прикладі ((T1, T1), (T1, T2)), два випадки можна стиснути, вказуючи на третій випадок.
uli
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.