อะไรคือความแตกต่างระหว่างจากบนลงล่างและจากบนลงล่าง?


177

วิธีการจากล่างขึ้นบน (สู่การเขียนโปรแกรมแบบไดนามิก) ประกอบด้วยการดูปัญหาย่อย "เล็ก" เป็นครั้งแรกจากนั้นจึงแก้ปัญหาย่อยที่ใหญ่กว่าโดยใช้วิธีแก้ไขปัญหาที่มีขนาดเล็กลง

จากบนลงล่างประกอบด้วยการแก้ปัญหาในลักษณะ "เป็นธรรมชาติ" และตรวจสอบว่าคุณได้คำนวณวิธีแก้ปัญหาไปยังปัญหาย่อยแล้วหรือไม่

ฉันสับสนเล็กน้อย ความแตกต่างระหว่างสองสิ่งนี้คืออะไร?


2
ที่เกี่ยวข้อง: stackoverflow.com/questions/6184869/…
aioobe

คำตอบ:


247

rev4: ความคิดเห็นฝีปากมากโดยผู้ใช้ Sammaron ได้ตั้งข้อสังเกตว่าบางทีคำตอบนี้ก่อนหน้านี้สับสนจากบนลงล่างและจากล่างขึ้นบน ในขณะที่คำตอบเดิม (rev3) นี้และคำตอบอื่น ๆ กล่าวว่า "bottom-up คือ memoization" ("สมมติว่ามีปัญหาย่อย") มันอาจเป็นสิ่งที่ตรงกันข้าม (นั่นคือ "จากบนลงล่าง" อาจเป็น "ถือว่าเป็นปัญหาย่อย" และ " จากล่างขึ้นบน "อาจเป็น" เขียนโปรแกรมย่อย ") ก่อนหน้านี้ฉันได้อ่านเกี่ยวกับการบันทึกความจำซึ่งเป็นประเภทของการเขียนโปรแกรมแบบไดนามิกที่แตกต่างจากประเภทย่อยของการเขียนโปรแกรมแบบไดนามิก ฉันพูดถึงมุมมองนั้นแม้จะไม่ได้สมัครเป็นสมาชิก ฉันได้เขียนคำตอบนี้ใหม่เพื่อไม่เชื่อเรื่องคำศัพท์จนกว่าจะมีการอ้างอิงที่เหมาะสมในวรรณคดี ฉันได้แปลงคำตอบนี้เป็นวิกิชุมชน กรุณาเลือกแหล่งข้อมูลทางวิชาการ รายการอ้างอิง:} {วรรณกรรม: 5 }

ปะยางรถ

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

ตัวอย่างเช่นลองพิจารณาตัวอย่างที่คุณชื่นชอบของ Fibonnaci นี่เป็นต้นไม้เต็มของปัญหาย่อยหากเราทำการเรียกซ้ำแบบไร้เดียงสา:

TOP of the tree
fib(4)
 fib(3)...................... + fib(2)
  fib(2)......... + fib(1)       fib(1)........... + fib(0)
   fib(1) + fib(0)   fib(1)       fib(1)              fib(0)
    fib(1)   fib(0)
BOTTOM of the tree

(ในบางปัญหาที่หายากอื่น ๆ ต้นไม้นี้อาจไม่มีที่สิ้นสุดในบางสาขาซึ่งเป็นตัวแทนที่ไม่สิ้นสุดและด้านล่างของต้นไม้อาจมีขนาดใหญ่อนันต์นอกจากนี้ในบางปัญหาคุณอาจไม่ทราบว่าต้นไม้เต็มหน้าตาเป็นอย่างไร เวลาดังนั้นคุณอาจต้องใช้กลยุทธ์ / อัลกอริทึมในการตัดสินใจว่าปัญหาย่อยใดที่จะเปิดเผย)


การบันทึก, การจัดตาราง

