dynamic-programming
การทำความเข้าใจเกี่ยวกับ Dynamic Programming (DP)
Dynamic Programming (DP) เป็นเทคนิคที่ใช้ในการแก้ปัญหาที่ซับซ้อน โดยการแบ่งปัญหาหลักให้กลายเป็นปัญหาย่อย ๆ ซึ่งสามารถแก้ไขได้อย่างมีประสิทธิภาพ โดยการเก็บผลลัพธ์ของปัญหาย่อยที่ได้คำนวณแล้ว เพื่อหลีกเลี่ยงการคำนวณซ้ำซ้อนในภายหลัง (การทำ Memoization หรือ Tabulation) จึงทำให้สามารถลดเวลาในการคำนวณจาก O(2^n) เป็น O(n) หรือ O(n^2) ขึ้นอยู่กับปัญหาที่กำลังแก้ไข
1. หลักการของ Dynamic Programming
การใช้ Dynamic Programming จะมีสองวิธีหลักคือ Memoization และ Tabulation ซึ่งทั้งสองวิธีมีแนวคิดพื้นฐานในการแก้ปัญหาดังนี้:
-
Memoization (Top-Down Approach):
- เริ่มต้นจากการแก้ไขปัญหาหลักแล้วค่อย ๆ แบ่งออกเป็นปัญหาย่อย ๆ
- หากผลลัพธ์ของปัญหาย่อยใดถูกคำนวณแล้ว ก็จะเก็บไว้ในหน่วยความจำ (ในรูปแบบของ Memoization Table) เพื่อให้ไม่ต้องคำนวณซ้ำอีก
- ใช้ Recursion ในการทำงาน
-
Tabulation (Bottom-Up Approach):
- เริ่มจากการแก้ปัญหาย่อย ๆ ก่อน แล้วค่อย ๆ รวมผลลัพธ์เหล่านั้นเพื่อให้ได้ผลลัพธ์ของปัญหาหลัก
- ไม่ใช้การเรียกฟังก์ชันซ้ำ (ไม่มี Recursion)
- ใช้ Iteration ในการคำนวณ
2. ลักษณะของปัญหาที่เหมาะกับ Dynamic Programming
การใช้ Dynamic Programming จะเหมาะสมกับปัญหาที่มีคุณสมบัติสองประการหลัก คือ:
-
Overlapping Subproblems (ปัญหาย่อยที่ซ้ำกัน):
- ปัญหาหลักสามารถแบ่งออกเป็นปัญหาย่อยหลาย ๆ ครั้ง ซึ่งบางปัญหาย่อยจะถูกคำนวณซ้ำหลายครั้ง
- Dynamic Programming จะช่วยเก็บผลลัพธ์ของปัญหาย่อยเหล่านี้เพื่อนำกลับมาใช้ใหม่แทนที่จะคำนวณใหม่ทุกครั้ง
-
Optimal Substructure (โครงสร้างที่ดีที่สุดของปัญหาย่อย):
- ผลลัพธ์ของปัญหาหลักสามารถคำนวณได้จากผลลัพธ์ของปัญหาย่อยที่ดีที่สุด
- Dynamic Programming จะคำนวณผลลัพธ์ของปัญหาย่อยและใช้ผลลัพธ์เหล่านั้นในการคำนวณผลลัพธ์ของปัญหาหลัก
3. การใช้ Dynamic Programming: ตัวอย่างปัญหาคลาสสิก
1. Fibonacci Sequence (ลำดับฟีโบนัชชี)
ปัญหาคลาสสิกที่สุดใน Dynamic Programming คือการหาค่าลำดับฟีโบนัชชี:
- F(0) = 0
- F(1) = 1
- F(n) = F(n-1) + F(n-2)
โดยปกติแล้ว เราสามารถใช้ Recursion ในการหาค่าฟีโบนัชชีได้ แต่การคำนวณจะซ้ำซ้อนมากและช้า โดยเฉพาะเมื่อ n มีค่ามาก
การใช้ Memoization (Top-Down)
def fibonacci_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]
print(fibonacci_memo(10)) # ผลลัพธ์: 55
การใช้ Tabulation (Bottom-Up)
def fibonacci_tab(n):
if n <= 1:
return n
fib = [0] * (n + 1)
fib[1] = 1
for i in range(2, n + 1):
fib[i] = fib[i - 1] + fib[i - 2]
return fib[n]
print(fibonacci_tab(10)) # ผลลัพธ์: 55
2. Knapsack Problem (ปัญหายัดกระเป๋า)
ปัญหา Knapsack เป็นปัญหาคลาสสิกใน Dynamic Programming ที่เราต้องเลือกไอเท็มบางอย่างจากชุดของไอเท็มที่มีขนาดและมูลค่าต่างกัน เพื่อให้รวมมูลค่าสูงสุดโดยไม่เกินน้ำหนักที่กำหนด
ปัญหาคลาสสิก:
- เรามีชุดของไอเท็มที่แต่ละชิ้นมีน้ำหนักและมูลค่า
- เรามี capacity ของกระเป๋าที่จำกัด
- เราต้องเลือกไอเท็มที่สามารถบรรจุในกระเป๋าเพื่อให้ได้มูลค่าสูงสุด
การใช้ Tabulation (Bottom-Up)
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
if weights[i - 1] <= w:
dp[i][w] = max(dp[i - 1][w], values[i - 1] + dp[i - 1][w - weights[i - 1]])
else:
dp[i][w] = dp[i - 1][w]
return dp[n][capacity]
weights = [1, 2, 3, 8, 7, 4]
values = [20, 5, 10, 40, 15, 25]
capacity = 10
print(knapsack(weights, values, capacity)) # ผลลัพธ์: 60
4. ข้อดีและข้อเสียของ Dynamic Programming
ข้อดี:
- ประสิทธิภาพ: Dynamic Programming ช่วยลดการคำนวณซ้ำซ้อน โดยการเก็บผลลัพธ์ของปัญหาย่อยที่ได้คำนวณแล้วใน Memoization Table หรือ Tabulation Table
- แก้ปัญหาที่ซับซ้อน: สามารถใช้ในการแก้ปัญหาที่ซับซ้อนได้ โดยการแบ่งปัญหาย่อย ๆ ที่สามารถคำนวณได้แยกกัน
ข้อเสีย:
- ใช้หน่วยความจำมาก: การใช้ Memoization หรือ Tabulation อาจจะใช้พื้นที่หน่วยความจำมาก โดยเฉพาะเมื่อมีจำนวนปัญหาย่อยมาก
- ต้องมีโครงสร้างที่เหมาะสม: การใช้ Dynamic Programming ต้องมั่นใจว่าปัญหามี Optimal Substructure และ Overlapping Subproblems เพื่อให้การใช้เทคนิคนี้มีประสิทธิภาพ
5. สรุป
Dynamic Programming เป็นเทคนิคการแก้ปัญหาที่ใช้ในการคำนวณปัญหาที่มีลักษณะของ Overlapping Subproblems และ Optimal Substructure โดยการแบ่งปัญหาหลักให้กลายเป็นปัญหาย่อย ๆ และเก็บผลลัพธ์ของปัญหาย่อยเพื่อหลีกเลี่ยงการคำนวณซ้ำ การใช้ Memoization หรือ Tabulation จะช่วยให้การคำนวณปัญหาที่ซับซ้อนสามารถทำได้เร็วขึ้นและมีประสิทธิภาพมากขึ้น