'การทำลายป่า' ลบ 'ต้นไม้' ออกจากโปรแกรมอย่างไร


12

ฉันคิดว่าเข้าใจว่าการตัดไม้ทำลายป่าใช้งานและสร้างรายการได้อย่างไรในเวลาเดียวกัน (จากการพับและฟังก์ชั่นการขยาย - ดูคำตอบที่ดีใน CodeReview ที่นี่ ) แต่เมื่อฉันเปรียบเทียบกับรายการ wikipedia เกี่ยวกับเทคนิคที่พูดถึง ต้นไม้ 'จากโปรแกรม

ฉันเข้าใจว่าโปรแกรมสามารถแยกวิเคราะห์ลงในแผนผังวากยสัมพันธ์ (ถูกไหม?) แต่อะไรคือความหมายของการใช้การตัดไม้ทำลายป่าเพื่อความเรียบง่าย (มันคืออะไร) ของโปรแกรม? และฉันจะทำอย่างไรกับรหัสของฉัน?

คำตอบ:


9

Yatima2975 ดูเหมือนจะครอบคลุมคำถามสองข้อแรกของคุณฉันจะพยายามครอบคลุมคำถามที่สาม เมื่อต้องการทำสิ่งนี้ฉันจะปฏิบัติต่อกรณีที่เรียบง่ายไม่สมจริง แต่ฉันแน่ใจว่าคุณจะสามารถจินตนาการถึงสิ่งที่สมจริงกว่าได้

ลองนึกภาพคุณต้องการที่จะคำนวณความลึกของเต็มต้นไม้ไบนารีของการสั่งซื้อnประเภทของต้นไม้ไบนารี (ไม่มีป้ายกำกับ) คือ (ในไวยากรณ์ Haskell):n

type Tree = Leaf | Node Tree Tree

ตอนนี้ต้นไม้ที่เต็มรูปแบบของการสั่งซื้อคือ:n

full : Int -> Tree
full n | n == 0 = Leaf
full n = Node (full (n-1)) (full (n-1))

และความลึกของต้นไม้คำนวณโดย

depth : Tree -> Int
depth Leaf = 0
depth (Node t1 t2) = 1 + max (depth t1) (depth t2)

ตอนนี้คุณสามารถเห็นได้ว่าการคำนวณใด ๆ ของแรกจะสร้างต้นไม้เต็มรูปแบบของการสั่งซื้อใช้แล้วแยกแยะต้นไม้ที่ใช้{} การตัดไม้ทำลายป่าขึ้นอยู่กับการสังเกตว่ารูปแบบ (การก่อสร้างตามด้วย deconstruction) สามารถลัดวงจรได้บ่อยครั้ง: เราสามารถแทนที่การเรียกใด ๆ ไปยังโดยการเรียกเพียงครั้งเดียวเพื่อ :n f u l l d e p t h d e p t h ( f u l l n ) f u l l _ d e p t hdepth (full n)nfulldepthdepth (full n)full_depth

full_depth : Int -> Int
full_depth n | n == 0 = 0
full_depth n = 1 + max (full_depth (n-1)) (full_depth (n-1))

สิ่งนี้หลีกเลี่ยงการจัดสรรหน่วยความจำของแผนผังเต็มและความจำเป็นในการดำเนินการจับคู่รูปแบบซึ่งช่วยปรับปรุงประสิทธิภาพอย่างมาก นอกจากนี้หากคุณเพิ่มการเพิ่มประสิทธิภาพ

max t t --> t

จากนั้นคุณเปลี่ยนขั้นตอนเวลาเอ็กซ์โปเนนเชียลเป็นเวลาเชิงเส้นหนึ่ง ... มันจะเจ๋งถ้ามีการเพิ่มประสิทธิภาพเพิ่มเติมที่รับรู้ว่าเป็นตัวตนของจำนวนเต็ม แต่ฉันไม่แน่ใจว่าเช่นนั้น การปรับให้เหมาะสมที่สุดถูกใช้ในทางปฏิบัติfull_depth

คอมไพเลอร์หลักเท่านั้นที่ดำเนินการตัดไม้ทำลายป่าโดยอัตโนมัติคือ GHC และถ้าฉันจำได้ถูกต้องสิ่งนี้จะดำเนินการเฉพาะเมื่อประกอบฟังก์ชั่นในตัว (สำหรับเหตุผลทางเทคนิค)


ได้รับรางวัลเพราะฉันได้รับคำตอบมากกว่านี้จากสูตรที่ได้รับมากกว่าคำตอบอื่น ๆ แม้ว่าพวกเขาจะครอบคลุมอาณาเขตเดียวกัน
Cris Stringfellow

6

ก่อนรายการเป็นชนิดของต้นไม้ หากเราแสดงรายการเป็นรายการที่เชื่อมโยงมันเป็นเพียงทรีที่แต่ละโหนดมีลูกหลาน 1 หรือ 0 คน

แยกวิเคราะห์ต้นไม้เป็นเพียงการใช้ประโยชน์ของต้นไม้เป็นโครงสร้างข้อมูล ต้นไม้มีแอปพลิเคชั่นมากมายในสาขาวิทยาศาสตร์คอมพิวเตอร์รวมถึงการเรียงลำดับการนำแผนที่ไปใช้อาเรย์แบบเชื่อมโยง ฯลฯ

