트리에서 모든 노드의 모든 자손을 생성하는 가장 효율적인 방법


9

트리를 가져 오는 가장 효율적인 알고리즘을 찾고 있습니다 (가장자리 목록으로 저장되거나 부모 노드에서 자식 노드 목록으로 매핑 목록으로 저장 됨). 모든 노드에 대해 그 노드의 하위 노드 (리프 레벨 및 비 리프 레벨)의 목록을 생성합니다.

스케일로 인해 구현은 recusion 대신 루프를 거쳐야합니다. 이상적으로 O (N)이어야합니다.

이 SO 질문 은 트리에서 하나의 노드에 대한 답변을 찾기 위해 합리적으로 명확한 표준 솔루션을 다룹니다. 그러나 분명히 모든 트리 노드에서 해당 알고리즘을 반복하는 것은 매우 비효율적입니다 (제 머리 꼭대기에서 O (NlogN) ~ O (N ^ 2)).

나무 뿌리가 알려져 있습니다. 나무는 절대적으로 임의의 모양입니다 (예 : N-nary가 아니고 어떤 식 으로든 균형을 잡지 않고 모양이 균일하지 않습니다)-일부 노드에는 1-2 명의 자녀가 있고 일부에는 30K 명의 자녀가 있습니다.

실용적인 수준에서 (알고리즘에는 영향을 미치지는 않지만) 트리에는 ~ 100K-200K 노드가 있습니다.


루프와 스택을 사용하여 재귀를 시뮬레이션 할 수 있습니다. 솔루션에 허용됩니까?
Giorgio

@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)이므로 각 노드의 하위 항목에 대해 목록에 저장되어 있거나 계산 된 Ad Hoc에 관계없이 반복됩니다. .

물론 실제 컬렉션이 필요한 경우에는 작동하지 않습니다.


죄송합니다 -1 aglorithm의 전체 목적은 데이터를 사전 계산하는 것입니다. 게으른 계산은 심지어 algo를 실행하는 이유를 완전히 물리 치고 있습니다.
DVK

2
@DVK 네, 귀하의 요구 사항을 이해하지 못했을 수 있습니다. 결과 목록으로 무엇을하고 있습니까? 목록을 사전 계산하는 데 병목 현상이 있지만 (목록을 사용하지 않는 경우) 집계 한 모든 데이터를 사용하고 있지 않다는 것을 나타내므로 지연 계산이 승리합니다. 그러나 모든 데이터를 사용하는 경우 사전 계산 알고리즘은 크게 관련이 없습니다. 데이터 사용의 알고리즘 복잡성은 목록 작성의 복잡성과 최소한 같습니다.
amon

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

알고리즘은 O (n)입니다. 알고리즘을 자세히 보면 노드에서 한 번 반복됩니다. 동시에 각 부모 노드에 대한 자식 노드 모음을 동시에 만듭니다.
Low Flying Pelican

1
모든 노드 (O (n))를 반복합니다. 그런 다음 모든 자식을 반복합니다. 지금은 무시할 것입니다 (일관된 요소라고 상상해 봅시다). 그런 다음 현재 노드의 모든 부모를 반복합니다. 잔액 트리에서 이것은 O (log n)이지만 트리가 연결된 목록 인 축 퇴한 경우 O (n) 일 수 있습니다. 따라서 모든 노드를 통한 루핑 비용과 부모를 통한 루핑 비용을 곱하면 O (n log n)에서 O (n²)의 시간 복잡성을 얻게됩니다. 멀티 스레딩이 없으면 "동시"가 없습니다.
amon

"동시에"는 동일한 루프에서 콜렉션을 작성하고 다른 루프는 포함하지 않음을 의미합니다.
Low Flying Pelican

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|). 따라서 위의 알고리즘은 항상 트리 크기에서 선형입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.