มีอย่างน้อยสองเทคนิคหลักของการเขียนโปรแกรมแบบไดนามิกซึ่งไม่ได้เป็นพิเศษร่วมกัน:

  • การบันทึก - นี่เป็นวิธีการไม่รู้จบ: คุณคิดว่าคุณได้คำนวณปัญหาย่อยทั้งหมดแล้วและคุณไม่มีความคิดว่าลำดับการประเมินที่ดีที่สุดคืออะไร โดยทั่วไปแล้วคุณจะทำการเรียกซ้ำ (หรือการทำซ้ำที่เทียบเท่า) จากรูทและหวังว่าคุณจะเข้าใกล้ลำดับการประเมินผลที่ดีที่สุดหรือรับหลักฐานที่แสดงว่าคุณจะช่วยให้คุณได้รับลำดับการประเมินที่ดีที่สุด คุณจะมั่นใจได้ว่าการเรียกซ้ำซ้ำจะไม่คำนวณปัญหาย่อยเนื่องจากคุณแคชผลลัพธ์และทำให้ต้นไม้ย่อยที่ซ้ำกันไม่ได้รับการคำนวณใหม่

    • ตัวอย่างเช่นถ้าคุณกำลังคำนวณลำดับ Fibonacci fib(100)คุณก็จะเรียกนี้และมันจะเรียกfib(100)=fib(99)+fib(98)ซึ่งจะเรียกfib(99)=fib(98)+fib(97)... ฯลฯ ... fib(2)=fib(1)+fib(0)=1+0=1ซึ่งจะเรียก จากนั้นในที่สุดก็จะแก้ไขfib(3)=fib(2)+fib(1)ได้ แต่ไม่จำเป็นต้องคำนวณใหม่fib(2)เพราะเราแคชไว้
    • สิ่งนี้เริ่มต้นที่ด้านบนของต้นไม้และประเมินปัญหาย่อยจากใบไม้ / subtrees กลับขึ้นไปที่ราก
  • ตาราง - คุณสามารถนึกถึงการเขียนโปรแกรมแบบไดนามิกเป็นอัลกอริทึม "การเติมตาราง" (แม้ว่าโดยปกติจะมีหลายมิติมิติ 'ตาราง' นี้อาจมีรูปทรงเรขาคณิตที่ไม่ใช่แบบยุคลิดในกรณีที่หายากมาก *) นี่เป็นเหมือนการบันทึกช่วยจำ แต่ใช้งานได้มากกว่าและเกี่ยวข้องกับอีกหนึ่งขั้นตอน: คุณต้องเลือกล่วงหน้าลำดับที่แน่นอนที่คุณจะทำการคำนวณของคุณ สิ่งนี้ไม่ควรหมายความว่าคำสั่งจะต้องคงที่ แต่คุณมีความยืดหยุ่นมากกว่าการบันทึก

    • ตัวอย่างเช่นถ้าคุณกำลังดำเนินการ fibonacci คุณอาจเลือกที่จะคำนวณตัวเลขในลำดับนี้: fib(2), fib(3), fib(4)... แคชทุกค่าเพื่อให้คุณสามารถคำนวณคนต่อไปได้ง่ายขึ้น คุณสามารถคิดว่ามันเป็นการเติมตาราง (การแคชรูปแบบอื่น)
    • โดยส่วนตัวฉันไม่ได้ยินคำว่า 'การจัดระเบียบ' มากนัก แต่มันเป็นคำที่เหมาะสมมาก บางคนคิดว่า "การเขียนโปรแกรมแบบไดนามิก" นี้
    • ก่อนที่จะรันอัลกอริธึมโปรแกรมเมอร์จะพิจารณาทรีทั้งหมดแล้วเขียนอัลกอริทึมเพื่อประเมินปัญหาย่อยตามลำดับเฉพาะไปยังรูทโดยทั่วไปจะเติมในตาราง
    • เชิงอรรถ *: บางครั้ง 'ตาราง' ไม่ใช่ตารางสี่เหลี่ยมที่มีการเชื่อมต่อแบบกริดเหมือนกัน แต่อาจมีโครงสร้างที่ซับซ้อนมากขึ้นเช่นต้นไม้หรือโครงสร้างเฉพาะสำหรับโดเมนปัญหา (เช่นเมืองที่อยู่ในระยะทางบินบนแผนที่) หรือแม้แต่แผนภาพโครงสร้างบังตาที่เป็นช่องซึ่งในขณะที่กริดเหมือนไม่มี โครงสร้างการเชื่อมต่อขึ้น - ลง - ซ้าย - ขวา ฯลฯ ตัวอย่างเช่น user3290797 เชื่อมโยงตัวอย่างการเขียนโปรแกรมแบบไดนามิกในการค้นหาชุดอิสระสูงสุดในต้นไม้ซึ่งสอดคล้องกับการเติมช่องว่างในต้นไม้

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

