การเขียนโปรแกรมแบบไดนามิกคืออะไร? [ปิด]


276

การเขียนโปรแกรมแบบไดนามิกคืออะไร?

มันแตกต่างจากการเรียกซ้ำบันทึก ฯลฯ อย่างไร

ฉันได้อ่านบทความวิกิพีเดียแล้ว แต่ฉันยังไม่เข้าใจจริงๆ


1
นี่คือบทเรียนหนึ่งโดย Michael A. Trick จาก CMU ที่ฉันพบว่ามีประโยชน์อย่างยิ่ง: mat.gsia.cmu.edu/classes/dynamic/dynamic.htmlแน่นอนว่านอกเหนือจากทรัพยากรทั้งหมดที่ผู้อื่นได้แนะนำ (ทรัพยากรอื่น ๆ ทั้งหมดโดยเฉพาะ CLR และ Kleinberg, Tardos นั้นดีมาก!) เหตุผลที่ฉันชอบบทช่วยสอนนี้คือเพราะมันแนะนำแนวคิดขั้นสูงอย่างค่อยเป็นค่อยไป มันเป็นวัสดุที่ค่อนข้างเก่า แต่ก็เป็นส่วนเสริมที่ดีในรายการแหล่งข้อมูลที่นำเสนอที่นี่ ตรวจสอบหน้าของ Steven Skiena และการบรรยายเกี่ยวกับ Dynamic Programming: cs.sunysb.edu/~algorith/video-lectures http:
Edmon

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

3
@ dimo414 "การเขียนโปรแกรม" ที่นี่มีความเกี่ยวข้องมากขึ้นกับ "การเขียนโปรแกรมเชิงเส้น" ซึ่งอยู่ภายใต้ชั้นเรียนของวิธีการเพิ่มประสิทธิภาพทางคณิตศาสตร์ ดูบทความการเพิ่มประสิทธิภาพทางคณิตศาสตร์สำหรับรายการวิธีการเขียนโปรแกรมทางคณิตศาสตร์อื่น ๆ
syockit

1
@ dimo414 "การเขียนโปรแกรม" ในบริบทนี้อ้างถึงวิธีการแบบตารางไม่ใช่การเขียนรหัสคอมพิวเตอร์ - Coreman
2618142

ปัญหาการลดต้นทุนตั๋วรถโดยสารที่อธิบายไว้ในcs.stackexchange.com/questions/59797/…นั้นได้รับการแก้ไขที่ดีที่สุดในการเขียนโปรแกรมแบบไดนามิก
trueadjustr

คำตอบ:


210

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

ตัวอย่างที่ดีคือการแก้ไขลำดับฟีโบนักชีสำหรับ n = 1,000,002

นี่จะเป็นกระบวนการที่ยาวมาก แต่ถ้าฉันให้ผลลัพธ์ให้คุณสำหรับ n = 1,000,000 และ n = 1,000,001 ทันใดนั้นปัญหาก็เริ่มจัดการได้ง่ายขึ้น

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

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

หนังสือ Cormen Algorithms มีบทที่ยอดเยี่ยมเกี่ยวกับการเขียนโปรแกรมแบบไดนามิก และฟรีบน Google หนังสือ! ตรวจสอบที่นี่


50
คุณไม่ได้อธิบายการบันทึกความจำใช่มั้ย
dreadwail

31
ฉันจะบอกว่าการบันทึกเป็นรูปแบบของการเขียนโปรแกรมแบบไดนามิกเมื่อฟังก์ชั่นบันทึกความทรงจำ / วิธีการที่เป็นซ้ำ
Daniel Huckstep

6
คำตอบที่ดีจะเพิ่มเพียงการกล่าวถึงโครงสร้างย่อยที่ดีที่สุดเท่านั้น (เช่นทุกเซตย่อยของเส้นทางใด ๆ ตามเส้นทางที่สั้นที่สุดจาก A ถึง B เป็นเส้นทางที่สั้นที่สุดระหว่าง 2 ปลายทางโดยสมมติว่าระยะทางที่สังเกตความไม่เท่าเทียมกันของสามเหลี่ยม)
Shea

