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


33

ขออภัยล่วงหน้าหากคำถามนี้ฟังดูเป็นใบ้ ...

เท่าที่ฉันรู้การสร้างอัลกอริทึมโดยใช้การเขียนโปรแกรมแบบไดนามิกทำงานในลักษณะนี้:

  1. แสดงปัญหาว่าเป็นความสัมพันธ์ที่เกิดซ้ำ;
  2. ใช้ความสัมพันธ์ที่เกิดซ้ำทั้งผ่านการบันทึกและผ่านวิธีการจากล่างขึ้นบน

เท่าที่ฉันรู้ฉันได้กล่าวทุกอย่างเกี่ยวกับการเขียนโปรแกรมแบบไดนามิก ฉันหมายถึง: การเขียนโปรแกรมแบบไดนามิกไม่ได้ให้เครื่องมือ / กฎ / วิธีการ / ทฤษฎีบทสำหรับการแสดงความสัมพันธ์ที่เกิดขึ้นอีกหรือเปลี่ยนเป็นรหัส

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


11
Historical factoid (ความคิดเห็นนี้จะไม่ช่วยคุณ แต่ Bellman เป็นผู้นำที่ดีถ้าคุณต้องการให้ทฤษฎีหนักกับการเขียนโปรแกรมแบบไดนามิก): เมื่อ Bellman เกิดขึ้นกับสิ่งที่เรียกว่าการเขียนโปรแกรมแบบไดนามิกเขาเรียกว่าความคิด "การเขียนโปรแกรมแบบไดนามิก "เพราะการทำงานตามทฤษฎีอย่างหมดจดจะไม่บินกับนายจ้างของเขาในเวลานั้นเขาจึงต้องการอะไรบางอย่างมากขึ้น buzzwordy ที่ไม่สามารถใช้ในลักษณะที่ดูถูก
G. Bach

3
เท่าที่ฉันรู้ว่ามันเป็นสองจุดที่คุณพูดถึง มันจะกลายเป็นพิเศษเมื่อมันหลีกเลี่ยงการระเบิดแบบเอกซ์โปเนนเชียลเนื่องจากมีปัญหาย่อยทับซ้อนกัน นั่นคือทั้งหมดที่ อาจารย์ของฉันชอบ "กระบวนทัศน์อัลกอริทึม" มากกว่า "วิธีคลุมเครือ"
Hendrik Jan

"การเขียนโปรแกรมแบบไดนามิก" น่าจะเป็น buzzword เป็นหลัก ไม่ได้หมายความว่ามันไม่มีประโยชน์แน่นอน
253751

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

@hobbs: แน่นอน แต่ความสามารถในการหาวิธีการเริ่มต้นของการเสียเวลานั้น)
j_random_hacker

คำตอบ:


27

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

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

