Алгоритм O (1), чтобы определить, является ли узел потомком другого узла в многостороннем дереве?

Представьте себе следующее дерево:

    A
   / \
  B   C
 / \   \
D   E   F

Я ищу способ запросить, является ли, например, F потомком A (примечание: F не обязательно должен быть прямым потомком A), что в данном конкретном случае будет истинный. Необходимо протестировать только ограниченное количество потенциальных родительских узлов в сравнении с большим пулом потенциальных потомков.

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

Вот что придумал:

  • Преобразовать многоканальное дерево в трие, т. е. присвоить следующие префиксы каждому узлу в приведенном выше дереве:

     A = 1
     B = 11
     C = 12
     D = 111
     E = 112
     F = 121
    
  • Затем зарезервируйте битовый массив для каждого возможного размера префикса и добавьте родительские узлы для тестирования, т.е. если C добавлен в потенциальный пул родительских узлов, выполните:

      1    2    3  <- Prefix length
    
    *[1]  [1]  ...
     [2] *[2]  ...
     [3]  [3]  ...
     [4]  [4]  ...
     ...  ...
    
  • При проверке, является ли узел потомком потенциального родительского узла, возьмите его префикс trie, найдите первый символ в первом «массиве префиксов» (см. выше) и, если он присутствует, найдите второй символ префикса во втором «префиксе». array" и так далее, т.е. проверка F приводит к:

     F = 1    2    1
    
       *[1]  [1]  ...
        [2] *[2]  ...
        [3]  [3]  ...
        [4]  [4]  ...
        ...  ...
    

    так что да F, является потомком C.

Этот тест кажется наихудшим случаем O (n), где n = максимальная длина префикса = максимальная глубина дерева, поэтому его наихудший случай в точности соответствует очевидному способу просто подняться по дереву и сравнить узлы. Однако это работает намного лучше, если тестируемый узел находится в нижней части дерева, а потенциальный родительский узел находится где-то вверху. Комбинация обоих алгоритмов смягчит оба наихудших сценария. Тем не менее, накладные расходы памяти вызывают беспокойство.

Есть ли другой способ сделать это? Любые указатели с благодарностью!


person Philip Kamenarsky    schedule 16.05.2011    source источник
comment
Всегда ли это бинарное дерево?   -  person Gumbo    schedule 16.05.2011


Ответы (7)


Ваши входные деревья всегда статичны? Если это так, то вы можете использовать алгоритм наименьшего общего предка, чтобы ответить на вопрос о потомках за время O (1) с конструкцией времени/пространства O (n). Запрос LCA получает два узла и спрашивает, какой из них является самым нижним узлом в дереве, поддерево которого содержит оба узла. Затем вы можете ответить на запрос IsDescentent одним запросом LCA, если LCA(A, B) == A или LCA(A, B) == B, то один является потомком другого.

В этом учебнике по алгоритму Topcoder подробно обсуждается проблема и предлагаются несколько решений. на различных уровнях сложности/эффективности кода.

person Rob Neuhaus    schedule 16.05.2011
comment
Отличный учебник, спасибо! Узнал кое-что :) К сожалению, мое дерево не является статичным, но я думаю, что идея LCA sqrt-parts может быть легко адаптирована для динамического использования, когда глубина дерева всегда примерно одинакова, как в моем случае. - person Philip Kamenarsky; 17.05.2011

Я не знаю, подойдет ли это к вашей проблеме, но один из способов хранения иерархий в базах данных с быстрыми функциями «дайте мне все, начиная с этого узла и ниже» - это сохранение «пути».

Например, для дерева, которое выглядит так:

    +-- b
    |
a --+       +-- d
    |       |
    +-- c --+
            |
            +-- e

вы должны хранить строки следующим образом, предполагая, что буква в приведенном выше дереве является «id» каждой строки:

id    path
a     a
b     a*b
c     a*c
d     a*c*d
e     a*c*e

Чтобы найти всех потомков определенного узла, вы должны выполнить запрос «STARTSWITH» в столбце пути, т.е. все узлы с путем, начинающимся с a*c*

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

Так, например:

  • e является потомком a, так как a*c*e начинается с a
  • d является потомком c, так как a*c*d начинается с a*c