5
ฉันจะไม่พูดว่า "ง่ายขึ้น" แต่เร็วกว่า ความเข้าใจผิดที่พบบ่อยคือ dp แก้ปัญหาที่อัลกอริธึมไร้เดียงสาทำไม่ได้และนั่นไม่ใช่กรณี ไม่ใช่เรื่องของการใช้งาน แต่มีประสิทธิภาพ
andandandand

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

175

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

ลองมาตัวอย่างง่ายๆของตัวเลข Fibonacci: หาที่ n THตัวเลข Fibonacci ที่กำหนดโดย

F n = F n-1 + F n-2และ F 0 = 0, F 1 = 1

recursion

วิธีที่ชัดเจนในการทำสิ่งนี้คือการเรียกซ้ำ:

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1

    return fibonacci(n - 1) + fibonacci(n - 2)

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

  • จากบนลงล่าง - การบันทึก

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

cache = {}

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    if n in cache:
        return cache[n]

    cache[n] = fibonacci(n - 1) + fibonacci(n - 2)

    return cache[n]
  • จากล่างขึ้นบน

วิธีที่ดีกว่าในการทำเช่นนี้คือกำจัดการเรียกซ้ำทั้งหมดโดยการประเมินผลลัพธ์ในลำดับที่ถูกต้อง:

cache = {}

def fibonacci(n):
    cache[0] = 0
    cache[1] = 1

    for i in range(2, n + 1):
        cache[i] = cache[i - 1] +  cache[i - 2]

    return cache[n]

เราสามารถใช้พื้นที่คงที่และเก็บเฉพาะผลลัพธ์บางส่วนที่จำเป็นระหว่างทาง:

def fibonacci(n):
  fi_minus_2 = 0
  fi_minus_1 = 1

  for i in range(2, n + 1):
      fi = fi_minus_1 + fi_minus_2
      fi_minus_1, fi_minus_2 = fi, fi_minus_1

  return fi
  • ใช้การเขียนโปรแกรมแบบไดนามิกได้อย่างไร

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

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

ฉันได้รวบรวมปัญหาเพื่อช่วยให้เข้าใจตรรกะ: https://github.com/tristanguigue/dynamic-programing


3
นี่เป็นคำตอบที่ยอดเยี่ยมและการรวบรวมปัญหาใน Github ก็มีประโยชน์เช่นกัน ขอบคุณ!
p4sh4

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

ขอบคุณสำหรับคำอธิบาย มีเงื่อนไขที่ขาดหายไปจากล่างขึ้นบน: if n in cacheเช่นเดียวกับตัวอย่างจากบนลงล่างหรือว่าฉันขาดอะไรไป
DavidC

ฉันเข้าใจอย่างถูกต้องหรือไม่ว่าการวนซ้ำที่ค่าที่คำนวณในแต่ละการวนซ้ำถูกใช้ในการวนซ้ำครั้งถัดไปเป็นตัวอย่างของการเขียนโปรแกรมแบบไดนามิกหรือไม่?
Alexey

คุณสามารถให้การอ้างอิงสำหรับการตีความที่คุณให้รวมถึงกรณีพิเศษจากบนลงล่างและล่างขึ้นบนได้ไหม?
Alexey

37

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

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

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

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

ใส่อย่างชัดเจนมาก ฉันหวังว่าผู้สอนอัลกอริทึมสามารถอธิบายสิ่งนี้ได้ดี
Kelly S. French

21

เป็นการเพิ่มประสิทธิภาพของอัลกอริทึมของคุณที่ลดเวลาทำงาน

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

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

นี่คือตัวอย่างของปัญหาที่เหมาะสำหรับการเขียนโปรแกรมแบบไดนามิกจากผู้ตัดสินออนไลน์ของ UVA: Edit Steps Ladder

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