โดยทั่วไปรายการต้นไม้ ฯลฯ เป็นโครงสร้างข้อมูลแบบเรียกซ้ำ: แต่ละโหนดมีข้อมูลบางส่วนและอินสแตนซ์อื่นของโครงสร้างข้อมูลเดียวกัน การพับคือการดำเนินการกับโครงสร้างดังกล่าวทั้งหมดที่แปลงโหนดเป็นค่า "bottom up" แบบวนซ้ำ การแฉเป็นกระบวนการย้อนกลับจะแปลงค่าเป็นโหนด "จากบนลงล่าง"

สำหรับโครงสร้างข้อมูลที่กำหนดเราสามารถสร้างฟังก์ชั่นการพับและการแฉกลไกได้

ตัวอย่างเช่นลองทำรายการ (ฉันจะใช้ Haskell เพื่อเป็นตัวอย่างในขณะที่พิมพ์และไวยากรณ์ของมันสะอาดมาก) รายการอาจเป็นจุดสิ้นสุดหรือค่าและ "ส่วนท้าย"

data List a = Nil | Cons a (List a)

สมมติว่าเรากำลังพับรายการ ในแต่ละขั้นตอนเรามีโหนดปัจจุบันที่จะพับและเราได้พับโหนดย่อยซ้ำแล้วซ้ำอีก เราสามารถแสดงสถานะนี้เป็น

data ListF a r = NilF | ConsF a r

โดยที่rเป็นค่ากลางที่สร้างขึ้นโดยการพับรายการย่อย สิ่งนี้ทำให้เราสามารถแสดงฟังก์ชั่นการพับบนรายการ:

foldList :: (ListF a r -> r) -> List a -> r
foldList f Nil            = f NilF
foldList f (Cons x xs)    = f (ConsF x (foldList f xs))

เราแปลงListเข้าไปโดยซ้ำพับมากกว่ารายการย่อยและจากนั้นใช้ฟังก์ชั่นที่กำหนดไว้ในListF ListFหากคุณคิดเกี่ยวกับสิ่งนี้นี่เป็นเพียงการนำเสนอมาตรฐานอื่นfoldr:

foldr :: (a -> r -> r) -> r -> List a -> r
foldr f z = foldList g
  where
    g NilF          = z
    g (ConsF x r)   = f x r

เราสามารถสร้างunfoldListในแบบเดียวกัน:

unfoldList :: (r -> ListF a r) -> r -> List a
unfoldList f r = case f r of
                  NilF        -> Nil
                  ConsF x r'  -> Cons x (unfoldList f r')

อีกครั้งมันเป็นเพียงการเป็นตัวแทนของunfoldr:

unfoldr :: (r -> Maybe (a, r)) -> r -> [a]

(ขอให้สังเกตว่าMaybe (a, r)isomorphic ถึงListF a r)

และเราสามารถสร้างฟังก์ชั่นการตัดไม้ทำลายป่าด้วย:

deforest :: (ListF a r -> r) -> (s -> ListF a s) -> s -> r
deforest f u s = f (map (deforest f u) (u s))
  where
    map h NilF        = NilF
    map h (ConsF x r) = ConsF x (h r)

มันแยกกลางListและผสมฟังก์ชั่นการพับและการคลี่เข้าด้วยกันอย่างง่ายดาย

ขั้นตอนเดียวกันสามารถนำไปใช้กับโครงสร้างข้อมูลแบบเรียกซ้ำ ตัวอย่างเช่นต้นไม้ที่มีโหนดสามารถมี 0, 1, 2 หรือลูกหลานที่มีค่าในโหนด 1- หรือ 0 สาขา:

data Tree a = Bin (Tree a) (Tree a) | Un a (Tree a) | Leaf a

data TreeF a r = BinF r r | UnF a r | LeafF a

treeFold :: (TreeF a r -> r) -> Tree a -> r
treeFold f (Leaf x)       = f (LeafF x)
treeFold f (Un x r)       = f (UnF x (treeFold f r))
treeFold f (Bin r1 r2)    = f (BinF (treeFold f r1) (treeFold f r2))

treeUnfold :: (r -> TreeF a r) -> r -> Tree a
treeUnfold f r = case f r of
                  LeafF x         -> Leaf x
                  UnF x r         -> Un x (treeUnfold f r)
                  BinF r1 r2      -> Bin (treeUnfold f r1) (treeUnfold f r2)

แน่นอนเราสามารถสร้างdeforestTreeกลไกได้เหมือนเมื่อก่อน

(โดยปกติเราจะแสดงtreeFoldความสะดวกมากขึ้นเช่น:

treeFold' :: (r -> r -> r) -> (a -> r -> r) -> (a -> r) -> Tree a -> r

)

ฉันจะออกจากรายละเอียดฉันหวังว่ารูปแบบที่ชัดเจน

ดูสิ่งนี้ด้วย:


คำตอบที่ดีขอบคุณ ลิงก์และตัวอย่างโดยละเอียดมีค่า
Cris Stringfellow

3

มันค่อนข้างสับสน แต่มีการตัดไม้ทำลายป่า (ใช้เวลารวบรวม) เพื่อกำจัดต้นไม้กลางที่จะถูกสร้างขึ้น (ในเวลาทำงาน) การตัดไม้ทำลายป่าไม่เกี่ยวข้องกับการแฮ็กส่วนต่างๆของต้นไม้ไวยากรณ์นามธรรม (นั่นคือการกำจัดสาขาที่ตายแล้ว :-)

อีกสิ่งหนึ่งที่อาจทำให้คุณต้องผิดหวังก็คือรายการนั้นเป็นต้นไม้เพียงแค่ไม่สมดุลมาก!


อ่าใช่ ไม่สมดุลมาก !
Cris Stringfellow
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.