0Pricing
DSA Interview Prep · Pelajaran

Templat Backtracking: Pilih, Jelajahi, Batalkan Pilihan

Terapkan kerangka penelusuran mundur tiga langkah, telusuri kerjanya pada contoh kecil, dan identifikasi di bagian mana kondisi pemangkasan ditempatkan

Templat Backtracking: Pilih, Jelajahi, Batalkan Pilihan adalah pelajaran DSA Interview Prep gratis di CoddyKit. Ini adalah pelajaran 1 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar DSA Interview Prep, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Apa Itu Penelusuran Mundur?

Penelusuran mundur adalah metode sistematis untuk menemukan semua (atau sebagian) hasil dengan menjelajahi setiap kandidat secara bertahap dan meninggalkan (memangkas) sebuah cabang segera setelah ditentukan bahwa cabang tersebut tidak mungkin menghasilkan hasil yang valid. Metode ini digunakan untuk menyelesaikan Sudoku, menghasilkan permutasi, dan menemukan semua kombinasi yang valid. Bayangkan metode ini sebagai pencarian mendalam-terlebih-dahulu pada pohon keputusan.

# 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')

Templat Tiga Langkah

Setiap fungsi penelusuran mundur mengikuti tiga langkah: Pilih—pilih kandidat berikutnya dari opsi yang tersedia. Jelajahi—lakukan rekursi dengan pilihan tersebut, bergerak satu tingkat lebih dalam pada pohon keputusan. Batalkan Pilihan—batalkan pilihan setelah kembali dari rekursi untuk memulihkan keadaan sebelum mencoba kandidat berikutnya. Pola ini juga disebut tambah/rekursi/hapus atau tandai/rekursi/batalkan tanda dalam konteks yang berbeda.

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

Contoh Paling Sederhana: Semua Himpunan Bagian

Hasilkan semua himpunan bagian dari [1, 2, 3]. Pada setiap indeks, kita memilih untuk menyertakan atau mengecualikan elemen tersebut. Indeks awal maju setelah setiap pemanggilan sehingga kita tidak mengunjungi kembali elemen sebelumnya. Tidak diperlukan pemeriksaan batasan—setiap keadaan parsial valid. Proses ini menghasilkan 2ⁿ himpunan bagian. Langkah pembatalan pilihan dilakukan dengan path.pop() setelah pemanggilan rekursif.

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]]

Mengidentifikasi Kondisi Pemangkasan

Kekuatan penelusuran mundur dibandingkan pencarian menyeluruh terletak pada pemangkasan: mengenali sejak dini bahwa jalur parsial tidak dapat menghasilkan hasil yang valid. Untuk jumlah kombinasi (jumlah target dengan batas), setelah jumlah berjalan melebihi target, cabang yang lebih dalam hanya akan menghasilkan nilai yang lebih besar—pangkas dengan segera mengembalikan hasil. Untuk N-ratu, jika seorang ratu menyerang ratu yang sudah ada, lewati kolom tersebut. Pemangkasan mengubah pohon eksponensial menjadi pencarian yang dapat dikelola.

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]]

Pemulihan Keadaan Sangat Penting

Kesalahan umum dalam penelusuran mundur adalah tidak memulihkan keadaan sepenuhnya sebelum iterasi berikutnya. Jika Anda menggunakan struktur data yang dapat diubah (daftar, himpunan, kisi), setiap perubahan yang dibuat selama Pilih harus dibalik selama Batalkan Pilihan. Misalnya, saat mengubah kisi seperti pada Sudoku atau Pencarian Kata, kosongkan sel tersebut setelah pemanggilan rekursif. Jika langkah ini dilupakan, keadaan akan rusak untuk cabang-cabang sejajar.

# 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

Menelusuri Pohon Keputusan

Untuk jumlah kombinasi dengan [2, 3, 6, 7] dan target 7, telusuri pohonnya: di akar, coba 2. Dari 2, coba 2 lagi (remaining=3). Dari 2+2, coba 2 lagi (remaining=1). 2>1, jadi pangkas. Coba 3: 3>1, pangkas. Lakukan backtrack. Dari 2+2, coba 3 (remaining=3). 3 sama dengan nilai yang tersisa: catat [2,2,3]. Lakukan backtrack dan lanjutkan. Penelusuran ini menunjukkan bagaimana pemangkasan menghilangkan cabang sebelum menghasilkan hasil yang tidak valid.

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)

Penelusuran Mundur vs Pencarian Menyeluruh

Pencarian menyeluruh mencoba semua hasil lengkap yang mungkin lalu memvalidasi masing-masing. Penelusuran mundur melakukan pemangkasan selama konstruksi, sehingga tidak pernah menyelesaikan jalur yang tidak valid. Untuk N-ratu dengan N=8, pencarian menyeluruh memeriksa 8^8 = 16 juta penempatan. Penelusuran mundur menguranginya menjadi sekitar 2.057 pemanggilan rekursif. Perbedaannya meningkat drastis seiring bertambahnya N: untuk N=12, pencarian menyeluruh mencoba 8,9 miliar penempatan, sedangkan penelusuran mundur hanya menjelajahi sebagian kecil pohon.

