แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก
นำโครงร่างการย้อนกลับสามขั้นตอนไปใช้งาน ทดลองไล่การทำงานกับตัวอย่างขนาดเล็ก และระบุว่าควรใส่เงื่อนไขการตัดกิ่งไว้ตรงไหน
แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
การย้อนกลับคืออะไร
การย้อนกลับคือวิธีการอย่างเป็นระบบสำหรับค้นหา solution ทั้งหมด (หรือบางส่วน) โดยสำรวจตัวเลือกแต่ละตัวเพิ่มขึ้นทีละขั้น และละทิ้ง (การตัดกิ่ง) กิ่งทันทีที่ระบุได้ว่ากิ่งนั้นไม่สามารถสร้าง solution ที่ถูกต้องได้ นี่คืออัลกอริทึมเบื้องหลังการแก้ซูโดกุ การสร้างการเรียงสับเปลี่ยน และการค้นหาชุดผสมที่ถูกต้องทั้งหมด ลองนึกภาพว่านี่คือการค้นหาแบบเจาะลึกบนต้นไม้การตัดสินใจ
# Mental model: backtracking explores a decision tree
# At each node you make a choice, go deeper, then undo it
#
# Tree for generating subsets of [1,2,3]:
# []
# / \
# [1] []
# / \ / \
# [1,2][1][2] []
# ...
# Every leaf is a potential solution
# Pruning cuts branches early based on constraints
print('Backtracking = DFS on decision tree with pruning')แม่แบบสามขั้นตอน
ฟังก์ชันการย้อนกลับทุกฟังก์ชันทำตามสามขั้นตอน: เลือก — เลือกตัวเลือกถัดไปจากตัวเลือกที่มีอยู่ สำรวจ — เรียกฟังก์ชันซ้ำด้วยตัวเลือกนั้น โดยลงลึกไปอีกหนึ่งระดับใน the ต้นไม้การตัดสินใจ ยกเลิกการเลือก — ยกเลิกตัวเลือกหลังกลับจากการเรียกซ้ำ เพื่อคืนสถานะสำหรับตัวเลือกถัดไป รูปแบบนี้ยังเรียกว่า เพิ่ม/เรียกซ้ำ/นำออก หรือ ทำเครื่องหมาย/เรียกซ้ำ/ยกเลิกเครื่องหมาย ในบริบทที่แตกต่างกัน
def backtrack(current_state, choices, results):
# Base case: is current_state a complete solution?
if is_complete(current_state):
results.append(list(current_state)) # record solution
return
for choice in choices:
if is_valid(choice, current_state): # pruning condition
# 1. CHOOSE
current_state.append(choice)
# 2. EXPLORE
backtrack(current_state, choices, results)
# 3. UNCHOOSE (backtrack)
current_state.pop()
# Placeholder functions — filled per problem
def is_complete(state): return True
def is_valid(choice, state): return Trueตัวอย่างที่ง่ายที่สุด: เซตย่อยทั้งหมด
สร้างเซตย่อยทั้งหมดของ [1, 2, 3] ในแต่ละดัชนี เราเลือกรวมหรือไม่รวมองค์ประกอบนั้น ดัชนีเริ่มต้นจะเลื่อนไปข้างหน้าหลังการเรียกแต่ละครั้ง เพื่อไม่ให้ย้อนพิจารณาองค์ประกอบก่อนหน้า ไม่จำเป็นต้องตรวจสอบข้อจำกัด — ทุกสถานะบางส่วนถูกต้องทั้งหมด การดำเนินการนี้สร้างเซตย่อยจำนวน 2ⁿ ชุด ขั้นตอนการยกเลิกการเลือกคือ path.pop() หลังการเรียกซ้ำ
def subsets(nums):
result = []
def backtrack(start, path):
result.append(list(path)) # every state is a valid subset
for i in range(start, len(nums)):
path.append(nums[i]) # CHOOSE
backtrack(i + 1, path) # EXPLORE
path.pop() # UNCHOOSE
backtrack(0, [])
return result
print(subsets([1, 2, 3]))
# [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]การระบุเงื่อนไขการตัดกิ่ง
พลังของการย้อนกลับเมื่อเทียบกับการลองครบทุกกรณีอยู่ที่การตัดกิ่ง: การตระหนักได้ตั้งแต่เนิ่น ๆ ว่าเส้นทางบางส่วนไม่สามารถนำไปสู่ solution ที่ถูกต้องได้ สำหรับผลรวมชุดผสม (ผลรวมเป้าหมายภายใต้ขีดจำกัด) เมื่อผลรวมสะสมเกิน the เป้าหมายแล้ว กิ่งที่ลึกลงไปจะมีค่าเพิ่มขึ้นเท่านั้น — ให้ตัดกิ่งด้วยการคืนค่าทันที สำหรับปัญหา N-ควีน หากควีนโจมตีควีนที่มีอยู่แล้ว ให้ข้ามคอลัมน์นั้น การตัดกิ่งเปลี่ยนต้นไม้แบบเลขชี้กำลังให้เป็นการค้นหาที่จัดการได้
def combination_sum(candidates, target):
result = []
candidates.sort() # sort enables early termination
def backtrack(start, path, remaining):
if remaining == 0:
result.append(list(path))
return
for i in range(start, len(candidates)):
c = candidates[i]
if c > remaining: break # PRUNE: sorted, so rest are bigger too
path.append(c) # CHOOSE
backtrack(i, path, remaining - c) # EXPLORE (reuse allowed)
path.pop() # UNCHOOSE
backtrack(0, [], target)
return result
print(combination_sum([2, 3, 6, 7], 7)) # [[2,2,3],[7]]การคืนสถานะเป็นสิ่งสำคัญอย่างยิ่ง
ข้อผิดพลาดที่พบบ่อยในการย้อนกลับคือการคืนสถานะไม่ครบถ้วนก่อนการวนซ้ำครั้งถัดไป หากใช้โครงสร้างข้อมูลที่เปลี่ยนแปลงได้ (รายการ เซต ตาราง) การแก้ไขทุกอย่างที่ทำระหว่างเลือกต้องถูกย้อนกลับระหว่างยกเลิกการเลือก ตัวอย่างเช่น เมื่อแก้ไขตาราง (เช่น ซูโดกุหรือ Word Search) ให้กำหนดช่องนั้นเป็นค่าว่างหลัง the การเรียกซ้ำ หากลืมทำเช่นนี้ สถานะจะเสียหายสำหรับกิ่งพี่น้อง
# Bug: forgetting to unmark in word search
# Correct pattern for grid backtracking:
def word_search(board, word):
m, n = len(board), len(board[0])
def dfs(r, c, k):
if k == len(word): return True
if not (0<=r<m and 0<=c<n): return False
if board[r][c] != word[k]: return False
temp, board[r][c] = board[r][c], '#' # CHOOSE (mark visited)
found = any(dfs(r+dr, c+dc, k+1)
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)])
board[r][c] = temp # UNCHOOSE (restore cell)
return found
return any(dfs(r, c, 0) for r in range(m) for c in range(n))
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']]
print(word_search([row[:] for row in board], 'ABCCED')) # Trueการติดตามต้นไม้การตัดสินใจ
สำหรับผลรวมชุดผสมที่มี [2, 3, 6, 7] และเป้าหมาย 7 ให้ติดตามต้นไม้ดังนี้: ที่ราก ให้ลอง 2 จาก 2 ให้ลอง 2 อีกครั้ง (ค่าที่เหลือ=3) จาก 2+2 ให้ลอง 2 อีกครั้ง (ค่าที่เหลือ=1) 2>1 จึงตัดกิ่ง ลอง 3: 3>1 จึงตัดกิ่ง ย้อนกลับ จาก 2+2 ให้ลอง 3 (ค่าที่เหลือ=3) 3 ตรงกับค่าที่เหลือ: บันทึก [2,2,3] ย้อนกลับและดำเนินการต่อ การติดตามนี้แสดงให้เห็นว่าการตัดกิ่งกำจัดกิ่งต่าง ๆ ได้ก่อนที่จะสร้างผลลัพธ์ที่ไม่ถูกต้อง
def combination_sum_trace(candidates, target):
result = []
candidates.sort()
def backtrack(start, path, remaining, depth):
indent = ' ' * depth
print(f'{indent}explore({path}, remaining={remaining})')
if remaining == 0:
result.append(list(path))
print(f'{indent}FOUND: {path}')
return
for i in range(start, len(candidates)):
c = candidates[i]
if c > remaining:
print(f'{indent}PRUNE at {c}')
break
path.append(c)
backtrack(i, path, remaining - c, depth + 1)
path.pop()
backtrack(0, [], target, 0)
return result
combination_sum_trace([2, 3, 6, 7], 7)การย้อนกลับเทียบกับการลองครบทุกกรณี
การลองครบทุกกรณีจะลอง solution ที่สมบูรณ์ที่เป็นไปได้ทั้งหมด แล้วตรวจสอบแต่ละ solution การย้อนกลับจะตัดกิ่งระหว่างการสร้าง จึงไม่สร้างเส้นทางที่ไม่ถูกต้องจนเสร็จ สำหรับปัญหา N-ควีนที่ N=8 การลองครบทุกกรณีจะตรวจสอบการวาง 8^8 = 16 ล้านรูปแบบ การย้อนกลับลดจำนวนนี้เหลือประมาณ 2,057 ครั้งของการเรียกซ้ำ ความแตกต่างจะเพิ่มขึ้นอย่างมากเมื่อ N เพิ่มขึ้น: เมื่อ N=12 การลองครบทุกกรณีจะลองการวาง 8.9 พันล้านรูปแบบ ขณะที่การย้อนกลับสำรวจเพียงเศษส่วนหนึ่งของต้นไม้
# Compare call counts: brute force vs backtracking for permutations
import sys
calls_brute = [0]
calls_back = [0]
def brute_force_perms(nums):
from itertools import permutations
return list(permutations(nums))
def backtrack_perms(nums):
result = []
used = [False] * len(nums)
def bt(path):
calls_back[0] += 1
if len(path) == len(nums):
result.append(list(path))
return
for i, n in enumerate(nums):
if not used[i]:
used[i] = True
path.append(n)
bt(path)
path.pop()
used[i] = False
bt([])
return result
backtrack_perms([1,2,3,4])
print(f'Backtrack calls for 4 items: {calls_back[0]}')การรวบรวมเทียบกับการคืนค่าก่อนกำหนด
ปัญหาการย้อนกลับแบ่งออกเป็นสองประเภท: แจกแจง solution ทั้งหมด (รวบรวมเส้นทางที่สมบูรณ์ทุกเส้นทาง) หรือ ค้นหา solution ใด solution หนึ่ง (คืนค่า จริงทันทีที่เส้นทางหนึ่งสำเร็จ) สำหรับการแจกแจง ให้เพิ่มผลลัพธ์ต่อท้ายรายการผลลัพธ์เสมอ สำหรับการค้นหาใดก็ได้ ให้คืนค่า จริงทันทีจากการเรียกซ้ำ และส่งค่าดังกล่าวย้อนขึ้นไป การคืนค่า any(backtrack(...)) หรือ if backtrack(...): return True จะทำให้เกิดพฤติกรรมหยุดประเมินทันที
# Enumerate all: collect in results list
def all_solutions(candidates):
results = []
def bt(path, remaining):
if remaining == 0:
results.append(list(path))
return
for c in candidates:
if c <= remaining:
path.append(c); bt(path, remaining - c); path.pop()
bt([], 5)
return results
# Find any one: return True on first success
def any_solution(candidates, target):
def bt(path, remaining):
if remaining == 0: return True
for c in candidates:
if c <= remaining:
path.append(c)
if bt(path, remaining - c): return True # short-circuit
path.pop()
return False
path = []
return bt(path, target), pathการจดจำผลลัพธ์ด้วยการย้อนกลับ
การย้อนกลับล้วน ๆ จะสำรวจทุกเส้นทางโดยไม่แคช ซึ่งเหมาะเมื่อจำเป็นต้องใช้ solution ทั้งหมด อย่างไรก็ตาม ปัญหาการย้อนกลับบางอย่างมีปัญหาย่อยที่ทับซ้อนกัน ตัวอย่างเช่น ปัญหาการแบ่งคำ II สามารถแก้ได้ด้วยการย้อนกลับร่วมกับการจดจำผลลัพธ์: แคชรายการประโยคที่เป็นไปได้จากแต่ละดัชนีเริ่มต้น วิธีนี้เปลี่ยนการย้อนกลับที่มีกรณีเลวร้ายที่สุดเป็นเลขชี้กำลังให้เป็นอัลกอริทึมเวลาเชิงพหุนาม ให้สังเกตเมื่อปัญหาย่อยเกิดซ้ำเพื่อใช้แนวทางผสมนี้
from functools import lru_cache
def word_break_all(s, wordDict):
words = set(wordDict)
@lru_cache(maxsize=None)
def bt(start):
if start == len(s): return [''] # empty suffix
result = []
for end in range(start + 1, len(s) + 1):
word = s[start:end]
if word in words:
for rest in bt(end):
result.append(word if not rest else word + ' ' + rest)
return result
return bt(0)
print(word_break_all('catsanddog', ['cat','cats','and','sand','dog']))
# ['cat sand dog', 'cats and dog']ความซับซ้อนด้านเวลาของการย้อนกลับ
ความซับซ้อนด้านเวลาของการย้อนกลับขึ้นอยู่กับจำนวนใบในต้นไม้การตัดสินใจคูณกับงานต่อโหนด สำหรับ subsets: O(n × 2ⁿ) สำหรับการเรียงสับเปลี่ยน: O(n × n!) สำหรับผลรวมชุดผสม: O(เป้าหมาย/ตัวเลือกต่ำสุด ^ n) ในกรณีเลวร้ายที่สุด การตัดกิ่งลดค่าคงที่ แต่ไม่ลดขอบเขตเชิงเส้นกำกับ เมื่อถูกถามเรื่องความซับซ้อนในการสัมภาษณ์ ให้ระบุขนาดต้นไม้ในกรณีเลวร้ายที่สุด และกล่าวว่าการตัดกิ่งมักทำให้อัลกอริทึมเร็วขึ้นมากในทางปฏิบัติ
# Complexity quick reference:
# Subsets of n elements: O(n * 2^n) - 2^n subsets, each copied in O(n)
# Permutations of n: O(n * n!) - n! perms, each copied in O(n)
# Combination sum (target T): O(T^n / n!) worst case without pruning
# N-Queens: O(n!) - prune reduces practical count
# For n=10 permutations: 10! = 3,628,800 paths
import math
n = 10
print(f'n={n}: n!={math.factorial(n):,} paths')
print(f'n={n}: 2^n={2**n:,} subsets')การระบุปัญหาที่ใช้การย้อนกลับ
สัญญาณที่บ่งบอกว่าปัญหาต้องใช้การย้อนกลับ: (1) ค้นหาทั้งหมด หรือ สร้างทั้งหมด ชุดผสม การเรียงสับเปลี่ยน หรือ subsets (2) ปัญหาเกี่ยวข้องกับการวางสิ่งของหรือผู้คนภายใต้ข้อจำกัด (ปัญหา N-ควีน ซูโดกุ) (3) พื้นที่ของ solution มีขนาดเลขชี้กำลัง แต่ข้อจำกัดกำจัดกิ่งส่วนใหญ่ได้ตั้งแต่เนิ่น ๆ (4) จำเป็นต้องสำรวจเส้นทางในกราฟหรือตารางที่อาจย้อนกลับมายังสถานะเดิม เมื่อพบสัญญาณเหล่านี้ ให้เลือกใช้แม่แบบเลือก-สำรวจ-ยกเลิกการเลือก
# Common backtracking problem types:
# 1. Subsets / Power set
# 2. Permutations (with/without duplicates)
# 3. Combinations (k from n, combination sum)
# 4. Grid path finding (word search, unique paths with visited tracking)
# 5. Constraint satisfaction (N-queens, Sudoku solver)
# 6. String partitioning (palindrome partition, word break all)
# Template reminder:
def backtrack(start, path):
# base case: add to results or return True
for choice in get_choices(start):
if is_valid(choice, path): # prune
path.append(choice) # choose
backtrack(start+1, path) # explore
path.pop() # unchoose
def get_choices(start): return []
def is_valid(c, p): return Trueตรวจสอบอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า: แม่แบบการย้อนกลับมีสามขั้นตอน — เลือก สำรวจ ยกเลิกการเลือก — ซึ่งสอดคล้องกับการเพิ่มตัวเลือก การเรียกซ้ำ และการนำตัวเลือกออก เงื่อนไขการตัดกิ่งกำจัดกิ่งต่าง ๆ ได้ตั้งแต่เนิ่น ๆ และเป็นสิ่งที่ทำให้การย้อนกลับใช้งานได้จริงเมื่อเทียบกับการลองครบทุกกรณี และ ต้องคืนสถานะให้ครบถ้วนหลังการเรียกซ้ำแต่ละครั้ง เพื่อหลีกเลี่ยงการทำให้กิ่งพี่น้องเสียหาย บทถัดไปเราจะใช้แม่แบบนี้สร้างเซตย่อยทั้งหมดและเพาเวอร์เซต
คำถามที่พบบ่อย
บทเรียน “แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก”
นำโครงร่างการย้อนกลับสามขั้นตอนไปใช้งาน ทดลองไล่การทำงานกับตัวอย่างขนาดเล็ก และระบุว่าควรใส่เงื่อนไขการตัดกิ่งไว้ตรงไหน คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก
- เซตย่อยและเพาเวอร์เซต
- การเรียงสับเปลี่ยนและการจัดหมู่
- N-ควีนและการเผยแพร่ข้อจำกัด