[ก่อนหน้านี้คำตอบนี้สร้างข้อความเกี่ยวกับคำศัพท์จากบนลงล่างและล่างขึ้นบน มีวิธีการหลักสองอย่างที่ชัดเจนที่เรียกว่าการบันทึกและการจัดตารางซึ่งอาจมีความเกี่ยวข้องกับคำเหล่านั้น (แม้ว่าจะไม่ใช่ทั้งหมด) คำทั่วไปที่คนส่วนใหญ่ใช้ยังคงเป็น "การเขียนโปรแกรมแบบไดนามิก" และบางคนพูดว่า "การบันทึก" เพื่ออ้างถึงประเภทย่อยเฉพาะของ "การเขียนโปรแกรมแบบไดนามิก" คำตอบนี้ปฏิเสธที่จะพูดแบบบนลงล่างและล่างขึ้นบนจนกระทั่งชุมชนสามารถค้นหาเอกสารอ้างอิงที่เหมาะสมในเอกสารทางวิชาการ ในที่สุดมันเป็นสิ่งสำคัญที่จะเข้าใจความแตกต่างมากกว่าคำศัพท์]


ข้อดีและข้อเสีย

ความง่ายในการเขียนโปรแกรม

การบันทึกเป็นเรื่องง่ายมากที่จะเขียนโค้ด (โดยทั่วไปคุณสามารถ * เขียนคำอธิบายประกอบ "memoizer" หรือฟังก์ชั่น wrapper ที่ทำเพื่อคุณโดยอัตโนมัติ) และควรเป็นแนวทางแรกของคุณ ข้อเสียของการจัดระเบียบคือคุณจะต้องมีการสั่งซื้อ

* (นี่เป็นเรื่องง่ายถ้าคุณกำลังเขียนฟังก์ชั่นด้วยตัวเองและ / หรือการเข้ารหัสในภาษาการเขียนโปรแกรมที่ไม่บริสุทธิ์ / ไม่สามารถใช้งานได้ ... ตัวอย่างเช่นถ้ามีคนเขียนfibฟังก์ชั่นprecompiled แล้วก็จำเป็นต้องเรียกซ้ำ คุณไม่สามารถบันทึกฟังก์ชั่นได้อย่างน่าอัศจรรย์โดยไม่ทำให้สายเรียกซ้ำเหล่านั้นเรียกใช้ฟังก์ชันที่บันทึกความทรงจำใหม่ของคุณ

Recursiveness

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

ความกังวลในทางปฏิบัติ

ด้วยการบันทึกความจำหากต้นไม้มีความลึกมาก (เช่นfib(10^6)) คุณจะมีพื้นที่ว่างเหลืออยู่เนื่องจากการคำนวณที่ล่าช้าแต่ละครั้งจะต้องวางบนสแต็กและคุณจะมี 10 ^ 6

optimality

วิธีใดวิธีหนึ่งอาจไม่เหมาะกับเวลาหากคำสั่งที่คุณเกิดขึ้น (หรือพยายาม) ปัญหาย่อยการเยี่ยมชมนั้นไม่เหมาะสมโดยเฉพาะอย่างยิ่งหากมีวิธีการคำนวณมากกว่าหนึ่งวิธี (โดยปกติการแคชจะแก้ไขปัญหานี้ได้ ไม่ใช่ในบางกรณีที่แปลกใหม่) โดยทั่วไปการบันทึกจะเพิ่มความซับซ้อนของเวลาลงในความซับซ้อนของพื้นที่ของคุณ (เช่นด้วยการจัดระเบียบคุณมีอิสระมากขึ้นที่จะทิ้งการคำนวณเช่นการใช้การจัดระเบียบด้วยตอแหลช่วยให้คุณใช้พื้นที่ O (1) แต่การบันทึกด้วย Fib ใช้ O (N) พื้นที่สแต็ก)

การเพิ่มประสิทธิภาพขั้นสูง

หากคุณกำลังทำปัญหาที่ซับซ้อนมากเช่นกันคุณอาจไม่มีทางเลือกนอกจากทำตาราง (หรืออย่างน้อยก็มีบทบาทอย่างแข็งขันมากขึ้นในการควบคุมการบันทึกที่คุณต้องการไป) นอกจากนี้หากคุณอยู่ในสถานการณ์ที่การเพิ่มประสิทธิภาพมีความสำคัญอย่างยิ่งและคุณต้องปรับให้เหมาะสมการจัดระเบียบจะช่วยให้คุณทำการปรับให้เหมาะสมซึ่งการบันทึกจะไม่ช่วยให้คุณทำอย่างมีสติ ในความเห็นต่ำต้อยของฉันในวิศวกรรมซอฟต์แวร์ปกติทั้งสองกรณีนี้ไม่เคยเกิดขึ้นดังนั้นฉันจะใช้การบันทึก ("ฟังก์ชั่นที่แคชคำตอบของมัน") เว้นแต่บางสิ่งบางอย่าง (เช่นพื้นที่สแต็ค) ทำให้การจัดระเบียบจำเป็น ... ในทางเทคนิคเพื่อหลีกเลี่ยงการสแต็คระเบิดคุณสามารถ 1) เพิ่มขีด จำกัด ขนาดสแต็กในภาษาที่อนุญาตหรือ 2) กินปัจจัยคงที่ของการทำงานพิเศษเพื่อจำลองสแต็คของคุณเสมือนจริง (ick)


