การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ
ลดพื้นที่ของ LCS และระยะห่างการแก้ไขจาก O(mn) เป็น O(min(m,n)) โดยเก็บเฉพาะแถวปัจจุบันและแถวก่อนหน้าในตาราง DP
การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดพื้นที่จึงสำคัญใน DP แบบ 2 มิติ
ตาราง DP 2 มิติสำหรับสตริงความยาว 1000 ต้องใช้ 1000×1000 = 1,000,000 เซลล์ หรือประมาณ 8 MB สำหรับจำนวนเต็ม 64 บิต สำหรับลำดับที่ยาวกว่านี้ (การจัดเรียง DNA และการเปรียบเทียบความแตกต่างของข้อความขนาดใหญ่) จะใช้งานได้ยาก ข้อสังเกตสำคัญคือสมการเวียนเกิดของ DP 2 มิติส่วนใหญ่อ้างอิงเพียงแถวปัจจุบันและแถวก่อนหน้า ดังนั้นจึงบีบอัดตารางทั้งหมดเป็นอาร์เรย์ 1 มิติหนึ่งหรือสองชุดได้ นี่คือแก่นของการลดการใช้พื้นที่ใน DP 2 มิติ
# Full 2D DP: O(mn) space
# LCS for 1000-char strings
m, n = 1000, 1000
dp_2d_size = m * n * 8 # bytes (64-bit ints)
print(f'2D table: {dp_2d_size:,} bytes = {dp_2d_size//1024} KB')
# 1D rolling array: O(n) space
dp_1d_size = n * 8
print(f'1D array: {dp_1d_size:,} bytes = {dp_1d_size} bytes')
print(f'Space saving: {dp_2d_size // dp_1d_size}x')รูปแบบอาร์เรย์เลื่อน
รูปแบบอาร์เรย์เลื่อนแทนที่ตาราง 2 มิติเต็มรูปแบบด้วยอาร์เรย์ 1 มิติที่แทนแถวก่อนหน้า เมื่อคำนวณแถว i คุณจะอัปเดตแต่ละเซลล์ j โดยใช้ค่าปัจจุบัน dp[j] (ซึ่งยังคงเก็บค่า dp[i-1][j] ของแถวก่อนหน้า) และค่า dp[j-1] ที่เพิ่งอัปเดต (ซึ่งคือ dp[i][j-1]) ตัวแปร diagonal จะเก็บค่า dp[i-1][j-1] ก่อนถูกเขียนทับ รูปแบบนี้ใช้ได้กับ LCS ระยะห่างการแก้ไข และปัญหา DP 2 มิติส่วนใหญ่
# Rolling array template for 2D DP
# Before update: dp[j] holds dp[i-1][j] (previous row)
# After update: dp[j] holds dp[i][j] (current row)
def rolling_array_template(grid):
m, n = len(grid), len(grid[0])
dp = [0] * (n + 1) # represents one row
for i in range(1, m + 1):
diag = 0 # stores dp[i-1][j-1] before overwrite
for j in range(1, n + 1):
temp = dp[j] # save dp[i-1][j] before overwriting
# compute dp[i][j] using dp[j] (above) and dp[j-1] (left) and diag
dp[j] = diag + dp[j] + dp[j-1] # placeholder logic
diag = temp
return dp[n]LCS กับการใช้พื้นที่ O(min m,n)
สำหรับ LCS ให้ตรวจสอบว่า text1 เป็นสตริงที่สั้นกว่า (เพื่อให้ n มีค่าน้อย) จัดสรรอาร์เรย์ 1 มิติขนาด n+1 แล้วประมวลผลทีละแถว ในแต่ละเซลล์: บันทึก temp = dp[j] (ซึ่งคือ dp[i-1][j]) จากนั้น หากอักขระตรงกัน ให้ใช้ dp[j] = diag + 1 มิฉะนั้นให้ใช้ dp[j] = max(dp[j], dp[j-1]) สุดท้ายกำหนด diag = temp เมื่อประมวลผลครบทุกแถวแล้ว dp[n] จะเก็บความยาวของ LCS
def lcs_space_opt(text1, text2):
# Ensure text2 is the shorter one
if len(text1) < len(text2):
text1, text2 = text2, text1
m, n = len(text1), len(text2)
dp = [0] * (n + 1)
for i in range(1, m + 1):
diag = 0
for j in range(1, n + 1):
temp = dp[j] # dp[i-1][j]
if text1[i-1] == text2[j-1]:
dp[j] = diag + 1
else:
dp[j] = max(dp[j], dp[j-1])
diag = temp
return dp[n]
print(lcs_space_opt('ABCBDAB', 'BDCABA')) # 4
print(lcs_space_opt('AGGTAB', 'GXTXAYB')) # 4ระยะห่างการแก้ไขด้วยพื้นที่ O(n)
ระยะห่างการแก้ไขใช้รูปแบบอาร์เรย์เลื่อนแบบเดียวกัน อาร์เรย์ 1 มิติเริ่มต้นแทนแถวที่ 0: dp[j] = j (การแทรกอักขระ j ตัว) สำหรับแต่ละแถว i ให้กำหนด dp[0] = i (การลบอักขระ i ตัว) และบันทึก diag = dp[0] ก่อนอัปเดต ในลูปด้านใน ให้บันทึก temp = dp[j] คำนวณค่าใหม่จากการแทรก (dp[j-1]+1) การลบ (dp[j]+1) และการแทนที่ (diag + cost) จากนั้นกำหนด diag = temp
def edit_dist_opt(s, t):
m, n = len(s), len(t)
dp = list(range(n + 1)) # row 0: dp[0][j] = j
for i in range(1, m + 1):
diag = dp[0] # dp[i-1][0] before dp[0] update
dp[0] = i # dp[i][0] = i
for j in range(1, n + 1):
temp = dp[j] # dp[i-1][j]
cost = 0 if s[i-1] == t[j-1] else 1
dp[j] = min(
dp[j-1] + 1, # insert
dp[j] + 1, # delete
diag + cost # replace or match
)
diag = temp
return dp[n]
print(edit_dist_opt('horse', 'ros')) # 3
print(edit_dist_opt('intention', 'execution')) # 5ผลรวมเส้นทางต่ำสุดด้วยพื้นที่ O(n)
สำหรับปัญหาผลรวมเส้นทางต่ำสุดบนตาราง อาร์เรย์แบบเลื่อน 1 มิติเริ่มต้นเป็นผลรวมสะสมของ the แถวแรก (มีเพียงหนึ่งเส้นทางเข้าสู่แต่ละช่องใน the แถวแรก) สำหรับแต่ละแถวถัดไป ให้อัปเดตจากซ้ายไปขวา: dp[j] ก่อนอัปเดตคือค่าจาก the แถวด้านบน (dp[i-1][j]) และ dp[j-1] ที่เพิ่งอัปเดตคือค่าจาก the ด้านซ้าย ไม่จำเป็นต้องใช้แนวทแยงที่นี่ เพราะผลรวมเส้นทางต่ำสุดไม่ต้องใช้ the ช่องแนวทแยง
def min_path_sum_opt(grid):
m, n = len(grid), len(grid[0])
dp = [float('inf')] * n
dp[0] = 0
for i in range(m):
# Update first column (only from above)
dp[0] += grid[i][0]
for j in range(1, n):
# min of above (dp[j] = old) and left (dp[j-1] = updated)
dp[j] = grid[i][j] + min(dp[j], dp[j-1])
return dp[n-1]
grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_opt(grid)) # 7เมื่อจำเป็นต้องเข้าถึงแนวทแยง
ปัญหา DP แบบ 2 มิติไม่ได้ทุกปัญหาจะบีบอัดได้ด้วยอาร์เรย์แบบเลื่อนอย่างง่าย เพราะบางปัญหาต้องใช้ the สมาชิกแนวทแยง dp[i-1][j-1] หลังจากเขียนทับ dp[j] แล้ว วิธีแก้เหมือนกันเสมอ: บันทึก temp = dp[j] ก่อนอัปเดต แล้วใช้ค่านั้นเป็น diag สำหรับการคำนวณของ the คอลัมน์ถัดไป รูปแบบการมองล่วงหน้าหนึ่งช่องนี้รองรับสมการเวียนเกิดแบบสามทิศทางทั้งหมด (LCS, ระยะห่างการแก้ไข) ได้อย่างเรียบร้อย
# Recap: the diagonal save pattern
# Without it: dp[j-1] updated (left) and dp[j] about to be overwritten
# With it:
def show_diagonal_pattern(s1, s2):
n = len(s2)
dp = [0] * (n + 1)
for ch1 in s1:
diag = 0 # was dp[i-1][0] = 0 for LCS
for j, ch2 in enumerate(s2, 1):
temp = dp[j] # SAVE before overwrite
if ch1 == ch2:
dp[j] = diag + 1 # use saved diagonal
else:
dp[j] = max(dp[j], dp[j-1])
diag = temp # advance diagonal
return dp[n]
print(show_diagonal_pattern('ABCBDAB', 'BDCABA')) # 4การเพิ่มประสิทธิภาพพื้นที่ของกระเป๋าเป้ 2 มิติ
ปัญหากระเป๋าเป้ 0/1 ก็ได้ประโยชน์จากการเพิ่มประสิทธิภาพการใช้หน่วยความจำ ตาราง 2 มิติเต็มรูปแบบมีขนาด (จำนวนสิ่งของ+1) × (ความจุ+1) อาร์เรย์แบบเลื่อนลดขนาดเหลือ O(ความจุ) ความแตกต่างสำคัญจาก LCS/ระยะห่างการแก้ไขคือ ให้ควบคุม the มิติความจุแบบย้อนกลับ (จากค่าสูงไปต่ำ) วิธีนี้ทำให้สิ่งของแต่ละชิ้นถูกนับได้มากที่สุดหนึ่งครั้ง — การวนซ้ำไปข้างหน้าอาจทำให้เลือกสิ่งของชิ้นเดิมได้หลายครั้ง
def knapsack_01(weights, values, capacity):
dp = [0] * (capacity + 1)
for w, v in zip(weights, values):
# Reverse order: prevents using the same item twice
for c in range(capacity, w - 1, -1):
dp[c] = max(dp[c], dp[c - w] + v)
return dp[capacity]
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
cap = 7
print(knapsack_01(weights, values, cap)) # 9 (items 3+4: weight 3+4=7, value 4+5=9)การวนซ้ำไปข้างหน้าเทียบกับย้อนกลับ
การทราบว่าควรวนซ้ำลูปด้านในไปในทิศทางใดเป็นเรื่องสำคัญ: ย้อนกลับสำหรับกระเป๋าเป้ 0/1 (ใช้สิ่งของแต่ละชิ้นได้มากที่สุดหนึ่งครั้ง — การย้อนดูสถานะก่อนหน้าจะป้องกันการใช้ซ้ำ) ไปข้างหน้าสำหรับกระเป๋าเป้แบบไม่จำกัด (นำสิ่งของแต่ละชิ้นกลับมาใช้ซ้ำได้ — การดูสถานะที่อัปเดตแล้วจะอนุญาตให้ใช้หลายครั้ง) หากทำผิดจะเปลี่ยนปัญหา 0/1 ให้เป็นแบบไม่จำกัด หรือเปลี่ยนกลับกันโดยไม่แสดงข้อผิดพลาด โปรดยืนยันข้อจำกัดเสมอก่อนเลือกทิศทาง
# 0/1 Knapsack: each item used AT MOST ONCE → iterate reverse
def knapsack_01_demo(weights, values, cap):
dp = [0] * (cap + 1)
for w, v in zip(weights, values):
for c in range(cap, w-1, -1): # REVERSE
dp[c] = max(dp[c], dp[c-w] + v)
return dp[cap]
# Unbounded Knapsack: items can be reused → iterate forward
def knapsack_unbounded(weights, values, cap):
dp = [0] * (cap + 1)
for c in range(1, cap + 1):
for w, v in zip(weights, values):
if c >= w:
dp[c] = max(dp[c], dp[c-w] + v) # FORWARD
return dp[cap]
print(knapsack_01_demo([2,3],[3,4],5)) # 7
print(knapsack_unbounded([2,3],[3,4],5)) # 8 (use weight-2 twice: 3+3=6? or 4+... )เส้นทางที่แตกต่างกันด้วยพื้นที่ O(n)
สำหรับปัญหาเส้นทางที่แตกต่างกัน ตารางทั้งหมดสามารถแทนที่ด้วยแถวเดียวได้ กำหนดค่าเริ่มต้นของทุกช่องเป็น 1 (the แถวแรก) สำหรับแต่ละแถวถัดไป ให้อัปเดตจากซ้ายไปขวา: dp[j] += dp[j-1] ไม่จำเป็นต้องใช้แนวทแยง เพราะสมการเวียนเกิดใช้เพียง the ช่องด้านบน (dp[j] ซึ่งเป็นค่าปัจจุบันก่อนอัปเดต) และ the ช่องด้านซ้าย (dp[j-1] ซึ่งอัปเดตแล้ว) นี่คือการบีบอัดจาก 2 มิติเป็น 1 มิติที่ง่ายที่สุด
def unique_paths_opt(m, n):
dp = [1] * n # first row: all 1s
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1] # above (dp[j]) + left (dp[j-1])
return dp[n-1]
# With obstacles
def unique_paths_obstacles_opt(grid):
m, n = len(grid), len(grid[0])
dp = [0] * n
dp[0] = 1
for i in range(m):
if grid[i][0] == 1: dp[0] = 0 # blocked column
for j in range(1, n):
if grid[i][j] == 1: dp[j] = 0 # blocked
else: dp[j] += dp[j-1]
return dp[n-1]
print(unique_paths_opt(3, 7)) # 28
print(unique_paths_obstacles_opt([[0,0,0],[0,1,0],[0,0,0]])) # 2บัฟเฟอร์สองแถวสำหรับสมการเวียนเกิดที่ซับซ้อน
เมื่อสมการเวียนเกิดต้องใช้ช่องจาก the สองแถวก่อนหน้าขึ้นไป (เช่น รูปแบบบางอย่างของ DP ช่วง หรือการลดรูป DP 3 มิติ) ให้ใช้บัฟเฟอร์สองแถว: รักษาอาร์เรย์ prev และ curr แล้วสลับอาร์เรย์หลังจบแต่ละแถว วิธีนี้ใช้พื้นที่ O(2n) = O(n) สำหรับสมการเวียนเกิดที่ย้อนกลับไป k แถว ให้รักษาอาร์เรย์ k ชุดเป็นบัฟเฟอร์วงกลม วิธีนี้ขยายรูปแบบอาร์เรย์แบบเลื่อนหนึ่งแถวให้ครอบคลุมกรณีทั่วไป
def lcs_two_row_buffer(s1, s2):
m, n = len(s1), len(s2)
prev = [0] * (n + 1) # dp[i-1]
curr = [0] * (n + 1) # dp[i]
for i in range(1, m + 1):
curr[0] = 0
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = max(prev[j], curr[j-1])
prev, curr = curr, prev # swap (curr becomes prev)
return prev[n] # after swap, prev holds the last computed row
print(lcs_two_row_buffer('ABCBDAB', 'BDCABA')) # 4เมื่อไม่สามารถเพิ่มประสิทธิภาพพื้นที่ได้
การเพิ่มประสิทธิภาพพื้นที่ไม่สามารถทำได้เสมอไป หากต้องสร้าง solution ที่เหมาะที่สุดกลับคืน (ไม่ใช่เพียงค่าของ solution) โดยทั่วไปจำเป็นต้องใช้ the ตารางทั้งหมดสำหรับการย้อนรอย วิธีแก้ที่เป็นไปได้ ได้แก่: (1) จัดเก็บตารางการตัดสินใจแยกต่างหากที่มีขนาดเท่ากัน (2) ใช้อัลกอริทึมของ Hirschberg ซึ่งคำนวณ LCS ได้ในเวลา O(mn) และใช้พื้นที่ O(ค่าต่ำสุด(m,n)) รวมถึงการสร้าง solution กลับคืน โดยแบ่งปัญหาที่จุดกึ่งกลางแบบเรียกซ้ำ (3) ยอมรับการใช้พื้นที่ O(mn) เมื่อจำเป็นต้องสร้าง solution กลับคืน
# When reconstruction needed: must keep full table or use Hirschberg
# Hirschberg's idea: compute LCS length in O(n) space at midpoint of s1,
# recurse on left and right halves. O(mn) time, O(n) space + reconstruction.
# For interview: mention the trade-off
# 'I can reduce to O(n) space if only the value is needed.
# To also reconstruct the sequence, I need the full O(mn) table
# or a more complex divide-and-conquer approach.'
print('Space opt: O(n) for length only')
print('Full table: O(mn) needed for reconstruction')ตรวจสอบอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า: ตาราง DP แบบ 2 มิติสามารถบีบอัดให้ใช้พื้นที่ O(n) ได้ด้วยอาร์เรย์ 1 มิติแบบเลื่อน เมื่อใช้เพียง the แถวก่อนหน้า รูปแบบตัวแปรแนวทแยง (บันทึก temp ก่อนเขียนทับ) รองรับสมการเวียนเกิดที่ต้องใช้ dp[i-1][j-1] และ กระเป๋าเป้ 0/1 ต้องวนความจุแบบย้อนกลับ ขณะที่กระเป๋าเป้แบบไม่จำกัดต้องวนไปข้างหน้า บทถัดไปเราจะศึกษาแม่แบบการย้อนกลับ: เลือก สำรวจ ยกเลิกการเลือก — รากฐานของอัลกอริทึมการค้นหาแบบครบถ้วน
คำถามที่พบบ่อย
บทเรียน “การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ”
ลดพื้นที่ของ LCS และระยะห่างการแก้ไขจาก O(mn) เป็น O(min(m,n)) โดยเก็บเฉพาะแถวปัจจุบันและแถวก่อนหน้าในตาราง DP คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เส้นทางไม่ซ้ำและผลรวมเส้นทางต่ำสุดบนกริด
- ลำดับร่วมที่ยาวที่สุด
- ระยะห่างการแก้ไข (Levenshtein)
- การเพิ่มประสิทธิภาพพื้นที่สำหรับ DP สองมิติ