ลองดูปัญหานั้นกันดีกว่าถ้าเรากำหนดฟังก์ชั่นต้นทุนเพื่อบอกเราว่าสตริงสองอันนั้นไกลแค่ไหนเรามีสองตัวพิจารณาการเปลี่ยนแปลงตามธรรมชาติสามประเภท:

การแทนที่ - เปลี่ยนอักขระเดียวจากรูปแบบ "s" เป็นอักขระอื่นในข้อความ "t" เช่นการเปลี่ยน "shot" เป็น "spot"

การแทรก - แทรกอักขระเดี่ยวในรูปแบบ "s" เพื่อช่วยจับคู่ข้อความ "t" เช่นการเปลี่ยน "ที่ผ่านมา" เป็น "agog"

การลบ - ลบอักขระเดียวจากรูปแบบ "s" เพื่อช่วยให้ตรงกับข้อความ "t" เช่นการเปลี่ยน "ชั่วโมง" เป็น "ของเรา"

เมื่อเราตั้งค่าการดำเนินการแต่ละรายการให้มีค่าใช้จ่ายเพียงขั้นตอนเดียวเราจะกำหนดระยะทางแก้ไขระหว่างสองสตริง แล้วเราจะคำนวณมันอย่างไร

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

#define MATCH 0 /* enumerated type symbol for match */
#define INSERT 1 /* enumerated type symbol for insert */
#define DELETE 2 /* enumerated type symbol for delete */


int string_compare(char *s, char *t, int i, int j)

{

    int k; /* counter */
    int opt[3]; /* cost of the three options */
    int lowest_cost; /* lowest cost */
    if (i == 0) return(j * indel(’ ’));
    if (j == 0) return(i * indel(’ ’));
    opt[MATCH] = string_compare(s,t,i-1,j-1) +
      match(s[i],t[j]);
    opt[INSERT] = string_compare(s,t,i,j-1) +
      indel(t[j]);
    opt[DELETE] = string_compare(s,t,i-1,j) +
      indel(s[i]);
    lowest_cost = opt[MATCH];
    for (k=INSERT; k<=DELETE; k++)
    if (opt[k] < lowest_cost) lowest_cost = opt[k];
    return( lowest_cost );

}

อัลกอริทึมนี้ถูกต้อง แต่ก็ช้าเช่นกัน

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

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

แล้วเราจะทำให้อัลกอริทึมเป็นจริงได้อย่างไร? การสังเกตที่สำคัญคือการเรียกแบบเรียกซ้ำเหล่านี้ส่วนใหญ่เป็นการคำนวณสิ่งที่เคยคำนวณมาก่อนแล้ว เราจะรู้ได้อย่างไร มีเพียง s | เท่านั้น | · | t | การโทรซ้ำแบบซ้ำที่เป็นไปได้เนื่องจากมีเพียงจำนวนคู่ที่แตกต่างกัน (i, j) เพื่อใช้เป็นพารามิเตอร์ของการโทรซ้ำ

โดยการเก็บค่าสำหรับแต่ละคู่ (i, j) เหล่านี้ในตารางเราสามารถหลีกเลี่ยงการคำนวณใหม่และค้นหาค่าได้ตามต้องการ

ตารางเป็นเมทริกซ์สองมิติ m โดยที่แต่ละ | s | · | t | เซลล์มีค่าใช้จ่ายของโซลูชันที่ดีที่สุดของปัญหาย่อยนี้รวมถึงตัวชี้หลักที่อธิบายวิธีที่เราไปถึงสถานที่นี้:

typedef struct {
int cost; /* cost of reaching this cell */
int parent; /* parent cell */
} cell;

cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */

เวอร์ชันการเขียนโปรแกรมแบบไดนามิกมีความแตกต่างสามประการจากเวอร์ชันแบบเรียกซ้ำ