ตัวอย่างที่ซับซ้อนมากขึ้น

ที่นี่เราแสดงตัวอย่างของความสนใจเป็นพิเศษซึ่งไม่เพียง แต่เป็นปัญหา DP ทั่วไป แต่แยกความแตกต่างระหว่างการบันทึกและการจัดระเบียบอย่างน่าสนใจ ตัวอย่างเช่นสูตรหนึ่งอาจง่ายกว่าอีกสูตรหนึ่งหรืออาจมีการปรับให้เหมาะสมซึ่งโดยทั่วไปต้องใช้การจัดระเบียบ:

  • อัลกอริทึมในการคำนวณระยะทางแก้ไข [ 4 ] ที่น่าสนใจเป็นตัวอย่างที่ไม่สำคัญของอัลกอริทึมเติมตารางสองมิติ

3
@ coder000001: ตัวอย่างหลามคุณสามารถ google ค้นหาpython memoization decorator; บางภาษาจะช่วยให้คุณเขียนแมโครหรือรหัสซึ่ง encapsulate รูปแบบการบันทึก รูปแบบการบันทึกช่วยจำไม่มีอะไรมากไปกว่า "แทนที่จะเรียกใช้ฟังก์ชันค้นหาค่าจากแคช (ถ้าไม่มีค่าให้คำนวณและเพิ่มลงในแคชก่อน")
ninjagecko

