DP จากล่างขึ้นบนด้วยตาราง
แปลงคำตอบจากบนลงล่างเป็นตาราง DP แบบวนซ้ำ และลดพื้นที่จาก O(n) เป็น O(1) เมื่อจำเป็นต้องเก็บเพียงค่าบางรายการล่าสุด
DP จากล่างขึ้นบนด้วยตาราง เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
DP จากล่างขึ้นบน: แนวทางการเติมตาราง
DP จากล่างขึ้นบน (การเติมตาราง) จะเติมคำตอบของปัญหาย่อยลงในตาราง โดยเริ่มจากปัญหาย่อยที่เล็กที่สุดแล้วสร้างคำตอบให้ใหญ่ขึ้นไปเรื่อย ๆ แทนที่จะเรียกซ้ำลงไปแล้วเก็บแคชขณะย้อนกลับ คุณจะคำนวณแบบวนซ้ำจากพื้นฐานขึ้นไป ตารางมักเป็นอาร์เรย์แบบ 1 มิติหรือ 2 มิติ โดยแต่ละช่องจะคำนวณจากช่องที่เติมค่าไว้ก่อนแล้ว วิธีนี้กำจัดการเรียกซ้ำทั้งหมด ไม่ต้องมีสแตกการเรียก ไม่ติดขีดจำกัดการเรียกซ้ำ และใช้ข้อมูลในแคชได้มีประสิทธิภาพกว่า
# Converting top-down to bottom-up:
# Top-down: start at fib(n), recurse to smaller, cache
# Bottom-up: start at fib(0), fill table to fib(n)
# Key question for bottom-up:
# 'In what order do I fill the table so that when I compute dp[i],
# all values dp[i] depends on are already filled?'
# For Fibonacci: dp[i] needs dp[i-1] and dp[i-2]
# Fill order: i = 2, 3, 4, ..., n (left to right)
print('Bottom-up: fill small sub-problems first, build to answer')ฟีโบนักชีจากล่างขึ้นบน
ฟีโบนักชีแบบจากล่างขึ้นบนจะเติม dp[0..n] จากซ้ายไปขวา โดย dp[i] = dp[i-1] + dp[i-2] สำหรับ i >= 2 กรณีฐานคือ dp[0] = 0 และ dp[1] = 1 ซึ่งเก็บไว้โดยตรงในอาร์เรย์ ความซับซ้อนด้านเวลาคือ O(n) และความซับซ้อนด้านพื้นที่คือ O(n) สำหรับตารางเต็ม เมื่อเห็นว่า dp[i] พึ่งพาเพียงค่าสองค่าล่าสุด คุณสามารถลดพื้นที่เหลือ O(1) ด้วยตัวแปรสองตัวได้ ซึ่งเป็นขั้นตอนการปรับปรุงการใช้พื้นที่
def fib_bottom_up(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[0] = 0 # base case
dp[1] = 1 # base case
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
print([fib_bottom_up(i) for i in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Space-optimised to O(1):
def fib_optimised(n):
if n <= 1: return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(fib_optimised(50)) # 12586269025การแลกเหรียญจากล่างขึ้นบน
สำหรับ การแลกเหรียญ ตารางจากล่างขึ้นบนคือ dp[0..amount] โดย dp[i] = จำนวนเหรียญขั้นต่ำที่ใช้สร้างจำนวนเงิน i กำหนดค่าเริ่มต้นให้ dp[0] = 0 (จำนวนเงินเป็นศูนย์ใช้เหรียญศูนย์เหรียญ) และ dp[1..amount] = infinity สำหรับจำนวนเงิน i ตั้งแต่ 1 ถึงเป้าหมาย ให้ลองเหรียญแต่ละชนิด หาก i >= coin ให้คำนวณ dp[i] = min(dp[i], 1 + dp[i - coin]) คำตอบคือ dp[amount] หรือ -1 หากค่ายังคงเป็น infinity
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # base case: 0 coins for amount 0
for i in range(1, amount + 1):
for coin in coins:
if i >= coin: # can use this coin
dp[i] = min(dp[i], 1 + dp[i - coin])
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change([1, 5, 6, 9], 11)) # 2: (5+6)
print(coin_change([2], 3)) # -1: impossible
print(coin_change([1, 2, 5], 11)) # 3: 5+5+1
print(coin_change([186, 419, 83, 408], 6249)) # 20ลำดับการเติมค่า: ข้อสังเกตสำคัญ
ลำดับการเติมค่าคือหัวใจของ DP จากล่างขึ้นบน สำหรับสถานะ dp[i] ใด ๆ ต้องคำนวณสถานะทั้งหมดที่สถานะนั้นพึ่งพาให้เสร็จก่อน สำหรับ DP แบบ 1 มิติที่ dp[i] พึ่งพา dp[i-1] และ dp[i-2] ให้เติมค่าจากซ้ายไปขวา สำหรับ DP แบบ 2 มิติที่ dp[i][j] พึ่งพา dp[i-1][j] และ dp[i][j-1] ให้เติมค่าทีละแถว (จากบนลงล่าง และจากซ้ายไปขวา) ควรวาดลูกศรแสดงการพึ่งพาก่อนเขียนโค้ดเสมอ เพื่อยืนยันลำดับการเติมค่า
# Fill order examples:
# 1D: dp[i] = f(dp[i-1], dp[i-2])
# Arrows point LEFT: fill LEFT TO RIGHT
# i: 0 -> 1 -> 2 -> ... -> n
# 2D: dp[i][j] = f(dp[i-1][j], dp[i][j-1])
# Arrows point LEFT and UP: fill TOP-LEFT TO BOTTOM-RIGHT
# Fill row 0 first, then row 1, etc.
# 2D reversed: dp[i][j] = f(dp[i+1][j], dp[i][j+1])
# Arrows point RIGHT and DOWN: fill BOTTOM-RIGHT TO TOP-LEFT
# Used in interval DP and some string problems
print('Draw dependencies first, then determine fill order')LCS จากล่างขึ้นบน: ตารางแบบ 2 มิติ
ตารางจากล่างขึ้นบนของ ลำดับย่อยร่วมที่ยาวที่สุดมีขนาด (m+1) × (n+1) โดย dp[i][j] = LCS ของ s1[:i] และ s2[:j] กรณีฐานคือ dp[0][j] = dp[i][0] = 0 (สตริงว่างมีค่า LCS เท่ากับ 0 เมื่อเทียบกับสิ่งใดก็ตาม) เติมค่าทีละแถว: หาก s1[i-1] == s2[j-1] ให้ dp[i][j] = 1 + dp[i-1][j-1] มิฉะนั้น dp[i][j] = max(dp[i-1][j], dp[i][j-1]) คำตอบคือ dp[m][n]
def lcs_bottom_up(s1, s2):
m, n = len(s1), len(s2)
# (m+1) x (n+1) table, initialised to 0
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]: # characters match
dp[i][j] = 1 + dp[i-1][j-1]
else: # skip one character
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
print(lcs_bottom_up('abcde', 'ace')) # 3
print(lcs_bottom_up('ABCBDAB', 'BDCAB')) # 4: 'BCAB' or 'BDAB'การปรับปรุงการใช้พื้นที่: อาร์เรย์แบบเลื่อน
ตาราง DP แบบ 2 มิติจำนวนมากสามารถลดเหลือ 1 มิติ (หรือ 2 แถว) ได้โดยสังเกตว่า dp[i][j] พึ่งพาเพียงแถวปัจจุบันและแถวก่อนหน้า เก็บอาร์เรย์สองชุดคือ prev และ curr หรืออัปเดตอาร์เรย์ชุดเดียวตามลำดับที่ถูกต้อง สำหรับ LCS dp[i][j] พึ่งพา dp[i-1][j], dp[i][j-1] และ dp[i-1][j-1] ดังนั้นการเก็บไว้เพียงแถวก่อนหน้าก็เพียงพอ
def lcs_space_optimised(s1, s2):
m, n = len(s1), len(s2)
# Keep only one row (previous row state)
prev = [0] * (n + 1)
for i in range(1, m + 1):
curr = [0] * (n + 1)
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
curr[j] = 1 + prev[j-1] # dp[i-1][j-1]
else:
curr[j] = max(prev[j], curr[j-1]) # dp[i-1][j] and dp[i][j-1]
prev = curr
return prev[n]
print(lcs_space_optimised('abcde', 'ace')) # 3
# Space: O(n) instead of O(mn)โจรปล้นบ้านจากล่างขึ้นบน
โจรปล้นบ้านแบบจากล่างขึ้นบนจะเติม dp[0..n-1] โดย dp[i] = ผลประโยชน์สูงสุดจากการขโมยบ้านตั้งแต่ 0 ถึง i dp[0] = nums[0], dp[1] = max(nums[0], nums[1]) และสำหรับ i >= 2: dp[i] = max(dp[i-1], dp[i-2] + nums[i]) เนื่องจาก dp[i] พึ่งพาเพียงค่าสองค่าล่าสุด จึงปรับปรุงการใช้พื้นที่ให้เหลือ O(1) ได้ทันทีด้วยตัวแปรสองตัว ซึ่งเป็นรูปแบบที่พบบ่อยสำหรับ DP แบบ 1 มิติที่มีการพึ่งพาค่าก่อนหน้าสองขั้น
def rob_bottom_up(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
# Full table version: O(n) space
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[-1]
def rob_optimised(nums):
# O(1) space: only need last two values
if not nums: return 0
if len(nums) == 1: return nums[0]
prev2, prev1 = nums[0], max(nums[0], nums[1])
for i in range(2, len(nums)):
prev2, prev1 = prev1, max(prev1, prev2 + nums[i])
return prev1
print(rob_optimised([2, 7, 9, 3, 1])) # 12ผลรวมเส้นทางต่ำสุดในตาราง
ผลรวมเส้นทางต่ำสุด (LeetCode #64) คือโจทย์ที่ให้หาเส้นทางจากมุมซ้ายบนไปยังมุมขวาล่าง โดยทำให้ผลรวมของค่าต่าง ๆ ต่ำที่สุด (เคลื่อนที่ได้เฉพาะไปทางขวาหรือลงด้านล่าง) DP แบบ 2 มิติ: dp[i][j] = ผลรวมต่ำสุดเพื่อไปถึงช่อง (i,j) dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) เติมค่าจากซ้ายไปขวาและจากบนลงล่าง กรณีฐานคือ dp[0][0] = grid[0][0] แถวแรกเติมค่าโดยเคลื่อนไปทางขวาเท่านั้น และคอลัมน์แรกเติมค่าโดยเคลื่อนลงด้านล่างเท่านั้น
def min_path_sum(grid):
rows, cols = len(grid), len(grid[0])
dp = [[0] * cols for _ in range(rows)]
dp[0][0] = grid[0][0]
# Fill first row (can only come from left)
for c in range(1, cols):
dp[0][c] = dp[0][c-1] + grid[0][c]
# Fill first column (can only come from above)
for r in range(1, rows):
dp[r][0] = dp[r-1][0] + grid[r][0]
# Fill rest of the table
for r in range(1, rows):
for c in range(1, cols):
dp[r][c] = grid[r][c] + min(dp[r-1][c], dp[r][c-1])
return dp[rows-1][cols-1]
grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum(grid)) # 7: 1+3+1+1+1การแก้ไขตาราง DP ในข้อมูลต้นฉบับ
เมื่อห้ามใช้พื้นที่เพิ่มเติม บางครั้งคุณสามารถแก้ไขตารางข้อมูลเข้าด้วยตัวเองเพื่อใช้เป็นตาราง DP ได้ สำหรับผลรวมเส้นทางต่ำสุด ให้เขียนทับ grid[i][j] ด้วยต้นทุนต่ำสุดในการไปถึงช่องนั้น วิธีนี้ใช้พื้นที่เพิ่มเติม O(1) แต่ ทำลายข้อมูลเข้า ควรแจ้งข้อแลกเปลี่ยนนี้แก่ผู้สัมภาษณ์เสมอ และยืนยันว่าเป็นสิ่งที่ยอมรับได้ หากต้องเก็บข้อมูลเข้าไว้ ให้ใช้แนวทางอาร์เรย์แบบเลื่อนแทน
def min_path_sum_inplace(grid):
rows, cols = len(grid), len(grid[0])
# Modify grid in-place (O(1) extra space, destroys input)
for r in range(rows):
for c in range(cols):
if r == 0 and c == 0:
continue # starting cell
elif r == 0:
grid[r][c] += grid[r][c-1] # first row
elif c == 0:
grid[r][c] += grid[r-1][c] # first column
else:
grid[r][c] += min(grid[r-1][c], grid[r][c-1])
return grid[rows-1][cols-1]
import copy
grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_inplace(copy.deepcopy(grid))) # 7เปรียบเทียบ DP แบบจากบนลงล่างกับแบบจากล่างขึ้นบนในการทอนเหรียญ
ทั้งสองแนวทางแก้ปัญหาการทอนเหรียญได้อย่างเหมาะสมที่สุด แต่มีความแตกต่างกันในการใช้งานจริง แบบจากบนลงล่างเขียนได้ชัดเจนกว่า และคำนวณเฉพาะปัญหาย่อยที่เข้าถึงได้จริงเท่านั้น ส่วนแบบจากล่างขึ้นบนจะคำนวณจำนวนเงินทั้งหมดตั้งแต่ 0 ถึงเป้าหมาย แม้แต่จำนวนที่ไม่สามารถทำได้ด้วยเหรียญที่กำหนด ซึ่งจะยังคงมีค่าเป็นอนันต์ สำหรับปัญหาแบบเบาบางที่มีสถานะเข้าถึงได้ไม่มาก แบบจากบนลงล่างจะมีประสิทธิภาพกว่า ส่วนปัญหาแบบหนาแน่น แบบจากล่างขึ้นบนจะมีค่าใช้จ่ายส่วนเกินต่ำกว่า
import functools
# Top-down: only computes reachable amounts
def coin_change_top(coins, amount):
@functools.lru_cache(maxsize=None)
def dp(rem):
if rem == 0: return 0
if rem < 0: return float('inf')
return 1 + min(dp(rem - c) for c in coins)
r = dp(amount)
return r if r != float('inf') else -1
# Bottom-up: computes all amounts 0 to target
def coin_change_bottom(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for c in coins:
if i >= c: dp[i] = min(dp[i], 1 + dp[i-c])
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change_top([1,5,6,9], 11)) # 2
print(coin_change_bottom([1,5,6,9], 11)) # 2เส้นทางไม่ซ้ำกัน: DP สองมิติแบบคลาสสิก
เส้นทางไม่ซ้ำกัน (LeetCode #62) ให้หาจำนวนเส้นทางจากมุมซ้ายบนไปยังมุมขวาล่างของตารางขนาด m×n โดยเคลื่อนที่ได้เฉพาะไปทางขวาหรือด้านล่าง สูตรเวียนเกิดนั้นตรงไปตรงมา: dp[i][j] = dp[i-1][j] + dp[i][j-1] ซึ่งหมายถึงเส้นทางจากด้านบนบวกกับเส้นทางจากด้านซ้าย กรณีฐานคือ แถวแรกทั้งหมดและคอลัมน์แรกทั้งหมดมีเส้นทางได้เพียง 1 เส้นทาง เพราะมีทิศทางให้เดินได้เพียงทางเดียว DP สองมิตินี้ใช้เวลา O(mn) และลดพื้นที่ได้เหลือ O(n) ด้วยแถวแบบเลื่อน
def unique_paths(m, n):
# dp[i][j] = number of paths to reach cell (i,j)
dp = [[1] * n for _ in range(m)]
# Base: first row and first column are all 1
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
print(unique_paths(3, 7)) # 28
print(unique_paths(3, 2)) # 3
# O(n) space rolling row:
def unique_paths_opt(m, n):
row = [1] * n
for _ in range(1, m):
for j in range(1, n):
row[j] += row[j-1]
return row[n-1]
print(unique_paths_opt(3, 7)) # 28ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้เกี่ยวกับ DP แบบจากล่างขึ้นบนด้วยการทำตาราง และวิธีกำหนดลำดับการเติมค่าจากลูกศรแสดงการพึ่งพา การปรับให้ใช้พื้นที่น้อยลง ด้วยอาร์เรย์แบบเลื่อน (จาก O(mn) เหลือ O(n)) และการติดตามด้วยตัวแปรสองตัว (จาก O(n) เหลือ O(1)) รวมถึงการเขียนแบบจากล่างขึ้นบนสำหรับ ฟีโบนักชี การทอนเหรียญ LCS โจรปล้นบ้าน และผลรวมเส้นทางที่มีค่าต่ำสุด ต่อไป เราจะทำโจทย์การทอนเหรียญและบันไดต้นทุนต่ำสุดตั้งแต่ต้นจนจบ
คำถามที่พบบ่อย
บทเรียน “DP จากล่างขึ้นบนด้วยตาราง” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “DP จากล่างขึ้นบนด้วยตาราง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “DP จากล่างขึ้นบนด้วยตาราง”
แปลงคำตอบจากบนลงล่างเป็นตาราง DP แบบวนซ้ำ และลดพื้นที่จาก O(n) เป็น O(1) เมื่อจำเป็นต้องเก็บเพียงค่าบางรายการล่าสุด คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “DP จากล่างขึ้นบนด้วยตาราง” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- รู้จัก DP: ปัญหาย่อยที่ซ้ำซ้อน
- DP จากบนลงล่างด้วยการจดจำผลลัพธ์
- DP จากล่างขึ้นบนด้วยตาราง
- การทอนเหรียญและบันไดต้นทุนต่ำสุด