ก่อนอื่นจะได้รับค่ากลางโดยใช้การค้นหาตารางแทนการเรียกซ้ำ

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

** สาม ** ประการที่สามมันเป็นเครื่องมือที่ใช้cell()ฟังก์ชั่นเป้าหมายทั่วไปมากขึ้นแทนที่จะส่งกลับ m [| s |] [| t |] .cost สิ่งนี้จะทำให้เราสามารถใช้รูทีนนี้กับปัญหาที่กว้างขึ้น

ที่นี่การวิเคราะห์โดยเฉพาะอย่างยิ่งของสิ่งที่ใช้ในการรวบรวมผลลัพธ์บางส่วนที่เหมาะสมที่สุดคือสิ่งที่ทำให้การแก้ปัญหาเป็น "แบบไดนามิก"

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


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

คุณต้องรวบรวมผลลัพธ์ทีละขั้นตอนเพื่อสร้างอัลกอริทึม "ไดนามิก" การเขียนโปรแกรมแบบไดนามิกเกิดจากการทำงานของ Bellman ใน OR หากคุณพูดว่า "การข้ามคำจำนวนใด ๆ เป็นการเขียนโปรแกรมแบบไดนามิก" คุณกำลังลดคำศัพท์ลงเนื่องจากการค้นหาคำใด ๆ en.wikipedia.org/wiki/Dynamic_programming
andandandand

12

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

โดยละเอียดยิ่งขึ้นเพื่อแก้ปัญหาเส้นทางที่สั้นที่สุดคุณจะ:

  • ค้นหาระยะทางจากโหนดเริ่มต้นไปยังทุกโหนดที่สัมผัส (พูดจาก A ถึง B และ C)
  • ค้นหาระยะทางจากโหนดเหล่านั้นไปยังโหนดที่สัมผัสได้ (จาก B ถึง D และ E และจาก C ถึง E และ F)
  • ตอนนี้เรารู้เส้นทางที่สั้นที่สุดจาก A ถึง E: มันคือผลรวมสั้นที่สุดของ Axe และ xE สำหรับโหนด x ที่เราได้เข้าชม
  • ทำซ้ำกระบวนการนี้จนกว่าเราจะไปถึงโหนดปลายทางสุดท้าย

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

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


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

@TerryLi มันอาจจะทำให้ "รู้สึก" แต่มันไม่ง่ายที่จะเข้าใจ ปัญหาจำนวนฟีโบนักชีเป็นที่รู้จักและเข้าใจง่าย
Ajay

5

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

คำนิยาม

Dynamic programming (DP) เป็นเทคนิคการออกแบบอัลกอริธึมทั่วไปสำหรับการแก้ปัญหาที่มีปัญหาย่อยทับซ้อนกัน เทคนิคนี้ถูกคิดค้นโดยนักคณิตศาสตร์ชาวอเมริกัน“ Richard Bellman” ในปี 1950

ความคิดหลัก

แนวคิดหลักคือการบันทึกคำตอบของการซ้อนทับปัญหาย่อยที่เล็กลงเพื่อหลีกเลี่ยงการคำนวณซ้ำ

คุณสมบัติการเขียนโปรแกรมแบบไดนามิก

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

4

ฉันยังใหม่กับ Dynamic Programming มาก (อัลกอริทึมที่มีประสิทธิภาพสำหรับปัญหาบางประเภท)

ในคำที่ง่ายที่สุดแค่คิดว่าการเขียนโปรแกรมแบบไดนามิกเป็นวิธีแบบเรียกซ้ำโดยใช้ความรู้ก่อนหน้า

ความรู้ก่อนหน้าคือสิ่งที่สำคัญที่สุดที่นี่ติดตามการแก้ปัญหาย่อยที่คุณมีอยู่แล้ว

พิจารณาสิ่งนี้เป็นตัวอย่างพื้นฐานที่สุดสำหรับ dp จาก Wikipedia

