ถอดรหัสวิธีและการนับเส้นทาง
แก้โจทย์วิธีถอดรหัส (การจับคู่ตัวเลขกับตัวอักษร) ด้วย DP ลักษณะคล้ายฟีโบนัชชี แล้วนับเส้นทางบนบันไดที่มีจำนวนก้าวแปรผัน
ถอดรหัสวิธีและการนับเส้นทาง เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ปัญหาวิธีการถอดรหัส
วิธีการถอดรหัส (LeetCode 91) จะแปลงสตริงตัวเลขเป็นตัวอักษร: 'A'=1, 'B'=2, ..., 'Z'=26 เมื่อกำหนดสตริงตัวเลขที่เข้ารหัสแล้ว ให้หาจำนวนวิธีที่แตกต่างกันในการถอดรหัส ตัวอย่างเช่น '12' สามารถถอดเป็น 'AB' (1+2) หรือ 'L' (12) จึงมี 2 วิธี '226' สามารถเป็น 'BZ' (2+26), 'VF' (22+6) หรือ 'BBF' (2+2+6) จึงมี 3 วิธี ศูนย์นำหน้าทำให้การถอดรหัสบางแบบไม่ถูกต้อง
# Encoding: A=1, B=2, ..., Z=26
# '12' → 'AB' or 'L' → 2 ways
# '226' → 'BZ' or 'VF' or 'BBF' → 3 ways
# '06' → invalid (no letter for '0')
# '10' → 'J' only → 1 way (only valid as 10, not 1+0)
s = '226'
print('Decodings for', s, ':', 3) # Expected: 3การกำหนดสูตร DP สำหรับจำนวนวิธีถอดรหัส
ให้ dp[i] = จำนวนวิธีถอดรหัส s[:i] กรณีฐานคือ dp[0] = 1 (สตริงว่างมีหนึ่งวิธี) และ dp[1] = 1 หาก s[0] != '0' มิฉะนั้นเป็น 0 การเปลี่ยนสถานะคือ หาก s[i-1] != '0' ให้บวก dp[i-1] (การถอดรหัสเลขหลักเดียว) หาก 10 ≤ int(s[i-2:i]) ≤ 26 ให้บวก dp[i-2] (การถอดรหัสเลขสองหลัก) โดยพื้นฐานแล้วนี่คือ รูปแบบลำดับฟีโบนัชชีที่มีการตรวจสอบความถูกต้อง
def num_decodings(s):
n = len(s)
dp = [0] * (n + 1)
dp[0] = 1 # empty prefix
dp[1] = 0 if s[0] == '0' else 1
for i in range(2, n + 1):
# Single digit decode
if s[i-1] != '0':
dp[i] += dp[i-1]
# Two digit decode
two_digit = int(s[i-2:i])
if 10 <= two_digit <= 26:
dp[i] += dp[i-2]
return dp[n]
print(num_decodings('12')) # 2
print(num_decodings('226')) # 3
print(num_decodings('06')) # 0กับดักเลขศูนย์นำหน้า
ส่วนที่ยากที่สุดของการถอดรหัสวิธีต่าง ๆ คือการจัดการเลขศูนย์ เลข 0 ที่อยู่เดี่ยว ๆ ไม่สามารถถอดรหัสได้ (ไม่มีตัวอักษรใดแทนค่าเป็น 0) ดังนั้นหาก s[i-1] == '0' อย่าบวก dp[i-1] เลข 0 ที่เป็นหลักที่สอง จะใช้ได้ก็ต่อเมื่อเลขสองหลักนั้นเป็น 10 หรือ 20 เท่านั้น ส่วน 30 หรือ 40 (รวมถึงค่าที่สูงกว่านั้น) ใช้ไม่ได้เนื่องจากเกิน 26 ตรวจสอบ 10 ≤ two_digit ≤ 26 เสมอ อย่าตรวจสอบเพียง two_digit ≤ 26 เท่านั้น
def num_decodings(s):
if not s or s[0] == '0': return 0
n = len(s)
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1 # s[0] != '0' guaranteed by guard above
for i in range(2, n + 1):
one = int(s[i-1])
two = int(s[i-2:i])
if one != 0: dp[i] += dp[i-1] # valid single digit
if 10 <= two <= 26: dp[i] += dp[i-2] # valid two digits
return dp[n]
print(num_decodings('10')) # 1 (only 'J')
print(num_decodings('30')) # 0 (30 > 26, '0' alone invalid)
print(num_decodings('100')) # 0 (dp[2]=1 then '00' invalid, single '0' invalid)การถอดรหัสแบบลดหน่วยความจำ
เช่นเดียวกับลำดับฟีโบนัชชี ความสัมพันธ์เวียนเกิดของจำนวนวิธีถอดรหัสจะย้อนดูเพียงสองตำแหน่งก่อนหน้า ดังนั้นจึงลดหน่วยความจำจาก O(n) เป็น O(1) ได้ด้วยตัวแปรสองตัว ใช้ prev2 (ย้อนกลับไปสองขั้น) และ prev1 (ย้อนกลับไปหนึ่งขั้น) ในแต่ละขั้น ให้คำนวณ curr จากค่าทั้งสอง แล้วเลื่อนค่าไป นี่เป็นการปรับให้เหมาะสมแบบใช้ตัวแปรสองตัวเช่นเดียวกับลำดับฟีโบนัชชี → การปรับให้เหมาะสมแบบใช้ตัวแปรสองตัว
def num_decodings_o1(s):
if not s or s[0] == '0': return 0
prev2 = 1 # dp[0]
prev1 = 1 # dp[1]
for i in range(2, len(s) + 1):
curr = 0
if s[i-1] != '0':
curr += prev1
two = int(s[i-2:i])
if 10 <= two <= 26:
curr += prev2
prev2, prev1 = prev1, curr
return prev1
print(num_decodings_o1('226')) # 3
print(num_decodings_o1('12')) # 2
print(num_decodings_o1('0')) # 0การนับเส้นทางบนบันได
การปีนบันได (LeetCode 70) ถามว่า หากสามารถก้าวครั้งละ 1 หรือ 2 ขั้น จะมีวิธีปีนบันได n ขั้นได้กี่วิธี ปัญหานี้ตรงกับลำดับฟีโบนัชชีพอดี: ways(n) = ways(n-1) + ways(n-2) โดย ways(1)=1, ways(2)=2, ways(3)=3, ways(4)=5 และสามารถสรุปทั่วไปได้เมื่อก้าวได้สูงสุด k ขั้น: ways(n) = sum(ways(n-1), ..., ways(n-k))
def climb_stairs(n):
if n <= 2: return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
prev2, prev1 = prev1, prev1 + prev2
return prev1
for i in range(1, 8):
print(f'climb_stairs({i}) = {climb_stairs(i)}')
# 1, 2, 3, 5, 8, 13, 21 — Fibonacci!การปีนบันไดด้วยจำนวนขั้นที่เปลี่ยนแปลงได้
เมื่อสามารถก้าวได้หลายจำนวนจากเซตที่กำหนด (เช่น {1, 3, 5}) ความสัมพันธ์เวียนเกิดจะเป็น dp[i] = sum(dp[i-k] for k in steps if i-k >= 0) ใช้หน้าต่างเลื่อนขนาด max(steps) เพื่อประหยัดหน่วยความจำ นี่คือรูปแบบการนับของปัญหาเป้สะพายหลังแบบไม่จำกัดจำนวน โดยสามารถใช้ขนาดการก้าวแต่ละค่าได้กี่ครั้งก็ได้
def count_ways(n, steps):
dp = [0] * (n + 1)
dp[0] = 1 # one way to stay at ground
for i in range(1, n + 1):
for step in steps:
if i >= step:
dp[i] += dp[i - step]
return dp[n]
# Steps of 1 or 2 (classic climbing stairs)
print(count_ways(5, [1, 2])) # 8
# Steps of 1, 3, or 5
print(count_ways(5, [1, 3, 5])) # 5
# Steps of 2 or 3
print(count_ways(6, [2, 3])) # 3 (2+2+2, 3+3, 2+4-invalid, 2+2+2, 3+3, 3+2+1-no...)การปีนบันไดด้วยต้นทุนต่ำสุด
การปีนบันไดด้วยต้นทุนต่ำสุด (LeetCode 746) กำหนดต้นทุนให้แต่ละขั้น และถามหาต้นทุนต่ำสุดในการไปถึงยอดบันได จากขั้น i สามารถกระโดดไปยัง i+1 หรือ i+2 ได้ ความสัมพันธ์เวียนเกิดคือ dp[i] = cost[i] + min(dp[i-1], dp[i-2]) คุณสามารถเริ่มจากขั้น 0 หรือขั้น 1 ก็ได้ คำตอบคือ min(dp[n-1], dp[n-2])
def min_cost_climbing(cost):
n = len(cost)
if n == 1: return cost[0]
dp = [0] * n
dp[0] = cost[0]
dp[1] = cost[1]
for i in range(2, n):
dp[i] = cost[i] + min(dp[i-1], dp[i-2])
return min(dp[-1], dp[-2]) # can start from step 0 or 1
print(min_cost_climbing([10, 15, 20])) # 15
print(min_cost_climbing([1, 100, 1, 1, 1, 100, 1, 1, 100, 1])) # 6การถอดรหัสวิธีต่าง ๆ II: ตัวเลขแทนค่าได้
การถอดรหัสวิธีต่าง ๆ II (LeetCode 639) เพิ่มอักขระแทนค่าได้ '*' ซึ่งสามารถแทนตัวเลขใดก็ได้ตั้งแต่ 1 ถึง 9 ทำให้จำนวนวิธีถอดรหัสที่ถูกต้องเพิ่มขึ้นอย่างมาก '*' หนึ่งตัวมีส่วนช่วย 9 วิธี (แทนตัวเลข 1-9 ได้) '*' สองตัวรวมกันสร้างชุดค่าผสมเลขสองหลักได้ 9×9 แบบ แต่ใช้ได้เฉพาะค่าที่ไม่เกิน 26 เท่านั้น (11-19 = 9 วิธี, 21-26 = 6 วิธี ดังนั้น '**' มี 15 วิธี) จึงต้องวิเคราะห์แต่ละกรณีอย่างรอบคอบ
def num_decodings_ii(s):
MOD = 10**9 + 7
prev2, prev1 = 1, 9 if s[0] == '*' else (0 if s[0] == '0' else 1)
for i in range(1, len(s)):
curr = 0
c, p = s[i], s[i-1]
# Single digit
if c == '*': curr += 9 * prev1
elif c != '0': curr += prev1
# Two digits
if p == '*' and c == '*': curr += 15 * prev2 # 11-19(9) + 21-26(6)
elif p == '*': curr += (2 if c <= '6' else 1) * prev2
elif c == '*': curr += (9 if p == '1' else (6 if p == '2' else 0)) * prev2
else:
two = int(p + c)
if 10 <= two <= 26: curr += prev2
prev2, prev1 = prev1, curr % MOD
return prev1 % MOD
print(num_decodings_ii('*')) # 9
print(num_decodings_ii('1*')) # 18ความเชื่อมโยงกับลำดับฟีโบนัชชี
ทั้งการถอดรหัสวิธีต่าง ๆ และการปีนบันไดล้วนเป็นปัญหาใน ตระกูลฟีโบนัชชีที่แฝงอยู่ หาก DP ใดมี dp[i] ขึ้นอยู่กับเพียง dp[i-1] และ dp[i-2] ปัญหานั้นจะมีรูปแบบฟีโบนัชชีและแก้ได้ด้วยหน่วยความจำ O(1) การตรวจสอบความถูกต้อง (เช่น ตัวเลขศูนย์และขนาดการก้าว) จะเปลี่ยนว่าการเปลี่ยนสถานะใดใช้งานได้ แต่ไม่เปลี่ยนโครงสร้างพื้นฐานที่ย้อนดูสองตำแหน่งก่อนหน้า การจดจำตระกูลนี้ได้ทันทีเป็นรูปแบบที่มีประโยชน์อย่างมากในการเพิ่มความเร็วระหว่างการสัมภาษณ์งาน
# Fibonacci family: dp[i] = f(dp[i-1], dp[i-2])
# Fibonacci itself: dp[i] = dp[i-1] + dp[i-2]
# Climbing stairs: dp[i] = dp[i-1] + dp[i-2]
# Decode ways: dp[i] = (dp[i-1] if one_valid) + (dp[i-2] if two_valid)
# Min cost stairs: dp[i] = cost[i] + min(dp[i-1], dp[i-2])
# House robber: dp[i] = max(dp[i-1], nums[i] + dp[i-2])
# All solved with 2 rolling variables:
prev2, prev1 = 0, 1
for _ in range(10):
prev2, prev1 = prev1, prev1 + prev2
print('Fibonacci F(10):', prev1) # 89การนับเส้นทางบนตาราง
ปัญหาการนับที่เกี่ยวข้องกันคือ เมื่อกำหนดตารางขนาด m×n จะมีเส้นทางที่ไม่ซ้ำกันจากมุมซ้ายบนไปยังมุมขวาล่างกี่เส้นทาง หากเคลื่อนที่ได้เฉพาะไปทางขวาหรือด้านล่าง คำตอบคือสัมประสิทธิ์ทวินาม C(m+n-2, m-1) วิธีแก้ด้วย DP จะเติมตารางสองมิติ โดยมี dp[i][j] = dp[i-1][j] + dp[i][j-1] นี่คือบันไดฟีโบนัชชีในรูปแบบสองมิติ แต่ละช่องมีค่าเป็นผลรวมของช่องด้านบนและช่องด้านซ้าย
def unique_paths(m, n):
dp = [[1] * n for _ in range(m)]
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]
# Or use math for O(1) solution
import math
def unique_paths_math(m, n):
return math.comb(m + n - 2, m - 1)
print(unique_paths(3, 7)) # 28
print(unique_paths_math(3, 7)) # 28
print(unique_paths(3, 3)) # 6สรุปจุดพลาดในการสัมภาษณ์งาน
จุดพลาดที่พบบ่อยในการถอดรหัสวิธีต่าง ๆ ได้แก่ (1) ลืมว่าเลข 0 เดี่ยว ๆ ใช้ไม่ได้ — ตรวจสอบ s[i-1] != '0' เสมอก่อนบวก dp[i-1] (2) ใช้ two_digit <= 26 โดยไม่ตรวจสอบ two_digit >= 10 — '07' ไม่ควรถอดรหัสเป็น 'G' (3) คืนค่า dp[n-1] แทนที่จะเป็น dp[n] — ตารางเริ่มนับดัชนีที่ 1 ดังนั้น dp[n] จึงสอดคล้องกับสตริงทั้งหมด ตรวจสอบดัชนีของอาร์เรย์ให้ถี่ถ้วนเสมอเมื่อ ตาราง DP ของคุณมีสมาชิกมากกว่าข้อมูลนำเข้าอยู่หนึ่งรายการ
# Common bug: checking two_digit <= 26 without >= 10
def buggy_decode(s):
dp = [0] * (len(s) + 1)
dp[0] = dp[1] = 1
for i in range(2, len(s) + 1):
if s[i-1] != '0': dp[i] += dp[i-1]
two = int(s[i-2:i])
# BUG: '07' gives two=7, and 7 <= 26 would add dp[i-2]
# Fix: require two >= 10
if 10 <= two <= 26: dp[i] += dp[i-2] # CORRECT
return dp[len(s)]
print(buggy_decode('06')) # 0 (correct, '0' alone invalid)
print(buggy_decode('07')) # 0 (correct, '07' not valid, '0' alone invalid)
print(buggy_decode('27')) # 1 (only 'BG', 27>26 so no two-digit)ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์งานเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า การถอดรหัสวิธีต่าง ๆ ใช้ความสัมพันธ์เวียนเกิดคล้ายฟีโบนัชชี โดยมีเงื่อนไขตรวจสอบความถูกต้องสำหรับการถอดรหัสเลขหลักเดียว (ต้องไม่เป็นศูนย์) และเลขสองหลัก (10-26) การปีนบันไดและการปีนบันไดด้วยต้นทุนต่ำสุดเป็นรูปแบบเฉพาะของฟีโบนัชชีที่แก้ได้ด้วยหน่วยความจำ O(1) และ การจดจำตระกูลฟีโบนัชชีที่ย้อนดูสองตำแหน่งก่อนหน้าช่วยประหยัดเวลาได้มากระหว่างการสัมภาษณ์งาน บทถัดไปเราจะศึกษา DP สองมิติด้วยเส้นทางที่ไม่ซ้ำกันและผลรวมเส้นทางต่ำสุดบนตาราง
คำถามที่พบบ่อย
บทเรียน “ถอดรหัสวิธีและการนับเส้นทาง” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ถอดรหัสวิธีและการนับเส้นทาง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ถอดรหัสวิธีและการนับเส้นทาง”
แก้โจทย์วิธีถอดรหัส (การจับคู่ตัวเลขกับตัวอักษร) ด้วย DP ลักษณะคล้ายฟีโบนัชชี แล้วนับเส้นทางบนบันไดที่มีจำนวนก้าวแปรผัน คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ถอดรหัสวิธีและการนับเส้นทาง” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม
- ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด
- การแบ่งคำและการแบ่งสตริงเป็นส่วน
- ถอดรหัสวิธีและการนับเส้นทาง