การเขียนโปรแกรมแบบไดนามิกช่วยให้คุณคิดเกี่ยวกับเวลาทำงานของอัลกอริทึมของคุณ เวลาทำงานนั้นขึ้นอยู่กับตัวเลขสองตัวคือจำนวนของปัญหาย่อยที่คุณต้องแก้ไขและเวลาที่ใช้ในการแก้ปัญหาย่อยแต่ละรายการ นี่เป็นวิธีที่สะดวกในการคิดเกี่ยวกับปัญหาการออกแบบอัลกอริทึม เมื่อคุณมีความสัมพันธ์ที่เกิดซ้ำกับผู้สมัครคุณสามารถดูได้และรู้สึกได้อย่างรวดเร็วว่าเวลาทำงานอาจเป็นเท่าไหร่ (ตัวอย่างเช่นคุณมักจะสามารถบอกได้อย่างรวดเร็วว่ามีปัญหาย่อยจำนวนเท่าใดซึ่งเป็นขอบเขตล่างของ เวลาที่ใช้งานหากมีปัญหาย่อยมากมายที่คุณต้องแก้ปัญหาการเกิดซ้ำอาจจะไม่ใช่วิธีที่ดี) นอกจากนี้ยังช่วยให้คุณแยกแยะปัญหาย่อยสลายของผู้สมัคร ตัวอย่างเช่นถ้าเรามีสตริงกำหนด subproblem โดยคำนำหน้า S [ 1 .. ฉัน]หรือต่อท้าย S [ J . n ]หรือซับสตริง S [ i . . j ]อาจมีเหตุผล (จำนวนของปัญหาย่อยคือชื่อพหุนามใน n ) แต่การกำหนดปัญหาย่อยตามลำดับของ Sนั้นไม่น่าจะเป็นวิธีที่ดี (จำนวนของปัญหาย่อยเป็นเลขชี้กำลังใน n ) ซึ่งจะช่วยให้คุณตัด "ช่องว่างการค้นหา" ของการเกิดซ้ำที่เป็นไปได้S[1..n]S[1..i]S[j..n]S[i..j]nSn

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

  • ถ้าอินพุตเป็นจำนวนเต็มบวก วิธีการหนึ่งในการกำหนด subproblem คือการแทนที่nด้วยจำนวนเต็มเล็กกว่าn (st 0 n n )nnn0nn

  • หากอินพุตเป็นสตริง วิธีการบางอย่างของผู้สมัครในการกำหนดปัญหาย่อย ได้แก่ : แทนที่S [ 1 .. n ]ด้วยคำนำหน้าS [ 1 .. i ] ; แทนที่S [ 1 .. n ]ด้วยคำต่อท้ายS [ j . . n ] ; แทนที่S [ 1 .. n ]ด้วยสตริงย่อยS [ i . . j ]S[1..n]S[1..n]S[1..i]S[1..n]S[j..n]S[1..n]S[i..j]. (ที่นี่ปัญหาย่อยถูกกำหนดโดยตัวเลือกของ .)i,j

  • หากอินพุตเป็นรายการให้ทำเช่นเดียวกับที่คุณทำกับสตริง

  • หากอินพุตเป็นtree หนึ่งทางเลือกในการกำหนด subproblem คือการแทนที่Tด้วยทรีย่อยของT (เช่นเลือกโหนดxและแทนที่Tด้วยทรีย่อยที่ root ที่xซึ่ง subproblem ถูกกำหนดโดยตัวเลือกของx )TTTxTxx

  • หากอินพุตเป็นคู่ให้ดูชนิดของxและประเภทyซ้ำ ๆเพื่อระบุวิธีการเลือกปัญหาย่อยสำหรับแต่ละรายการ ในคำอื่น ๆ วิธีที่ผู้สมัครคนหนึ่งที่จะกำหนด subproblem คือการแทนที่( x , Y )โดย( x ' , Y ' )ที่x 'เป็น subproblem สำหรับxและy ที่'เป็น subproblem สำหรับปี (คุณสามารถพิจารณาปัญหาย่อยของแบบฟอร์ม( x , y(x,y)xy(x,y)(x,y)xxyyหรือ ( x , y ) .)(x,y)(x,y)

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

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


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

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

ขอบคุณมาก! โดยการฝึกฉันได้รับความคุ้นเคยมากขึ้นกับ "กระบวนการกึ่งคอนกรีต" บางส่วนที่คุณกำลังอธิบาย
เฮ้เฮ้เฮ้

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

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

9

ความเข้าใจเกี่ยวกับการเขียนโปรแกรมแบบไดนามิกของคุณถูกต้อง ( afaik ) และคำถามของคุณเป็นธรรม

ฉันคิดว่าพื้นที่การออกแบบเพิ่มเติมที่เราได้รับจากการเกิดขึ้นอีกครั้งที่เราเรียกว่า "การเขียนโปรแกรมแบบไดนามิก" สามารถมองเห็นได้ดีที่สุดเมื่อเปรียบเทียบกับ schemata อื่น ๆ ของวิธีการเรียกซ้ำ

สมมติว่าอินพุตของเราคืออาร์เรย์เพื่อเน้นแนวคิดA[1..n]

  1. วิธีการอุปนัย

    นี่คือแนวคิดที่จะทำให้ปัญหาของคุณมีขนาดเล็กลงแก้ไขรุ่นที่เล็กลงและหาวิธีแก้ปัญหาสำหรับต้นฉบับ แผนผัง,

    f(A)=g(f(A[1..nc]),A)

    ด้วยฟังก์ชั่น / อัลกอริทึมที่แปลการแก้ปัญหาg

    ตัวอย่าง: การ หาซูเปอร์สตาร์ในเวลาเชิงเส้น

  2. หาร & พิชิต

    แบ่งพาร์ติชันอินพุตเป็นชิ้นส่วนเล็ก ๆ หลาย ๆ อันแก้ปัญหาสำหรับแต่ละอันและรวมกัน แผนผัง (สำหรับสองส่วน)

    )f(A)=g(f(A[1..c]),f(A[c+1..n]),A)

    ตัวอย่าง:ผสาน - / Quicksort ระยะทางคู่สั้นที่สั้นที่สุดในระนาบ

  3. การเขียนโปรแกรมแบบไดนามิก

    พิจารณาทุกวิธีในการแบ่งปัญหาออกเป็นปัญหาเล็ก ๆ และเลือกวิธีที่ดีที่สุด แผนผัง (สำหรับสองส่วน)

    }f(A)=best{g(f(A[1..c]),f(A[c+1..n]))|1cn1}

    ตัวอย่าง:แก้ไขระยะทางปัญหาการเปลี่ยนแปลง

    หมายเหตุด้านที่สำคัญ: การเขียนโปรแกรมแบบไดนามิกไม่ได้ดุร้าย ! แอพพลิเคชั่นที่ในทุกขั้นตอนลดพื้นที่การค้นหาลงอย่างมากbest

