ความแตกต่างระหว่างอัลกอริธึมการหารและการพิชิตและอัลกอริธึมการเขียนโปรแกรมแบบไดนามิกคืออะไร? คำสองคำแตกต่างกันอย่างไร ฉันไม่เข้าใจความแตกต่างระหว่างพวกเขา
โปรดยกตัวอย่างง่ายๆเพื่ออธิบายความแตกต่างระหว่างสองสิ่งนี้กับสิ่งที่ดูเหมือนว่าคล้ายคลึงกัน
ความแตกต่างระหว่างอัลกอริธึมการหารและการพิชิตและอัลกอริธึมการเขียนโปรแกรมแบบไดนามิกคืออะไร? คำสองคำแตกต่างกันอย่างไร ฉันไม่เข้าใจความแตกต่างระหว่างพวกเขา
โปรดยกตัวอย่างง่ายๆเพื่ออธิบายความแตกต่างระหว่างสองสิ่งนี้กับสิ่งที่ดูเหมือนว่าคล้ายคลึงกัน
คำตอบ:
หารและพิชิต
แบ่งและพิชิตงานโดยการแบ่งปัญหาออกเป็นปัญหาย่อยพิชิตแต่ละปัญหาย่อยซ้ำและรวมโซลูชันเหล่านี้
การเขียนโปรแกรมแบบไดนามิก
การเขียนโปรแกรมแบบไดนามิกเป็นเทคนิคสำหรับการแก้ปัญหาปัญหาย่อยทับซ้อนกัน แต่ละปัญหาย่อยจะได้รับการแก้ไขเพียงครั้งเดียวและผลลัพธ์ของปัญหาย่อยแต่ละรายการจะถูกเก็บไว้ในตาราง (โดยทั่วไปจะใช้เป็นอาร์เรย์หรือตารางแฮช) สำหรับการอ้างอิงในอนาคต วิธีแก้ปัญหาย่อยเหล่านี้อาจใช้เพื่อขอรับโซลูชันดั้งเดิมและเทคนิคการจัดเก็บโซลูชันย่อยปัญหาเรียกว่าการบันทึก
คุณอาจคิดถึง DP = recursion + re-use
ตัวอย่างคลาสสิกเพื่อทำความเข้าใจความแตกต่างคือการเห็นทั้งสองวิธีเหล่านี้ไปสู่การได้รับหมายเลข fibonacci ที่ n ตรวจสอบวัสดุนี้จาก MIT
วิธีหารและพิชิต
แนวทางการเขียนโปรแกรมแบบไดนามิก
ความแตกต่างอื่น ๆ ระหว่างการหารและการพิชิตและการเขียนโปรแกรมแบบไดนามิกอาจเป็น:
หารและพิชิต:
การเขียนโปรแกรมแบบไดนามิก:
บางครั้งเมื่อโปรแกรมซ้ำคุณเรียกใช้ฟังก์ชันด้วยพารามิเตอร์เดียวกันหลาย ๆ ครั้งซึ่งไม่จำเป็น
ตัวอย่างฟีโบนักชีที่มีชื่อเสียง:
index: 1,2,3,4,5,6...
Fibonacci number: 1,1,2,3,5,8...
function F(n) {
if (n < 3)
return 1
else
return F(n-1) + F(n-2)
}
มารัน F (5):
F(5) = F(4) + F(3)
= {F(3)+F(2)} + {F(2)+F(1)}
= {[F(2)+F(1)]+1} + {1+1}
= 1+1+1+1+1
ดังนั้นเราจึงเรียกว่า: 1 ครั้ง F (4) 2 ครั้ง F (3) 3 ครั้ง F (2) 2 ครั้ง F (1)
วิธีการเขียนโปรแกรมแบบไดนามิก: หากคุณเรียกใช้ฟังก์ชันที่มีพารามิเตอร์เดียวกันมากกว่าหนึ่งครั้งให้บันทึกผลลัพธ์ลงในตัวแปรเพื่อเข้าถึงโดยตรงในครั้งถัดไป วิธีการทำซ้ำ:
if (n==1 || n==2)
return 1
else
f1=1, f2=1
for i=3 to n
f = f1 + f2
f1 = f2
f2 = f
เรียก F (5) อีกครั้ง:
fibo1 = 1
fibo2 = 1
fibo3 = (fibo1 + fibo2) = 1 + 1 = 2
fibo4 = (fibo2 + fibo3) = 1 + 2 = 3
fibo5 = (fibo3 + fibo4) = 2 + 3 = 5
อย่างที่คุณเห็นเมื่อใดก็ตามที่คุณต้องการการโทรหลายครั้งคุณเพียงแค่เข้าถึงตัวแปรที่เกี่ยวข้องเพื่อรับค่าแทนการคำนวณใหม่
โดยวิธีการเขียนโปรแกรมแบบไดนามิกไม่ได้หมายความว่าจะแปลงรหัสซ้ำเป็นรหัสซ้ำ นอกจากนี้คุณยังสามารถบันทึกผลลัพธ์ย่อยลงในตัวแปรได้หากคุณต้องการรหัสแบบเรียกซ้ำ ในกรณีนี้เทคนิคเรียกว่าการบันทึก สำหรับตัวอย่างของเราดูเหมือนว่านี้:
// declare and initialize a dictionary
var dict = new Dictionary<int,int>();
for i=1 to n
dict[i] = -1
function F(n) {
if (n < 3)
return 1
else
{
if (dict[n] == -1)
dict[n] = F(n-1) + F(n-2)
return dict[n]
}
}
ดังนั้นความสัมพันธ์กับการหารและการพิชิตคืออัลกอริทึม D&D พึ่งพาการสอบถามซ้ำ และบางรุ่นก็มี "การเรียกใช้ฟังก์ชันหลายครั้งพร้อมปัญหาพารามิเตอร์เดียวกัน" ค้นหา "การคูณเมทริกซ์โซ่" และ "การเรียงลำดับทั่วไปที่ยาวที่สุด" สำหรับตัวอย่างที่ต้องการ DP เพื่อปรับปรุง T (n) ของ D&D algo
อย่างที่ฉันเห็นตอนนี้ฉันสามารถพูดได้ว่าการเขียนโปรแกรมแบบไดนามิกเป็นส่วนขยายของการแบ่งและพิชิตกระบวนทัศน์การเขียนโปรแกรมแบบไดนามิกเป็นส่วนขยายของแบ่งและพิชิตกระบวนทัศน์
ฉันจะไม่ถือว่าพวกเขาเป็นสิ่งที่แตกต่างอย่างสิ้นเชิง เพราะพวกเขาทั้งสองทำงานโดยแบ่งย่อยปัญหาซ้ำ ๆ กันเป็นสองปัญหาย่อยซ้ำ ๆ กันหรือประเภทเดียวกันหรือที่เกี่ยวข้องจนกระทั่งสิ่งเหล่านี้กลายเป็นเรื่องง่ายพอที่จะแก้ไขได้โดยตรง การแก้ไขปัญหาย่อยจะถูกรวมเข้าด้วยกันเพื่อให้การแก้ไขปัญหาเดิม
เหตุใดเราจึงยังคงมีชื่อกระบวนทัศน์แตกต่างกันไปและทำไมฉันจึงเรียกส่วนขยายแบบไดนามิก มันเป็นเพราะวิธีการเขียนโปรแกรมแบบไดนามิกอาจนำมาใช้ในการแก้ไขปัญหาเฉพาะในกรณีที่ปัญหายังมีข้อ จำกัด บางอย่างหรือข้อกำหนดเบื้องต้น และหลังจากนั้นการเขียนโปรแกรมแบบไดนามิกที่ขยายวิธีการแบ่งและพิชิตด้วยเทคนิคการบันทึกความจำหรือการจัดตาราง
ไปทีละขั้นตอน ...
เนื่องจากเราเพิ่งค้นพบว่ามีคุณลักษณะที่สำคัญสองประการที่ต้องมีการแบ่งและเอาชนะปัญหาเพื่อให้การเขียนโปรแกรมแบบไดนามิกสามารถใช้งานได้
โครงสร้างย่อยที่เหมาะสม - ทางออกที่ดีที่สุดสามารถสร้างได้จากทางออกที่ดีที่สุดของปัญหาย่อย
ปัญหาย่อยที่ทับซ้อนกัน - ปัญหาสามารถแบ่งย่อยเป็นปัญหาย่อยซึ่งถูกนำมาใช้ซ้ำหลายครั้งหรืออัลกอริทึมแบบเรียกซ้ำสำหรับปัญหาแก้ปัญหาย่อยเดียวกันซ้ำแล้วซ้ำอีกแทนที่จะสร้างปัญหาย่อยใหม่เสมอ
เมื่อตรงตามเงื่อนไขทั้งสองนี้เราสามารถพูดได้ว่าปัญหาการแบ่งแยกและการพิชิตนี้อาจแก้ไขได้โดยใช้วิธีการโปรแกรมแบบไดนามิก
วิธีการเขียนโปรแกรมแบบไดนามิกขยายวิธีการแบ่งและพิชิตด้วยสองเทคนิค (การบันทึกและการจัดตาราง ) ที่ทั้งสองมีวัตถุประสงค์ในการจัดเก็บและการใช้โซลูชั่นย่อยปัญหาที่อาจปรับปรุงประสิทธิภาพอย่างมาก ตัวอย่างเช่นการใช้ฟังก์ชัน Fibonacci แบบเรียกซ้ำแบบเรียกซ้ำมีความซับซ้อนของเวลาO(2^n)
ซึ่งโซลูชัน DP ทำเช่นเดียวกันกับเท่านั้นO(n)
เวลาครั้ง
การบันทึก (การเติมแคชจากบนลงล่าง)หมายถึงเทคนิคการแคชและการนำผลลัพธ์ที่คำนวณมาก่อนหน้านี้มาใช้ใหม่ fib
ฟังก์ชั่นบันทึกความจำจึงมีลักษณะเช่นนี้:
memFib(n) {
if (mem[n] is undefined)
if (n < 2) result = n
else result = memFib(n-2) + memFib(n-1)
mem[n] = result
return mem[n]
}
ตาราง (การเติมแคชจากล่างขึ้นบน)คล้ายกัน แต่เน้นที่การเติมรายการของแคช การคำนวณค่าในแคชทำได้ง่ายที่สุดซ้ำ ๆ เวอร์ชันการจัดระเบียบของfib
จะมีลักษณะเช่นนี้:
tabFib(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับ memoization และการเปรียบเทียบการจัดระเบียบที่นี่
แนวคิดหลักที่คุณควรเข้าใจในที่นี้ก็คือเนื่องจากปัญหาการแบ่งแยกและการพิชิตของเราซ้อนทับปัญหาย่อยการแคชของการแก้ปัญหาย่อยจึงเป็นไปได้และทำให้การบันทึก / การเรียงลำดับขึ้นไปบนฉาก
เนื่องจากตอนนี้เราคุ้นเคยกับข้อกำหนดเบื้องต้น DP และวิธีการของเราจึงพร้อมที่จะนำทุกอย่างที่กล่าวไว้ข้างต้นเป็นภาพเดียว
หากคุณต้องการดูตัวอย่างโค้ดคุณสามารถดูคำอธิบายโดยละเอียดเพิ่มเติมได้ที่นี่ซึ่งคุณจะพบตัวอย่างอัลกอริทึมสองตัวอย่าง: การค้นหาแบบไบนารีและระยะทางแก้ไขขั้นต่ำ (ระยะทาง Levenshtein) ที่แสดงถึงความแตกต่างระหว่าง DP และ DC
ฉันถือว่าคุณได้อ่าน Wikipedia และแหล่งข้อมูลทางวิชาการอื่น ๆ เกี่ยวกับเรื่องนี้แล้วดังนั้นฉันจะไม่รีไซเคิลข้อมูลใด ๆ ฉันต้องเตือนด้วยว่าฉันไม่ได้เป็นผู้เชี่ยวชาญด้านวิทยาการคอมพิวเตอร์ แต่อย่างใด แต่ฉันจะแบ่งปันสองเซ็นต์ของฉันในการทำความเข้าใจหัวข้อเหล่านี้ ...
แบ่งปัญหาออกเป็นปัญหาย่อยโดยสิ้นเชิง อัลกอริทึมแบบเรียกซ้ำสำหรับลำดับฟีโบนักชีเป็นตัวอย่างของการเขียนโปรแกรมแบบไดนามิกเพราะมันจะแก้หา Fib (n) โดยการแก้ปัญหาครั้งแรกสำหรับ Fib (n-1) เพื่อที่จะแก้ปัญหาเดิมก็แก้ที่แตกต่างกันปัญหา
อัลกอริทึมเหล่านี้มักจะแก้ปัญหาชิ้นส่วนที่คล้ายกันแล้วนำมารวมกันในตอนท้าย การควบรวมกิจการเป็นตัวอย่างคลาสสิกของการแบ่งแยกและพิชิต ความแตกต่างที่สำคัญระหว่างตัวอย่างนี้และตัวอย่างฟีโบนักชีคือในการรวมกลุ่มการแบ่งสามารถตามอำเภอใจ (ในทางทฤษฎี) โดยพลการและไม่ว่าคุณจะเชือดมันขึ้นมาอย่างไรคุณก็ยังผสานและเรียงลำดับ ต้องทำงานในปริมาณเดียวกันเพื่อรวมอาร์เรย์เข้าด้วยกันไม่ว่าคุณจะแบ่งมันอย่างไร การแก้ปัญหาเรื่อง fib (52) ต้องใช้ขั้นตอนมากกว่าขั้นตอนแก้ปัญหาเรื่อง fib (2)
ฉันคิดว่าDivide & Conquer
เป็นวิธีแบบเรียกซ้ำและDynamic Programming
เติมตาราง
ตัวอย่างเช่นMerge Sort
เป็นDivide & Conquer
อัลกอริธึมในแต่ละขั้นตอนคุณแบ่งอาร์เรย์ออกเป็นสองส่วนเรียกซ้ำสองส่วนซ้ำMerge Sort
แล้วรวมเข้าด้วยกัน
Knapsack
เป็นDynamic Programming
อัลกอริทึมในขณะที่คุณกำลังกรอกตารางแสดงโซลูชันที่ดีที่สุดสำหรับปัญหาย่อยของเป้โดยรวม แต่ละรายการในตารางสอดคล้องกับค่าสูงสุดที่คุณสามารถพกพาได้ในถุงน้ำหนักที่ได้รับรายการ 1-j
การหารและการพิชิตเกี่ยวข้องกับสามขั้นตอนในการเรียกซ้ำแต่ละระดับ:
การเขียนโปรแกรมแบบไดนามิกที่เกี่ยวข้องดังต่อไปนี้สี่ขั้นตอน:
1. สมบัติโครงสร้างของการแก้ปัญหาที่ดีที่สุด
2. กำหนดค่าของโซลูชันที่ดีที่สุดซ้ำ ๆ
3. คำนวณมูลค่าโซลูชันที่เหมาะสมที่สุด
4. สร้างทางออกที่ดีที่สุดจากข้อมูลคำนวณ
เพื่อความเข้าใจที่ง่ายขึ้นให้ดูที่การแบ่งและพิชิตเป็นวิธีเดรัจฉานแรงและการเพิ่มประสิทธิภาพเป็นโปรแกรมแบบไดนามิก
NBแบ่งและเอาชนะอัลกอริธึมที่มีปัญหาย่อยที่ทับซ้อนกันสามารถปรับให้เหมาะสมกับ dp เท่านั้น
fact(5) = 5* fact(4) = 5 * (4 * fact(3))= 5 * 4 * (3 *fact(2))= 5 * 4 * 3 * 2 * (fact(1))
อย่างที่เราเห็นข้างต้นไม่มีการทำซ้ำความจริง (x) ดังนั้นแฟคทอเรียลจึงไม่มีปัญหาการซ้อนทับกัน
fib(5) = fib(4) + fib(3) = (fib(3)+fib(2)) + (fib(2)+fib(1))
อย่างที่เราเห็นข้างต้น Fib (4) และ fib (3) ทั้งคู่ใช้ fib (2) ในทำนองเดียวกัน fib (x) จำนวนมากจะถูกทำซ้ำ นั่นเป็นสาเหตุที่ฟีโบนัชชีซ้อนทับปัญหาย่อย