Завантаження структури пальця дерева


16

Після роботи з 2-3 пальчиковими деревами я дуже вразила їх швидкість у більшості операцій. Однак одне питання, з яким я зіткнувся, - це великі накладні витрати, пов’язані з початковим створенням великого пальцевого дерева. Оскільки побудова визначається як послідовність операцій конкатенації, ви в кінцевому підсумку будуєте велику кількість пальцевих дерев, які не потрібні.

Через складний характер дерев на 2-3 пальці я не бачу інтуїтивного способу їх завантаження, і всі мої пошуки виявилися порожніми. Отже, питання полягає в тому, як би ви могли зайнятися завантаженням дерева на 2-3 пальця з мінімальними накладними витратами?

Для того, щоб бути явним: дана послідовність з задовгі п генерувати палець дерева уявлення S з мінімальними операцій.SnS

Наївний спосіб здійснити послідовні дзвінки на операцію (в літературі оператор « »). Однак це створить n чітких структур пальчикових дерев, що представляють усі зрізи S для [ 1 .. i ] .нS[1..i]



@Dave я реально реалізував їхні документи, і вони не стосуються ефективного створення.
jbondeson

Я так багато рахував.
Дейв Кларк

Не могли б ви бути трохи більш конкретними щодо того, що ви маєте на увазі під "побудовою" в даному випадку? Це розгортання?
jbapple

@jbapple - я відредагував більш чітко, вибачте за плутанину.
jbondeson

Відповіді:


16

GHC - х Data.Sequence«s replicateфункція будує fingertree в простір і час, але це дозволено, знаючи елементи , які йдуть на праві ребра дерева пальця від початку. Цю бібліотеку автори оригінальної статті написали на деревах 2-3 пальців.O(lgn)

Якщо ви хочете побудувати пальцеве дерево за допомогою повторного конкатенації, ви, можливо, зможете зменшити перехідний простір при будівництві, змінивши представлення колючок. Колючки на 2-3 пальчикових деревах вміло зберігаються як синхронізовані одиночно пов’язані списки. Якщо натомість ви зберігаєте колючки як деки, можливо, це дозволить заощадити місце під час об'єднання дерев. Ідея полягає в тому, що об'єднання двох дерев однакової висоти займає простір шляхом повторного використання колючок дерев. При з'єднанні 2-3 пальчикових дерев, як було описано спочатку, колючки, які є внутрішніми для нового дерева, вже не можуть використовуватися як є.O(1)

Каплан та Тарджан "Чисто функціональні зображення представлених списками сортованих списків" описують складнішу структуру дерева пальців. У цьому документі (у розділі 4) також розглядається побудова, схожа на пропозицію деке, яку я зробив вище. Я вважаю, що структура, яку вони описують, може об'єднати два дерева однакової висоти в час та простір. Для будівництва пальчикових дерев це достатньо для вас економії місця?O(1)

NB: Вживання їх слова "завантажувальний запуск" означає щось трохи інше, ніж ваше вживання вище. Вони означають збереження частини структури даних, використовуючи більш просту версію тієї ж структури.


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

У цій відповіді я мав на увазі дві ідеї: (1) Повторна ідея (2) Швидше об'єднати дерева майже однакового розміру. Я думаю, що ідея копії може створити пальчикові дерева в дуже маленькому додатковому просторі, якщо вхід є масивом.
jbapple

Так, я бачив і те, і інше. Вибачте, що я не коментував їх обох. Спочатку я розглядаю реплікаційний код - хоча я, безумовно, розтягую свої знання Haskell, наскільки це піде. Спочатку рум'яна виглядає так, що може вирішити більшість проблем, які у мене виникають, за умови, що у вас швидкий випадковий доступ. Швидкий конмат може бути трохи більш загальним рішенням у випадку відсутності випадкового доступу.
jbondeson

10

Визначаючи відмінну відповідь jbapple щодо replicate, але використовуючи replicateA(на чому replicateпобудовано), я придумав таке:

--Unlike fromList, one needs the length explicitly. 
myFromList :: Int -> [b] -> Seq b
myFromList l xs = flip evalState xs $ Seq.replicateA l go
    where go = do
           (y:ys) <- get
            put ys
            return y

myFromList(у дещо більш ефективному варіанті) вже визначено та використовується внутрішньо в Data.Sequenceпобудові пальців дерев , які є результатами сортів.