ในความรู้สึกคุณรู้น้อยลงเรื่อย ๆ จากบนลงล่างและต้องทำการตัดสินใจมากขึ้นเรื่อย ๆ แบบไดนามิก

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


"Pruned Dynamic Programming" (เมื่อนำมาใช้) พิสูจน์ให้เห็นว่าการพยายามทำทุกอย่างที่เป็นไปได้นั้นไม่จำเป็นสำหรับความถูกต้อง
Ben Voigt

@BenVoigt Of course. I remained deliberately vague about what "all ways to partition" means; you want to rule out as many as possible, of course! (However, even if you try all ways of partitioning you don't get brute force since you only ever investigate combinations of optimal solutions to subproblems, whereas brute-force would investigate all combinations of all solutions.)
Raphael


5

Dynamic Programming allows you to trade memory for computation time. Consider the classic example, Fibonacci.

Fibonacci is defined by the recurrence Fib(n)=Fib(n1)+Fib(n2). If you solve using this recursion, you end up doing O(2n) calls to Fib(), since the recursion tree is a binary tree with height n.

Instead, you want to calculate Fib(2), then use this to find Fib(3), use that to find Fib(4), etc. This only takes O(n) time.

DP also provides us with basic techniques for translating a recurrence relation into a bottom-up solution, but these are relatively straightforward (and generally involve using an m dimensional matrix, or a frontier of such a matrix, where m is the number of parameters in the recurrence relation). These are well explained in any text about DP.


1
You talk only about the memoization part, which misses the point of the question.
Raphael

1
"Dynamic Programming allows you to trade memory for computation time" is not something I heard when doing undergrad, and it's a great way to look at this subject. This is an intuitive answer with a succinct example.
trueshot

@trueshot: Except that sometimes dynamic programming (and particularly, "Pruned Dynamic Programming") is able to reduce both time and space requirements.
Ben Voigt

@Ben I didn't say it was a one-to-one trade. You can prune a recurrence tree as well. I posit that I did answer the question, which was, "What does DP get us?" It gets us faster algorithms by trading space for time. I agree that the accepted answer is more thorough, but this is valid as well.
Kittsil

2

Here is another slightly different way of phrasing what dynamic programming gives you. Dynamic programming collapses an exponential number of candidate solutions into a polynomial number of equivalence classes, such that the candidate solutions in each class are indistinguishable in some sense.

Let me take as an example the problem of finding the number of increasing subsequences of length k in an array A of lenght n. It is useful to partition the set of all subsequences into equivalence classes such that two subsequences belong to the same class if and only if they have the same length and end in the same index. All of the 2n possible subsequences belong to exactly one of the O(n2) equivalence classes. This partitioning preserves enough information so that we can define a recurrence relation for the sizes of the classes. If f(i,) gives the number of subsequences which end in index i and have length , then we have:

f(i,)=j<i such thatA[j]<A[i]f(j,1)
f(i,1)=1 for all i=1n

This recurrence solves the problem in time O(n2k).

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