16
ฉันไม่เห็นใครพูดถึงเรื่องนี้ แต่ฉันคิดว่าข้อดีอีกอย่างของ Top down คือคุณจะสร้างตาราง / แคชแบบค้นหาอย่างกระจัดกระจาย (เช่นคุณกรอกค่าที่คุณต้องการจริง ๆ ) ดังนั้นนี่อาจเป็นข้อดีนอกเหนือจากการเขียนโปรแกรมง่าย กล่าวอีกอย่างคือจากบนลงล่างอาจช่วยให้คุณประหยัดเวลาในการทำงานจริงเนื่องจากคุณไม่ได้คำนวณทุกอย่าง (คุณอาจมีเวลาในการทำงานที่ดีขึ้นอย่างมาก แต่ก็ต้องใช้หน่วยความจำเพิ่มเติมเพื่อรักษาเฟรมสแต็กเพิ่มเติม (อีกครั้งปริมาณการใช้หน่วยความจำ 'อาจ' (อาจเท่านั้น) สองเท่า แต่ asymptotically เหมือนกัน
InformedA

2
ผมรู้สึกว่าจากบนลงล่างแนวทางว่าการแก้ปัญหาแคชปัญาที่ทับซ้อนกันเป็นเทคนิคที่เรียกว่าmemoization เทคนิคขึ้นด้านล่างที่เติมตารางและยังหลีกเลี่ยง recomputing ปัญาที่ทับซ้อนกันจะเรียกว่าการจัดระเบียบ เทคนิคเหล่านี้สามารถใช้เมื่อใช้การเขียนโปรแกรมแบบไดนามิกซึ่งหมายถึงการแก้ปัญหาย่อยเพื่อแก้ปัญหาที่ใหญ่กว่า ดูเหมือนจะขัดแย้งกับคำตอบนี้โดยที่คำตอบนี้ใช้การเขียนโปรแกรมแบบไดนามิกแทนการจัดระเบียบในหลาย ๆ ใครถูกต้อง?
Sammaron

1
@ Samaron: อืมคุณทำจุดดี ฉันน่าจะตรวจสอบแหล่งข้อมูลของฉันบน Wikipedia ซึ่งหาไม่เจอ เมื่อตรวจสอบ cstheory.stackexchange เล็กน้อยตอนนี้ฉันเห็นด้วย "bottom-up" จะหมายถึงด้านล่างเป็นที่รู้จักกันล่วงหน้า (การจัดระเบียบ) และ "จากบนลงล่าง" คือคุณถือว่าวิธีการแก้ปัญหาย่อย / subtrees ในขณะที่ฉันพบคำที่คลุมเครือและฉันตีความวลีในมุมมองแบบคู่ ("จากล่างขึ้นบน" คุณถือว่าเป็นคำตอบของปัญหาย่อยและจดจำ "จากบนลงล่าง" คุณรู้ว่ามีปัญหาเรื่องใดและคุณสามารถจัดระเบียบ) ฉันจะพยายามแก้ไขปัญหานี้ในการแก้ไข
ninjagecko

1
@mgiuffrida: บางครั้งพื้นที่สแต็กถือว่าแตกต่างกันไปขึ้นอยู่กับภาษาการเขียนโปรแกรม ยกตัวอย่างเช่นในหลามพยายามที่จะดำเนินการโกหก recursive memoized fib(513)จะล้มเหลวสำหรับการพูด คำศัพท์ที่มากเกินไปที่ฉันรู้สึกได้รับในทางที่นี่ 1) คุณสามารถทิ้งปัญหาย่อยที่คุณไม่ต้องการได้อีกต่อไป 2) คุณสามารถหลีกเลี่ยงการคำนวณปัญหาย่อยที่คุณไม่ต้องการได้ 3) 1 และ 2 อาจยากกว่าการเขียนโค้ดโดยไม่มีโครงสร้างข้อมูลที่ชัดเจนในการจัดเก็บปัญหาย่อยหรือถ้ายากขึ้นหากโฟลว์การควบคุมต้องสานระหว่างการเรียกใช้ฟังก์ชัน (คุณอาจต้องการสถานะหรือการดำเนินการต่อ)
ninjagecko

76

จากบนลงล่างและล่างขึ้นบน DP เป็นสองวิธีที่แตกต่างกันในการแก้ปัญหาเดียวกัน พิจารณาโซลูชันการเขียนโปรแกรมที่จดบันทึก (จากบนลงล่าง) vs ไดนามิก (จากล่างขึ้นบน) เพื่อคำนวณหมายเลขฟีโบนัชชี

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

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


1
อาตอนนี้ฉันเห็นความหมายของ "จากบนลงล่าง" และ "จากล่างขึ้นบน"; อันที่จริงแล้วมันหมายถึง memoization vs DP และคิดผมเป็นคนหนึ่งที่แก้ไขคำถามพูดถึง DP ในชื่อ ...
ninjagecko

อะไรคือ runtime ของ fibre memoized v v / s recursive ปกติ?
Siddhartha

exponential (2 ^ n) สำหรับ coz ปกติมันเป็นต้นไม้เรียกซ้ำฉันคิดว่า
Siddhartha