Загалом, інтуїція для replicateAцього проста. replicateAпобудований поверх додаткової функції дерева. applicativeTreeбере шматок дерева розміром mі створює добре збалансоване дерево, що містить його nкопії. Випадків nдо 8 (поодинокі)Deep пальцем) кодуються жорстко. Все, що вище цього, і викликає себе рекурсивно. Елемент "застосовного" просто полягає в тому, що він перемежовує конструкцію дерева з ефектами різьблення через, наприклад, у випадку вищевказаного коду, стан.

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

Ще кілька конкретних записок

main = print (length (show (Seq.fromList [1..10000000::Int])))

На деяких простих тестах це дало цікавий компроміс. Основна функція вище виконувалась майже на 1/3 нижче за допомогою myFromList, ніж з fromList. З іншого боку, myFromListвикористовували постійну купу в 2 МБ, тоді як стандарт fromListвикористовував до 926 МБ. Це 926 Мб виникає з необхідності одночасно зберігати весь список в пам'яті. Тим часом рішення с . Ми можемо усунути ці виділення, перемістившись у монаду, перетворену CPS, але це призводить до того, що в будь-який момент часу утримується набагато більше пам’яті, оскільки втрата ліні вимагає переходу списку без потокового потоку.myFromList здатне споживати структуру в ледачому потоковому режимі. Проблема зі швидкістю випливає з того, що myFromListнеобхідно виконати приблизно вдвічі більше виділень (в результаті побудови пари / знищення монади держави), ніжfromList

З іншого боку, якщо замість того, щоб змусити всю послідовність із показом, я переходжу до просто витягування голови чи останнього елемента, myFromListодразу представляє більший виграш - вилучення головного елемента майже миттєве, а вилучення останнього елемента - 0,8s . Тим часом, зі стандартом fromListвитягання або голови, або останнього елемента коштує ~ 2,3 секунди.

Це все деталі, і це наслідок чистоти та ліні. У ситуації з мутацією та випадковим доступом, я думаю, що replicateрішення суворо краще.

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


4
(1) Цікаво. Це схоже на правильний шлях , щоб зробити цю задачу. Я здивований, почувши, що це повільніше, ніж fromListколи вся послідовність вимушена. (2) Можливо, ця відповідь є занадто важкою для коду та мовою, залежною від cstheory.stackexchange.com. Було б чудово, якщо ви можете додати пояснення, як replicateAце працює незалежно від мови.
Цуйосі Іто

9

У той час як ви закінчуєтесь великою кількістю проміжних структур пальців, вони ділять переважну більшість своєї структури одна з одною. Зрештою, ви виділяєте щонайменше вдвічі більше пам’яті, ніж в ідеалізованому випадку, а решту звільняєте з першою колекцією. Асимптотика цього те саме настільки ж хороша, наскільки вони можуть отримати, оскільки вам потрібен кінчик пальця, наповнений n значеннями.

Ви можете створити пальцеве дерево, використовуючи Data.FingerTree.replicateта використовуючи їх FingerTree.fmapWithPosдля пошуку значень у масиві, який відіграє роль вашої кінцевої послідовності, або використовуючи traverseWithPosїх для вилучення зі списку чи іншого контейнера відомого розміру.

Це виділить О(журналн) вузли для початкового реплікаційного скелета, а потім замінити їх на О(н) вузли, необхідні для заселення скелета, "марна" О(журналн) пам'ять, поки збирання сміття не прибирає речі, тож замість оптимальних ~ 1000 вузлів вам доведеться заплатити за ~ 1010, а не за ~ 2000 від будівництва мінусів.

Нарешті, ви можете уникнути використання репліку на та генерування цього проміжку О(журналн)Дерево пам'яті реплікувало, використовуючи replicateA, але, як зазначав @sclv, тупінг для маніпулювання власним станом mapAccumLабо переходу з монадою стану фактично запровадить пропорційно аналогічні накладні витрати, щоб просто заплатити за всі додаткові клітини продукту.

TL; DR Якби мені довелося це зробити, я б, ймовірно, використовував:

rep :: (Int -> a) -> Int -> Seq a 
rep f n = mapWithIndex (const . f) $ replicate n () 

і індекс в масиві фіксованого розміру , я б просто поставити (arr !)на fвище.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.