Самый эффективный способ генерировать все потомки всех узлов дерева

9

Я ищу наиболее эффективный алгоритм, чтобы взять дерево (хранится как список ребер; ИЛИ как список отображений из родительского узла в список дочерних узлов); и создайте для КАЖДОГО узла список всех узлов, произошедших от него (конечный уровень и не конечный уровень).

Реализация должна быть с помощью циклов вместо повторения, из-за масштаба; и в идеале должно быть O (N).

Этот вопрос SO охватывает стандартное достаточно очевидное решение для поиска ответа для ОДНОГО узла в дереве. Но, очевидно, повторение этого алгоритма на каждом узле дерева крайне неэффективно (от вершины моей головы, от O (NlogN) до O (N ^ 2)).

Корень дерева известен. Дерево имеет абсолютно произвольную форму (например, не является N-ным, не сбалансировано каким-либо образом, имеет форму или форму, не имеет одинаковой глубины) - в некоторых узлах есть 1-2 дочерних элемента, в некоторых - 30 тыс. Дочерних элементов

На практическом уровне (хотя это не должно влиять на алгоритм) дерево имеет ~ 100K-200K узлов.

DVK
источник
Вы можете смоделировать рекурсию, используя цикл и стек, это разрешено для вашего решения?
Джорджио
@ Джорджио - конечно. Это то, что я пытался подразумевать под «петлями вместо отказов».
ДВК

Ответы:

5

Если вы действительно хотите ПРОИЗВОДИТЬ каждый список в виде разных копий, вы не можете надеяться, что в худшем случае вы получите пробел, превосходящий n ^ 2. Если вам просто нужен доступ к каждому списку:

Я бы выполнил обход дерева по порядку, начиная с корня:

http://en.wikipedia.org/wiki/Tree_traversal

Затем для каждого узла в дереве сохраните минимальный порядковый номер и максимальный порядковый номер в своем поддереве (это легко поддерживается с помощью рекурсии - и вы можете смоделировать это с помощью стека, если хотите).

Теперь вы помещаете все узлы в массив A длины n, где узел с порядковым номером i находится в позиции i. Затем, когда вам нужно найти список для узла X, вы смотрите в A [X.min, X.max] - обратите внимание, что этот интервал будет включать в себя узел X, который также можно легко исправить.

Все это выполняется за O (n) время и занимает O (n) пространство.

Надеюсь, это поможет.

Джеспер Нильсен
источник
2

Неэффективная часть - это не обход дерева, а построение списков узлов. Казалось бы, разумно создать такой список:

descendants[node] = []
for child in node.childs:
    descendants[node].push(child)
    for d in descendants[child]:
        descendants[node].push(d)

Поскольку каждый узел-потомок копируется в список каждого родителя, мы получаем в среднем O (n log n) сложности для сбалансированных деревьев и O (n²) наихудший случай для вырожденных деревьев, которые действительно связаны списками.

Мы можем перейти к O (n) или O (1) в зависимости от того, нужно ли вам выполнять какую-либо настройку, если мы используем ленивый способ вычисления списков. Предположим, child_iterator(node)что у нас есть дочерние элементы этого узла. Затем мы можем определить тривиально descendant_iterator(node):

def descendant_iterator(node):
  for child in child_iterator(node):
    yield from descendant_iterator(child)
  yield node

Нерекурсивное решение намного сложнее, поскольку управление итератором является сложным (сопрограммы!). Я обновлю этот ответ позже сегодня.

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

Конечно, это не сработает, если вам нужна фактическая коллекция для работы.

Амон
источник
Извините, -1. Вся цель аглорита - предварительные вычисления данных. Ленивые вычисления полностью побеждают причину даже запуска алгоритма.
ДВК
2
@DVK Хорошо, возможно, я неправильно понял ваши требования. Что вы делаете с полученными списками? Если предварительные вычисления списков являются узким местом (но не используют списки), это будет означать, что вы не используете все данные, которые вы агрегируете, и тогда ленивые вычисления будут победой. Но если вы используете все данные, алгоритм предварительного вычисления в значительной степени не имеет значения - алгоритмическая сложность использования данных будет, по крайней мере, равна сложности построения списков.
Амон
0

Этот короткий алгоритм должен сделать это, посмотрите на код public void TestTreeNodeChildrenListing()

Алгоритм фактически последовательно проходит через узлы дерева и ведет список родителей текущего узла. Согласно вашему требованию текущий узел является дочерним для каждого родительского узла, который добавляется к каждому из них как дочерний.

