0Pricing
Coding Interview Prep · บทเรียน

แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก

นำโครงร่างการย้อนกลับสามขั้นตอนไปใช้งาน ทดลองไล่การทำงานกับตัวอย่างขนาดเล็ก และระบุว่าควรใส่เงื่อนไขการตัดกิ่งไว้ตรงไหน

แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก
  2. เซตย่อยและเพาเวอร์เซต
  3. การเรียงสับเปลี่ยนและการจัดหมู่
  4. N-ควีนและการเผยแพร่ข้อจำกัด
← กลับไปที่ Coding Interview Prep