Будет ли это полезно в вашем случае?

person Lasse V. Karlsen    schedule 16.05.2011
comment
Это именно то, что я предлагаю, только для баз данных, насколько я могу судить :) В нативной реализации STARTSWITH будет медленной частью — для m потенциальных родительских узлов с максимальной длиной префикса n это будет O (nm) Я думаю, тогда как битовые массивы делают это O (n) за счет экспоненциального роста памяти :( - person Philip Kamenarsky; 16.05.2011
comment
длина пути может быть равна O(log(n)) поэтому начинается с O(log(n)) - person Saeed Amiri; 16.05.2011
comment
Будет ли работать статическая таблица сопоставления? т.е. вы взрываете все комбинации, поэтому вы будете хранить для каждого узла список потомков, где этот список заполнен всеми потомками, независимо от глубины? Для создания таблиц поиска потребовались бы значительные начальные затраты, но после этого можно было бы ответить, какие узлы являются потомками X, за постоянное время (амортизированное), по крайней мере, если вы использовали хэш-таблицу/словарь. - person Lasse V. Karlsen; 16.05.2011

Для обхода любого дерева потребуются шаги «глубины дерева». Поэтому, если вы поддерживаете сбалансированную древовидную структуру, вполне вероятно, что вам потребуется O(log n) операций для операции поиска. Насколько я понимаю, ваше дерево выглядит особенным, и вы не можете поддерживать его сбалансированным образом, верно? Так что O(n) будет возможно. Но это в любом случае плохо во время создания дерева, так что вы, вероятно, умрете до того, как воспользуетесь поиском...

В зависимости от того, как часто вам понадобится эта операция поиска по сравнению с вставкой, вы можете решить заплатить во время вставки, чтобы поддерживать дополнительную структуру данных. Я бы предложил хеширование, если вам действительно нужно амортизировать O(1). При каждой операции вставки вы помещаете всех родителей узла в хеш-таблицу. По вашему описанию это может быть O(n) элементов на данной вставке. Если вы делаете n inserts, это звучит плохо (в направлении O(n^2)), но на самом деле ваше дерево не может ухудшить это плохо, поэтому вы, вероятно, получите амортизированный общий размер hastable O(n log n). (на самом деле часть log n зависит от степени деградации вашего дерева. Если вы ожидаете, что она будет максимально деградирована, не делайте этого.)

Таким образом, вы заплатите около O(log n) за каждую вставку и получите эффективность хеш-таблицы O(1) для поиска .

person towi    schedule 16.05.2011
comment
Я только что увидел, что недостаточно разъяснил свой вопрос, извините за это :( Проблема в том, что существует потенциальный пул родительских узлов, скажем, A, B, C - тогда я хотел бы увидеть, является ли F потомком ЛЮБОГО узла в родительском пуле.С вашей идеей хэш-карты это все равно будет O (n), но накладные расходы памяти должны быть намного меньше :) - person Philip Kamenarsky; 16.05.2011
comment
На самом деле, я бы не стал следовать своему собственному совету. Я не очень люблю хэш-карты, мне больше нравятся гарантии. Во-вторых, я думаю, что я что-то упустил в своем описании... но если нет: что бы ни означал пул узлов, я думаю, что хэш-запись должна работать и там. Говоря языком С++, это на самом деле set<> то, о чем я думаю. Записи будут pair<Node,Node>, а вы должны использовать insert( make_pair(parent,child) ). Затем, если вы хотите проверить, что любой child.isThatAParent(potentialParent) - это просто return set.find( make_pair(node,potentialParent)!=end). Проверка сделана для любой заданной пары в O(1). - person towi; 16.05.2011

Почему бы вместо битового массива M-дерева просто не хранить двоичный "идентификатор дерева" (используя M бит на уровень) с каждым узлом? Для вашего примера (при условии, что M==2) : A=0b01, B=0b0101, C=0b1001, ...

Затем вы можете выполнить тест в O (1):

bool IsParent(node* child, node* parent)
{ 
   return ((child->id & parent->id) == parent->id)
}

Вы можете сжать хранилище до ceil(lg2(M)) бит на уровень, если у вас есть быстрый FindMSB(), которая возвращает позицию старшего набора битов:

mask = (1<<( FindMSB(parent->id)+1) ) -1;
retunr (child->id&mask == parent->id);
person AShelly    schedule 16.05.2011
comment
Не могли бы вы уточнить, как вычислить этот идентификатор? Имейте в виду, что это многостороннее дерево с потенциально большим количеством узлов на уровне. - person Philip Kamenarsky; 16.05.2011
comment
Я предполагаю, что A) вы уже можете вычислить идентификатор в форме, описанной выше, и B) M фиксировано. Для первой функции просто выполните foreach digit: id= (id<<M) | (1<<(digit-1)). Для второго метода это: id = (id<<B) | digit, где B — количество битов, необходимых для хранения M (т. е. findMSB(M)+1). - person AShelly; 16.05.2011
comment
И если M превышает 32/64 бита, просто используйте длинный массив, а затем перебирайте все части идентификатора при И? Я люблю это :) - person Philip Kamenarsky; 16.05.2011
comment
-ИЛИ- если у вас есть хранилище и вы не хотите возиться с битовыми полями, сохраните идентификаторы в виде строк байтов именно в той форме, которую вы показали: например, A - это \x01, F - это \x01\x02\x01. Это может обрабатывать до 255 дочерних элементов на узел, и вы можете выполнить тест с strstr(child,parent)==parent - person AShelly; 16.05.2011
comment
@AShelly Разве C=0b1001 не должно быть C=0b0110 в вашем примере с M==2? - person tonix; 07.08.2019
comment
Нет, представление верное, ошибся алгоритм построения в моем первом комментарии. Мы хотим, чтобы идентификатор уровня 0 был в младших значащих битах, чтобы нам не приходилось сдвигать его влево при добавлении новых уровней. Чтобы присвоить идентификаторы дерева дочерним элементам узла, пронумерованным от 1..M, конструкция должна быть trie_id = (1 << (childNum-1)) | parent_id - person AShelly; 07.08.2019
comment
@AShelly Хорошо, теперь я понял, спасибо за ваш комментарий! Просто чтобы убедиться, что я все правильно понял, в вашем примере A=0b01, B=0b0101, C=0b1001, ... вы резервируете 2 бита на tree depth + 1, поэтому, например. если дерево имеет глубину 11, как показано ниже: 1 (root, depth 0) -> 2 (child of 1, depth 1) -> 3 (child of 2, depth 2) -> ... -> 12 (child of 11, depth 11); вы будете использовать 2 bits * (11 depth + 1) = 24 bits бит для идентификатора дерева узлов, которые имеют глубину 11, верно? - person tonix; 19.08.2019