Конечный результат сохраняется в словаре.

    [TestFixture]
    public class TreeNodeChildrenListing
    {
        private TreeNode _root;

        [SetUp]
        public void SetUp()
        {
            _root = new TreeNode("root");
            int rootCount = 0;
            for (int i = 0; i < 2; i++)
            {
                int iCount = 0;
                var iNode = new TreeNode("i:" + i);
                _root.Children.Add(iNode);
                rootCount++;
                for (int j = 0; j < 2; j++)
                {
                    int jCount = 0;
                    var jNode = new TreeNode(iNode.Value + "_j:" + j);
                    iCount++;
                    rootCount++;
                    iNode.Children.Add(jNode);
                    for (int k = 0; k < 2; k++)
                    {
                        var kNode = new TreeNode(jNode.Value + "_k:" + k);
                        jNode.Children.Add(kNode);
                        iCount++;
                        rootCount++;
                        jCount++;

                    }
                    jNode.Value += " ChildCount:" + jCount;
                }
                iNode.Value += " ChildCount:" + iCount;
            }
            _root.Value += " ChildCount:" + rootCount;
        }

        [Test]
        public void TestTreeNodeChildrenListing()
        {
            var iteration = new Stack<TreeNode>();
            var parents = new List<TreeNode>();
            var dic = new Dictionary<TreeNode, IList<TreeNode>>();

            TreeNode node = _root;
            while (node != null)
            {
                if (node.Children.Count > 0)
                {
                    if (!dic.ContainsKey(node))
                        dic.Add(node,new List<TreeNode>());

                    parents.Add(node);
                    foreach (var child in node.Children)
                    {
                        foreach (var parent in parents)
                        {
                            dic[parent].Add(child);
                        }
                        iteration.Push(child);
                    }
                }

                if (iteration.Count > 0)
                    node = iteration.Pop();
                else
                    node = null;

                bool removeParents = true;
                while (removeParents)
                {
                    var lastParent = parents[parents.Count - 1];
                    if (!lastParent.Children.Contains(node)
                        && node != _root && lastParent != _root)
                    {
                        parents.Remove(lastParent);
                    }
                    else
                    {
                        removeParents = false;
                    }
                }
            }
        }
    }

    internal class TreeNode
    {
        private IList<TreeNode> _children;
        public string Value { get; set; }

        public TreeNode(string value)
        {
            _children = new List<TreeNode>();
            Value = value;
        }

        public IList<TreeNode> Children
        {
            get { return _children; }
        }
    }
}
Низко летящий пеликан
источник
Для меня это очень похоже на сложность от O (n log n) до O (n²) и улучшается лишь незначительно по сравнению с ответом, с которым DVK связался в своем вопросе. Так что, если это не улучшение, как это ответит на вопрос? Единственное значение, которое добавляет этот ответ, - это демонстрация итеративного выражения наивного алгоритма.
Амон
Это O (n). Если вы внимательно посмотрите на алгоритм, он повторяется один раз по узлам. В то же время он создает коллекцию дочерних узлов для каждого родительского узла одновременно.
Низколетящий пеликан
1
Вы перебираете все узлы, а это O (n). Затем вы перебираете все дочерние элементы, которые мы пока проигнорируем (давайте представим, что это некоторый постоянный фактор). Затем вы перебираете всех родителей текущего узла. В дереве балансов это O (log n), но в вырожденном случае, когда наше дерево представляет собой связанный список, это может быть O (n). Поэтому, если мы умножим стоимость зацикливания на всех узлах на стоимость зацикливания на их родителях, мы получим O (n log n) - O (n²) временную сложность. Без многопоточности не существует «одновременно».
Амон
«В то же время» означает, что он создает коллекцию в том же цикле, и другие циклы не задействованы.
Низколетящий пеликан
0

Обычно вы просто используете рекурсивный подход, поскольку он позволяет вам переключать порядок выполнения так, чтобы вы могли вычислить количество листьев, начиная с листьев вверх. Поскольку для обновления текущего узла вам потребуется использовать результат вашего рекурсивного вызова, потребуется специальное усилие для получения хвостовой рекурсивной версии. Если вы не предпримете это усилие, тогда, конечно, этот подход просто взорвет ваш стек для большого дерева.

Учитывая, что мы поняли, что основная идея состоит в том, чтобы получить порядок зацикливания, начинающийся с листьев и возвращающийся обратно к корню, естественная идея, которая приходит в голову, - выполнить топологическую сортировку дерева. Результирующая последовательность узлов может проходить линейно, чтобы суммировать количество листьев (при условии, что вы можете проверить, является ли узел листом O(1)). Общая временная сложность топологического вида равна O(|V|+|E|).

Я предполагаю, что ваше Nчисло узлов, которое |V|обычно (из номенклатуры DAG). Размер Eс другой стороны сильно зависит от арности вашего дерева. Например, двоичное дерево имеет не более 2 ребер на узел, поэтому O(|E|) = O(2*|V|) = O(|V|)в этом случае это приведет к общему O(|V|)алгоритму. Обратите внимание, что из-за общей структуры дерева у вас не может быть чего-то подобного O(|E|) = O(|V|^2). Фактически, поскольку каждый узел имеет уникального родителя, вы можете иметь не более одного ребра для подсчета на узел, когда вы рассматриваете только родительские отношения, поэтому для деревьев у нас есть гарантия того, что O(|E|) = O(|V|). Поэтому приведенный выше алгоритм всегда линейен по размеру дерева.

Фрэнк
источник