การหาลำดับฟีโบนักชี

function fib(n)   // naive implementation
    if n <=1 return n
    return fib(n − 1) + fib(n − 2)

ให้แบ่งการเรียกใช้ฟังก์ชันด้วย say n = 5

fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

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

ตอนนี้ให้ลองโดยการเก็บค่าที่เราพบแล้วในโครงสร้างข้อมูลบอกว่าแผนที่

var m := map(0 → 0, 1 → 1)
function fib(n)
    if key n is not in map m 
        m[n] := fib(n − 1) + fib(n − 2)
    return m[n]

ที่นี่เรากำลังบันทึกวิธีแก้ปัญหาย่อยในแผนที่หากเรายังไม่มี เทคนิคการบันทึกค่าที่เราได้คำนวณไปแล้วนี้เรียกว่าการบันทึก

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


ตรงไปตรงมาจากวิกิพีเดีย Downvoted !!
solidak

3

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

เจ็ดขั้นตอนในการพัฒนาอัลกอริทึมการเขียนโปรแกรมแบบไดนามิกมีดังนี้:

  1. สร้างคุณสมบัติแบบเรียกซ้ำที่ให้วิธีแก้ปัญหากับอินสแตนซ์ของปัญหา
  2. พัฒนาอัลกอริทึมแบบเรียกซ้ำตามคุณสมบัติแบบเรียกซ้ำ
  3. ดูว่าอินสแตนซ์เดียวกันของปัญหากำลังถูกแก้ไขอีกครั้งในการโทรซ้ำ
  4. พัฒนาอัลกอริทึมแบบเรียกซ้ำ
  5. ดูรูปแบบในการจัดเก็บข้อมูลในหน่วยความจำ
  6. แปลงอัลกอริธึมวนซ้ำแบบจำได้ให้เป็นอัลกอริทึมซ้ำ
  7. เพิ่มประสิทธิภาพอัลกอริทึมแบบวนซ้ำโดยใช้ที่เก็บข้อมูลตามที่ต้องการ (การเพิ่มประสิทธิภาพการจัดเก็บ)

เป็น6. Convert the memoized recursive algorithm into iterative algorithmขั้นตอนบังคับ? นี่หมายความว่าแบบฟอร์มสุดท้ายไม่ใช่แบบเรียกซ้ำ?
trueadjustr

ไม่บังคับไม่ใช่มันเป็นตัวเลือก
Adnan Qureshi

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

1

ในระยะสั้นความแตกต่างระหว่างการเรียกซ้ำการเรียกซ้ำและการเขียนโปรแกรมแบบไดนามิก

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

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

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

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


-2

นี่เป็นตัวอย่างรหัสหลามที่เรียบง่ายของRecursive, Top-down, Bottom-upวิธีการสำหรับ Fibonacci ชุด:

แบบเรียกซ้ำ: O (2 n )

def fib_recursive(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fib_recursive(n-1) + fib_recursive(n-2)


print(fib_recursive(40))

จากบนลงล่าง: O (n) มีประสิทธิภาพสำหรับอินพุตที่ใหญ่ขึ้น

def fib_memoize_or_top_down(n, mem):
    if mem[n] is not 0:
        return mem[n]
    else:
        mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
        return mem[n]


n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))

จากล่างขึ้นบน: O (n) เพื่อความเรียบง่ายและขนาดอินพุตที่เล็ก

def fib_bottom_up(n):
    mem = [0] * (n+1)
    mem[1] = 1
    mem[2] = 1
    if n == 1 or n == 2:
        return 1

    for i in range(3, n+1):
        mem[i] = mem[i-1] + mem[i-2]

    return mem[n]


print(fib_bottom_up(40))

กรณีแรกไม่ได้มีเวลาทำงานของ n ^ 2 ซับซ้อนเวลาเป็น O (2 ^ n): stackoverflow.com/questions/360748/...
แซม

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