1
ใช่มันเป็นเส้นตรง! ฉันดึงต้นไม้เรียกซ้ำออกมาและเห็นว่าสายใดที่สามารถหลีกเลี่ยงได้และตระหนักว่าการเรียกใช้ memo_fib (n - 2) จะถูกหลีกเลี่ยงทั้งหมดหลังจากการเรียกครั้งแรกและดังนั้นสาขาที่ถูกต้องทั้งหมดของต้นไม้การเรียกซ้ำจะถูกตัดออกและ จะลดลงเป็นเส้นตรง
Siddhartha

1
เนื่องจาก DP เกี่ยวข้องกับการสร้างตารางผลลัพธ์โดยที่แต่ละผลลัพธ์ถูกคำนวณอย่างมากที่สุดวิธีหนึ่งที่ง่ายในการแสดงภาพไทม์ของอัลกอริทึม DP คือการดูว่าตารางมีขนาดใหญ่เพียงใด ในกรณีนี้มันมีขนาด n (หนึ่งผลลัพธ์ต่อค่าอินพุต) ดังนั้น O (n) ในกรณีอื่น ๆ มันอาจเป็นเมทริกซ์ n ^ 2 ส่งผลให้ O (n ^ 2) ฯลฯ
Johnson Wong

22

คุณลักษณะที่สำคัญของการเขียนโปรแกรมแบบไดนามิกคือการปรากฏตัวของปัญาที่ทับซ้อนกัน นั่นคือปัญหาที่คุณกำลังพยายามแก้ไขสามารถแบ่งออกเป็นปัญหาย่อยและปัญหาย่อย ๆ เหล่านั้นแบ่งปันปัญหาย่อย มันเหมือนกับ "หารและพิชิต" แต่คุณจบลงด้วยการทำสิ่งเดียวกันหลาย ๆ ครั้ง ตัวอย่างที่ฉันใช้มาตั้งแต่ปี 2003 เมื่อสอนหรืออธิบายเรื่องเหล่านี้: คุณสามารถคำนวณตัวเลขฟีโบนักชีแบบวนซ้ำได้

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

fib(50)ใช้ภาษาที่คุณชื่นชอบและลองใช้มันสำหรับ จะใช้เวลานานมาก เวลามากพอ ๆ กับfib(50)ตัวของมันเอง! อย่างไรก็ตามมีงานที่ไม่จำเป็นเกิดขึ้นมากมาย fib(50)จะโทรfib(49)และfib(48)แต่ทั้งคู่จะสิ้นสุดการโทรfib(47)แม้ว่าค่าจะเหมือนกัน ในความเป็นจริงfib(47)จะมีการคำนวณสามครั้ง: โดยการโทรโดยตรงจากfib(49)โดยการโทรโดยตรงจากfib(48)และโดยการโทรโดยตรงจากอีกfib(48)คนหนึ่งที่ถูกวางไข่โดยการคำนวณของfib(49)... ดังนั้นคุณจะเห็นว่าเรามีปัญหาย่อยทับซ้อนกัน .

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

โดยปกติคุณสามารถเขียนโปรแกรมซ้ำที่เทียบเท่ากันซึ่งทำงานจากล่างขึ้นบนโดยไม่ต้องเรียกซ้ำ ในกรณีนี้นี่จะเป็นวิธีที่เป็นธรรมชาติมากขึ้น: วนจาก 1 ถึง 50 คำนวณตัวเลขทั้งหมดของฟีโบนักชีในขณะที่คุณไป

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

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

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

ขณะนี้มีปัญหาที่วิธีการจากบนลงล่างเป็นทางออกเดียวที่เป็นไปได้เนื่องจากพื้นที่ปัญหาใหญ่มากจนไม่สามารถแก้ปัญหาย่อยทั้งหมดได้ อย่างไรก็ตาม "แคช" ยังคงใช้งานได้ในเวลาที่เหมาะสมเพราะอินพุตของคุณต้องการเพียงส่วนย่อยของปัญหาย่อยที่ต้องแก้ไข --- แต่มันก็ยากเกินกว่าที่จะนิยามอย่างชัดเจนซึ่งปัญหาย่อยที่คุณต้องแก้ไขและด้วยเหตุนี้ แก้ปัญหา ในอีกทางหนึ่งมีสถานการณ์เมื่อคุณรู้ว่าคุณจะต้องแก้ปัญหาย่อยทั้งหมด ในกรณีนี้ไปและใช้จากล่างขึ้นบน

