ツリー(エッジのリストとして保存されている最も効率的なアルゴリズムを探しています。OR親ノードから子ノードのリストへのマッピングのリストとして);); 、すべてのノードの場合、そのノードから派生したすべてのノードのリスト(リーフレベルと非リーフレベル)。
規模の関係上、実装は反省ではなくループを介して行う必要があります。理想的にはO(N)である必要があります。
このSO質問 は、ツリー内の1つのノードの答えを見つけるための標準的な合理的に明白なソリューションをカバーしています。しかし、明らかに、すべてのツリーノードでそのアルゴリズムを繰り返すことは非常に非効率的です(頭の上からO(NlogN) to O(N ^ 2)).
ツリーのルートは既知です。ツリーは完全に任意の形状です(例:N-naryではなく、どのような形でもバランスが取れていない、形状または形式、均一な深さではない)。
実際には(アルゴリズムには影響しないはずですが)、ツリーには〜100K〜200Kのノードがあります。
実際にすべてのリストを異なるコピーとして生成したい場合、最悪の場合、n ^ 2よりも優れたスペースを実現することは望めません。各リストへのアクセスが必要な場合:
ルートから開始して、ツリー内を順番にトラバースします。
http://en.wikipedia.org/wiki/Tree_traversal
次に、ツリー内の各ノードに対して、サブツリーに最小順序番号と最大順序番号を格納します(これは、再帰によって簡単に維持できます。必要に応じて、スタックを使用してシミュレーションできます)。
次に、すべてのノードを長さnの配列Aに配置します。順序番号iのノードは位置iにあります。次に、ノードXのリストを見つける必要がある場合は、A [X.min、X.max]を調べます。この間隔にはノードXも含まれることに注意してください。これも簡単に修正できます。
これはすべてO(n)時間で完了し、O(n)スペースがかかります。
これがお役に立てば幸いです。
非効率な部分はツリーを走査するのではなく、ノードのリストを作成することです。次のようにリストを作成するのが賢明なようです。
_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)であり、したがって、ノードがリストに格納されているか計算されたアドホックであるかに関係なく、各ノードの子孫で反復しています。
もちろん、実際のコレクションで作業する必要がある場合、これは機能しません。
この短いアルゴリズムはそれを行うべきです、コードを見てください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(1)
で葉であることを確認できると仮定した場合)。トポロジカルソートの全体的な時間の複雑さはO(|V|+|E|)
です。
私はあなたのN
がノードの数であると仮定します。これは通常、(DAG命名法から)_|V|
_になります。一方、E
のサイズは、ツリーのアリティに大きく依存します。たとえば、バイナリツリーではノードあたり最大2つのエッジがあるため、その場合はO(|E|) = O(2*|V|) = O(|V|)
となり、全体的なO(|V|)
アルゴリズムになります。ツリーの全体的な構造により、O(|E|) = O(|V|^2)
のようなものは使用できないことに注意してください。実際、各ノードには一意の親があるため、親の関係のみを考慮した場合、ノードごとに最大1つのEdgeをカウントできます。したがって、ツリーの場合、O(|E|) = O(|V|)
が保証されます。したがって、上記のアルゴリズムは常にツリーのサイズで線形です。