# 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]}')

Mengumpulkan vs Mengembalikan Lebih Awal

Masalah penelusuran mundur terbagi menjadi dua kategori: menghasilkan semua hasil (mengumpulkan setiap jalur lengkap) atau menemukan satu hasil saja (mengembalikan True segera setelah sebuah jalur berhasil). Untuk enumerasi, selalu gunakan append untuk menambahkan hasil ke dalam daftar. Untuk pencarian satu hasil, segera kembalikan True dari pemanggilan rekursif dan teruskan nilai tersebut ke tingkat atas. Mengembalikan any(backtrack(...)) atau menggunakan if backtrack(...): return True menerapkan perilaku berhenti segera.

# 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

Memoisasi dengan Penelusuran Mundur

Penelusuran mundur murni menjelajahi setiap jalur tanpa penyimpanan tembolok, dan ini baik-baik saja saat semua hasil diperlukan. Namun, beberapa masalah penelusuran mundur memiliki submasalah yang saling tumpang tindih. Misalnya, Pemenggalan Kata II dapat diselesaikan dengan penelusuran mundur + memoisasi: simpan dalam tembolok daftar kalimat yang mungkin dibuat dari setiap indeks awal. Ini mengubah penelusuran mundur dengan waktu terburuk eksponensial menjadi algoritma dengan waktu polinomial. Kenali pengulangan submasalah agar dapat menerapkan pendekatan gabungan ini.

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']

Kompleksitas Waktu Penelusuran Mundur

Kompleksitas waktu penelusuran mundur bergantung pada jumlah daun dalam pohon keputusan dikalikan dengan pekerjaan per simpul. Untuk himpunan bagian: O(n × 2ⁿ). Untuk permutasi: O(n × n!). Untuk jumlah kombinasi: O(target/min_candidate ^ n) dalam kasus terburuk. Pemangkasan mengurangi konstanta, tetapi tidak mengubah batas asimtotik. Saat ditanya tentang kompleksitas dalam wawancara, berikan ukuran pohon kasus terburuk dan sebutkan bahwa pemangkasan biasanya membuat algoritma jauh lebih cepat dalam praktik.

# 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')

Mengidentifikasi Masalah Penelusuran Mundur

Tanda-tanda bahwa sebuah masalah memerlukan penelusuran mundur: (1) temukan semua atau hasilkan semua kombinasi, permutasi, atau himpunan bagian. (2) Masalah melibatkan penempatan elemen atau orang berdasarkan batasan (N-ratu, Sudoku). (3) Ruang hasil bersifat eksponensial, tetapi batasan menghilangkan sebagian besar cabang sejak awal. (4) Anda perlu menjelajahi jalur dalam graf atau kisi yang mungkin mengunjungi kembali keadaan. Saat melihat tanda-tanda ini, gunakan templat pilih-jelajahi-batalkan pilihan.

# 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

Pemeriksaan Singkat

Uji pemahaman Anda tentang konsep Struktur Data & Algoritma — Persiapan Wawancara Pemrograman dari pelajaran ini.

Rangkuman Pelajaran

Dalam pelajaran ini Anda telah mempelajari: templat penelusuran mundur memiliki tiga langkah—pilih, jelajahi, batalkan pilihan—yang masing-masing berarti menambahkan pilihan, melakukan rekursi, dan menghapusnya, kondisi pemangkasan menghilangkan cabang sejak awal dan menjadikan penelusuran mundur praktis dibandingkan pencarian menyeluruh, serta keadaan harus dipulihkan sepenuhnya setelah setiap pemanggilan rekursif agar cabang-cabang sejajar tidak rusak. Selanjutnya kita akan menerapkan templat ini untuk menghasilkan semua Himpunan Bagian dan Himpunan Kuasa.

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Templat Backtracking: Pilih, Jelajahi, Batalkan Pilihan” gratis?

Ya — teks lengkap “Templat Backtracking: Pilih, Jelajahi, Batalkan Pilihan” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus DSA Interview Prep, upgrade ke CoddyKit PRO. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Templat Backtracking: Pilih, Jelajahi, Batalkan Pilihan”?

Terapkan kerangka penelusuran mundur tiga langkah, telusuri kerjanya pada contoh kecil, dan identifikasi di bagian mana kondisi pemangkasan ditempatkan Kamu berlatih DSA Interview Prep dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai DSA Interview Prep?

Tidak diperlukan pengalaman sebelumnya. DSA Interview Prep di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 1 dari 4.

Berapa lama pelajaran “Templat Backtracking: Pilih, Jelajahi, Batalkan Pilihan” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran DSA Interview Prep ini?

Ya. Setiap pelajaran DSA Interview Prep menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Templat Backtracking: Pilih, Jelajahi, Batalkan Pilihan
  2. Himpunan Bagian dan Himpunan Kuasa
  3. Permutasi dan Kombinasi
  4. N-Queens dan Propagasi Kendala
← Kembali ke DSA Interview Prep