โดยส่วนตัวแล้วฉันจะใช้การเพิ่มประสิทธิภาพย่อหน้าด้านล่างเพื่อแก้ไขปัญหาการเพิ่มประสิทธิภาพ Word wrap (ค้นหาอัลกอริทึมการแบ่งบรรทัด Knuth-Plass อย่างน้อย TeX ใช้มันและซอฟต์แวร์บางอย่างของ Adobe Systems ใช้วิธีการที่คล้ายกัน) ผมจะใช้ด้านล่างขึ้นสำหรับจานแปลงฟูเรีย


สวัสดี!!! ฉันต้องการตรวจสอบว่าข้อเสนอต่อไปนี้ถูกต้องหรือไม่ - สำหรับอัลกอริธึมการเขียนโปรแกรมแบบไดนามิกการคำนวณค่าทั้งหมดที่มีจากล่างขึ้นบนจะเร็วกว่าแบบไม่แสดงอาการโดยใช้การเรียกซ้ำและบันทึกช่วยจำ - เวลาของอัลกอริทึมแบบไดนามิกมักจะΟ (Ρ) โดยที่Ρคือจำนวนของปัญหาย่อย - แต่ละปัญหาใน NP สามารถแก้ไขได้ในเวลาเอ็กซ์โปเนนเชียล
Mary Star

ฉันจะพูดอะไรเกี่ยวกับข้อเสนอข้างต้น คุณมีความคิดหรือไม่? @osa
Mary Star

@evinda (1) ผิดเสมอ มันอาจเป็นแบบเดียวกันหรือช้าลงแบบ asymptotically (เมื่อคุณไม่ต้องการปัญหาย่อยทั้งหมดการเรียกซ้ำอาจเร็วกว่า) (2) ถูกต้องก็ต่อเมื่อคุณสามารถแก้ปัญหาย่อยทั้งหมดใน O (1) (3) ถูกต้อง แต่ละปัญหาใน NP สามารถแก้ไขได้ในเวลาพหุนามบนเครื่อง nondeterministic (เช่นคอมพิวเตอร์ควอนตัมที่สามารถทำหลายสิ่งพร้อมกัน: มีเค้กและกินพร้อมกันและติดตามผลลัพธ์ทั้งสอง) ดังนั้นในแง่หนึ่งปัญหาแต่ละข้อใน NP สามารถแก้ไขได้ในเวลาเอ็กซ์โพเนนเชียลในคอมพิวเตอร์ปกติ หมายเหตุ: ทุกอย่างใน P ก็เป็น NP เช่นกัน เช่นการเพิ่มจำนวนเต็มสองจำนวน
osa

19

ให้นำชุดฟีโบนักชีเป็นตัวอย่าง

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

อีกวิธีที่จะนำมัน

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

ในกรณีที่มีหมายเลขฟีโบนักชีห้าหมายเลขแรก

Bottom(first) number :1
Top (fifth) number: 5 

ตอนนี้ลองมาดูตัวอย่างอัลกอริทึม Fibonacci ซีรีย์ซ้ำ

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

ตอนนี้ถ้าเรารันโปรแกรมนี้ด้วยคำสั่งดังต่อไปนี้

rcursive(5);

หากเราพิจารณาอัลกอริธึมอย่างใกล้ชิดเพื่อสร้างหมายเลขที่ห้าต้องใช้หมายเลขที่ 3 และ 4 ดังนั้นการสอบถามซ้ำของฉันจึงเริ่มต้นจากด้านบน (5) จากนั้นไปจนถึงตัวเลขด้านล่าง / ล่าง วิธีนี้เป็นวิธีการจากบนลงล่างจริงๆ