При предварительном обходе каждый набор потомков является непрерывным. Для вашего примера,

A B D E C F
+---------+ A
  +---+ B
    + D
      + E
        +-+ C
          + F

Если вы можете выполнить предварительную обработку, то все, что вам нужно сделать, это пронумеровать каждый узел и вычислить интервал потомков.

Если предварительная обработка невозможна, то дерево ссылок/вырезов предлагает O(log n) производительность как для обновлений, так и для запросов.

person slowpoke    schedule 16.05.2011
comment
Примечание. Производительность log n не зависит от конкретной топологии дерева. - person slowpoke; 16.05.2011

Вы можете ответить на запрос вида «Является ли узел A потомком узла B?» за постоянное время, просто используя два вспомогательных массива.

Предварительно обработайте дерево, посетив его в порядке глубины, и для каждого узла A сохраните время начала и окончания посещения в двух массивах Start[] и End[].

Итак, допустим, что End[u] и Start[u] — это соответственно время окончания и начала посещения узла u.

Тогда узел u является потомком узла v тогда и только тогда, когда:

Start[v] ‹= Start[u] и End[u] ‹= End[v].

и все готово, для проверки этого условия требуется всего два поиска в массивах Start и End

person Giro Tonti    schedule 24.11.2012
comment
К сожалению, дерево не статично, поэтому я не могу его предварительно обработать. - person Philip Kamenarsky; 25.11.2012

Взгляните на модель вложенных наборов. Очень эффективно выбирать, но слишком медленно обновлять

person Bohdan Petrenko    schedule 11.04.2018