Найефективніший спосіб генерації всіх нащадків усіх вузлів на дереві


9

Я шукаю найефективніший алгоритм взяти дерево (зберігається як список ребер; АБО як список відображень від батьківського вузла до списку дочірніх вузлів); і створити для КОЖНОГО вузла список усіх вузлів, що спускаються з нього (рівень аркуша та рівень без листя).

Реалізація повинна здійснюватися через цикли замість рекусії, через масштаб; і в ідеалі має бути O (N).

Це питання стосується стандартного досить очевидного рішення для пошуку відповіді для ONE вузла на дереві. Але очевидно, що повторення цього алгоритму на кожному дереві є дуже неефективним (від верхньої частини голови, від O (NlogN) до O (N ^ 2)).

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

На практичному рівні (хоча це не повинно впливати на алгоритм) дерево має ~ 100K-200K вузлів.


Ви можете імітувати рекурсію за допомогою циклу та стека, чи це дозволено для вашого рішення?
Джорджіо

@Giorgio - звичайно. Це те, що я намагався зрозуміти "через петлі замість рекусії".
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. Вся мета аглоритму - попередньо обчислити дані. Ледачі обчислення повністю перемагають причину навіть запуску альго.
DVK

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|). Тому вищевказаний алгоритм завжди лінійний за розміром дерева.

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