เพื่อหลีกเลี่ยงการคำนวณเดียวกันหลายครั้งเราใช้เทคนิคการเขียนโปรแกรมแบบไดนามิก เราเก็บค่าที่คำนวณไว้ก่อนหน้านี้และนำมาใช้ซ้ำ เทคนิคนี้เรียกว่าการจำ มีมากกว่าการเขียนโปรแกรมแบบไดนามิกอื่น ๆ แล้วการบันทึกความจำซึ่งไม่จำเป็นเพื่อหารือเกี่ยวกับปัญหาปัจจุบัน

จากบนลงล่าง

ให้เขียนอัลกอริทึมดั้งเดิมของเราใหม่และเพิ่มเทคนิคที่จำได้

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

และเราดำเนินการวิธีการดังต่อไปนี้

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

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

จากล่างขึ้นบน

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

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

ทีนี้ถ้าเราดูอัลกอริธึมนี้จริง ๆ แล้วเริ่มจากค่าต่ำลง ถ้าฉันต้องการหมายเลขฟีโบนักชีอันดับที่ 5 ฉันกำลังคำนวณหาอันดับที่ 1 จริง ๆ จากนั้นก็จะเป็นอันดับสองและอันดับสามจนถึงอันดับที่ 5 เทคนิคนี้เรียกว่าเทคนิคจากล่างขึ้นบน

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


เราสามารถพูดได้ว่าวิธีการจากล่างขึ้นบนมักถูกนำไปใช้ในทางที่ไม่เกิดซ้ำได้หรือไม่?
Lewis Chan

ไม่คุณสามารถแปลงลอจิกลูปเป็นการเรียกซ้ำ
Ashvin Sharma

3

การเขียนโปรแกรมแบบไดนามิกมักจะเรียกว่าการจำ!

1. การสาธิตเป็นเทคนิคจากบนลงล่าง (เริ่มแก้ปัญหาที่กำหนดโดยการแยกมันออก) และการเขียนโปรแกรมแบบไดนามิกเป็นเทคนิคจากล่างขึ้นบน (เริ่มแก้ไขจากปัญหาย่อยเล็กน้อยไปจนถึงปัญหาที่กำหนด)

2.DP ค้นหาวิธีแก้ปัญหาโดยเริ่มจากตัวพิมพ์ใหญ่และทำงานจนสุด DP ช่วยแก้ปัญหาย่อยทั้งหมดได้เพราะจะทำจากล่างขึ้นบน

แตกต่างจากการบันทึกซึ่งแก้ปัญหาย่อยที่จำเป็นเท่านั้น

  1. DP มีศักยภาพในการแปลงพลังงานเดรัจฉานแรงแบบเอกซ์โปเนนเชียลไทม์เป็นโพรซีเดอร์เวลาแบบพหุนาม

  2. DP อาจมีประสิทธิภาพมากกว่านี้มากเพราะเป็นการทำซ้ำ

ในทางตรงกันข้ามการบันทึกจะต้องชำระค่าโสหุ้ย (มักสำคัญ) เนื่องจากการเรียกซ้ำ

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


4
นั่นไม่เป็นความจริงการบันทึกช่วยจำใช้แคชซึ่งจะช่วยให้คุณประหยัดเวลาที่ซับซ้อนเช่นเดียวกับ DP
InformedA

3

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


1

ต่อไปนี้เป็นวิธีการแก้ปัญหา DP ที่ใช้สำหรับแก้ไขระยะทางซึ่งเป็นบนลงล่าง ฉันหวังว่ามันจะช่วยในการทำความเข้าใจกับโลกของการเขียนโปรแกรมแบบไดนามิก:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

คุณสามารถคิดถึงการใช้งานแบบวนซ้ำที่บ้านของคุณ มันค่อนข้างดีและท้าทายถ้าคุณไม่เคยแก้ไขอะไรแบบนี้มาก่อน


1

จากบนลงล่าง : การติดตามค่าที่คำนวณจนถึงตอนนี้และส่งคืนผลลัพธ์เมื่อตรงตามเงื่อนไขพื้นฐาน

int n = 5;
fibTopDown(1, 1, 2, n);

private int fibTopDown(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibTopDown(j, i + j, count + 1, n);
}

จากล่างขึ้นบน : ผลลัพธ์ปัจจุบันขึ้นอยู่กับผลลัพธ์ของปัญหาย่อย

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.