ツリー内のすべてのノードのすべての子孫を生成する最も効率的な方法


9

ツリー(エッジのリスト、または親ノードから子ノードのリストへのマッピングのリストのいずれかとして格納)を取得するための最も効率的なアルゴリズムを探しています。すべてのノードについて、そのノードの子孫であるすべてのノードのリスト(リーフレベルと非リーフレベル)を生成します。

規模の関係上、実装は反省ではなくループを介して行う必要があります。理想的にはO(N)である必要があります。

このSOの質問は、ツリー内の1つのノードの答えを見つけるための標準的な合理的に明白なソリューションをカバーしています。しかし、明らかに、すべてのツリーノードでそのアルゴリズムを繰り返すことは非常に非効率的です(頭上からO(NlogN)からO(N ^ 2)へ)。

ツリーのルートは既知です。ツリーは完全に任意の形状です(例:N-naryではなく、いかなる形でもバランスが取れていない、形状または形式、均一な深さではない)-一部のノードには1〜2個の子があり、一部には30Kの子があります。

実用的なレベルでは(アルゴリズムには影響しないはずですが)、ツリーには100K〜200Kのノードがあります。


ループとスタックを使用して再帰をシミュレートできますが、これはソリューションで許可されていますか?
Giorgio、

@ジョルジオ-もちろん。それが「反省の代わりにループを介して」私が暗示しようとしたことです。
DVK、

回答:


5

すべてのリストを異なるコピーとして実際に生成したい場合、最悪の場合、n ^ 2よりも優れたスペースを実現することは望めません。各リストへのアクセスが必要な場合:

ルートから開始して、ツリー内を順番にトラバースします。

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

次に、ツリー内の各ノードについて、サブツリーに最小の順序番号と最大の順序番号を格納します(これは、再帰によって簡単に維持できます。必要に応じて、スタックを使用してシミュレーションできます)。

次に、すべてのノードを長さnの配列Aに配置します。順序番号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、2015年

2
@DVK OK、私はあなたの要件を誤解しているかもしれません。結果のリストで何をしていますか?リストの事前計算がボトルネックである(ただし、リストを使用していない)場合、これは、集計したすべてのデータを使用していないことを示し、遅延計算が有利になります。ただし、すべてのデータを使用する場合、事前計算のアルゴリズムはほとんど関係ありません。データを使用するアルゴリズムの複雑さは、少なくともリストの作成の複雑さと同じです。
2015年

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がリンクした回答をわずかに改善するだけです。これが改善されない場合、これはどのように質問に答えますか?この回答が追加する唯一の価値は、単純なアルゴリズムの反復表現を紹介することです。
amon、2015年

O(n)です。アルゴリズムを詳しく見ると、ノードに対して1回反復されます。同時に、各親ノードの子ノードのコレクションを同時に作成します。
低空飛行ペリカン2015年

1
O(n)であるすべてのノードをループします。次に、すべての子をループ処理します。これは今のところ無視します(一定の要因であることを想像してみてください)。次に、現在のノードのすべての親をループします。バランスツリーでは、これはO(log n)ですが、ツリーがリンクリストである縮退の場合、O(n)になる場合があります。したがって、すべてのノードをループするコストとその親をループするコストを掛けると、O(n log n)からO(n²)の時間の複雑さが得られます。マルチスレッドなしでは、「同時に」はありません。
2015年

「同時に」とは、同じループでコレクションを作成し、他のループが関与していないことを意味します。
低空飛行ペリカン2015年

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)。各ノードがユニークな親を持っているので、実際には、あなたが唯一の親の関係を考えるときに木のために、我々はその保証を持っている、ノードごとにカウントするために最大で1つのエッジで持つことができますO(|E|) = O(|V|)。したがって、上記のアルゴリズムは常にツリーのサイズで線形です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.