วิธีที่มีประสิทธิภาพที่สุดในการสร้างการสืบทอดทั้งหมดของโหนดทั้งหมดในทรี


9

ฉันกำลังมองหาอัลกอริทึมที่มีประสิทธิภาพที่สุดในการดึงทรี (จัดเก็บเป็นรายการขอบหรือเป็นรายการของการแมปจากโหนดแม่ไปยังรายการโหนดย่อย) และสร้างสำหรับโหนด EVERY รายการของโหนดทั้งหมดที่สืบเชื้อสายมาจากมัน (ระดับใบและระดับที่ไม่ใช่ใบ)

การดำเนินการจะต้องผ่านลูปแทนการผสมเนื่องจากขนาด และควรเป็น O (N)

คำถาม SO นี้ครอบคลุมโซลูชันมาตรฐานที่เห็นได้ชัดเจนพอสมควรสำหรับการค้นหาคำตอบสำหรับหนึ่งโหนดในทรี แต่เห็นได้ชัดว่าการทำซ้ำอัลกอริทึมนั้นในโหนดต้นไม้ทุกโหนดไม่มีประสิทธิภาพสูง (จากส่วนหัวของฉัน O (NlogN) ถึง O (N ^ 2)

รู้จักรากของต้นไม้ ต้นไม้มีรูปร่างตามอำเภอใจอย่างแน่นอน (เช่นไม่ใช่ N-nary ไม่สมดุลในทางใดรูปร่างหรือรูปแบบไม่ลึกสม่ำเสมอ) - บางโหนดมีลูก 1-2 คนบางคนมีลูก 30K

ในระดับปฏิบัติ (แม้ว่าจะไม่ส่งผลกระทบต่ออัลกอริทึม) ต้นไม้มีโหนด ~ 100K-200K


คุณสามารถจำลองการสอบถามซ้ำโดยใช้การวนซ้ำและการซ้อนซ้ำสิ่งนี้อนุญาตให้ใช้กับโซลูชันของคุณหรือไม่
Giorgio

@Giorgio - แน่นอน นั่นคือสิ่งที่ฉันพยายามที่จะบอกเป็นนัยถึง "ผ่านลูปแทนการอธิบายซ้ำ"
DVK

คำตอบ:


5

หากคุณต้องการให้ PRODUCE ทุกรายการเป็นสำเนาที่แตกต่างกันคุณไม่สามารถหวังได้ว่าจะได้ช่องว่างที่ดีกว่า 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

วิธีแก้ปัญหาที่ไม่เกิดซ้ำมีส่วนเกี่ยวข้องมากกว่าเนื่องจากการควบคุมตัววนซ้ำนั้นยุ่งยาก (coroutines!) ฉันจะอัปเดตคำตอบนี้ในวันนี้

เนื่องจากการสำรวจเส้นทางของทรีคือ O (n) และการวนซ้ำในรายการนั้นเป็นแบบเส้นตรงเช่นกันเคล็ดลับนี้ทำให้เกิดค่าใช้จ่ายจนเสียค่าใช้จ่าย ตัวอย่างเช่นการพิมพ์รายการของผู้สืบทอดสำหรับแต่ละโหนดมีความซับซ้อนของตัวพิมพ์ใหญ่ที่สุด O (n²): การวนซ้ำโหนดทั้งหมดคือ O (n) และวนซ้ำสำหรับลูกหลานของโหนดแต่ละโหนดไม่ว่าพวกเขาจะถูกเก็บไว้ในรายการหรือเฉพาะกิจ .

แน่นอนว่าจะไม่สามารถใช้งานได้หากคุณต้องการคอลเล็กชันจริงเพื่อใช้งาน


ขออภัย -1 จุดประสงค์ทั้งหมดของ aglorithm คือการคำนวณข้อมูลล่วงหน้า การคำนวณขี้เกียจเป็นการเอาชนะเหตุผลทั้งหมดแม้แต่การเรียกใช้อัลโก
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) ถ้าคุณดูอัลกอริธึมอย่างใกล้ชิดมันจะทำซ้ำโหนดหนึ่งครั้ง ในเวลาเดียวกันมันจะสร้างคอลเลกชันของโหนดลูกสำหรับแต่ละโหนดแม่ในเวลาเดียวกัน
นกกระทุงบินต่ำ

1
คุณวนซ้ำโหนดทั้งหมดซึ่งเป็น O (n) จากนั้นคุณวนลูปทั้งหมดที่เราจะไม่สนใจตอนนี้ (ลองจินตนาการว่ามันเป็นปัจจัยคงที่) จากนั้นคุณวนลูปผ่านพาเรนต์ทั้งหมดของโหนดปัจจุบัน ในต้นไม้ยอดคงเหลือนี่คือ O (บันทึก n) แต่ในกรณีที่ความเสื่อมที่ต้นไม้ของเราเป็นรายการที่เชื่อมโยงมันอาจเป็น O (n) ดังนั้นหากเราคูณค่าใช้จ่ายของการวนลูปผ่านโหนดทั้งหมดด้วยค่าใช้จ่ายของการวนลูปผ่านพาเรนต์พวกมันจะได้ความซับซ้อนของเวลา O (n log n) ถึง O (n²) หากไม่มีมัลติเธรดจะไม่มี "ในเวลาเดียวกัน"
amon

"ในเวลาเดียวกัน" หมายความว่ามันสร้างคอลเลกชันในลูปเดียวกันและไม่มีลูปอื่น ๆ ที่เกี่ยวข้อง
นกกระทุงบินต่ำ

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.