0Pricing
Coding Interview Prep · درس

قالب التراجع: اختر واستكشف وتراجع

طبّق الهيكل الأساسي للتراجع ذي الخطوات الثلاث، وتتّبعه على مثال صغير، وحدّد مواضع إدراج شروط التقليم.

قالب التراجع: اختر واستكشف وتراجع درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ما المقصود بالتراجع

التراجع هو أسلوب منهجي للعثور على جميع الحلول (أو بعضها) من خلال استكشاف كل مرشح تدريجيًا والتخلي عن فرع (تقليمه) بمجرد التأكد من أنه لا يمكن أن يؤدي إلى حل صالح. وهو الخوارزمية التي تقف وراء حل Sudoku وتوليد التباديل والعثور على جميع التركيبات الصالحة. فكّروا فيه على أنه بحث بالعمق في شجرة قرارات.

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

قالب الخطوات الثلاث

تتبع كل دالة للتراجع ثلاث خطوات: Choose — اختيار المرشح التالي من الخيارات المتاحة. Explore — إجراء استدعاء تكراري مع هذا الاختيار، والتعمق مستوى واحدًا في شجرة القرارات. Unchoose — التراجع عن الاختيار بعد العودة من الاستدعاء التكراري لاستعادة الحالة استعدادًا للمرشح التالي. يُسمى هذا النمط أيضًا add/recurse/remove أو mark/recurse/unmark في سياقات مختلفة.

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

تحديد شرط التقليم

تكمن قوة التراجع مقارنة بالقوة الغاشمة في التقليم: أي إدراك مبكر أن مسارًا جزئيًا لا يمكن أن يؤدي إلى حل صالح. في مسألة مجموع التركيبات (مجموع مستهدف مع حدّ)، بمجرد أن يتجاوز المجموع الجاري الهدف، سيزداد أي فرع أعمق فقط — لذا يُقلَّم الفرع بالعودة فورًا. وفي مسألة N-queens، إذا كانت الملكة تهاجم ملكات موجودة، يُتخطّى ذلك العمود. يحوّل التقليم أشجارًا أسّية إلى عمليات بحث يمكن التعامل معها.

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

استعادة الحالة أمر بالغ الأهمية

من الأخطاء الشائعة في التراجع عدم استعادة الحالة بالكامل قبل التكرار التالي. عند استخدام بنية بيانات قابلة للتغيير (مثل قائمة أو مجموعة أو شبكة)، يجب عكس كل تعديل أُجري أثناء Choose في خطوة Unchoose. فعلى سبيل المثال، عند تعديل شبكة (مثل Sudoku أو Word Search)، يجب تعيين الخلية إلى فارغة بعد الاستدعاء التكراري. يؤدي نسيان ذلك إلى إفساد الحالة بالنسبة إلى الفروع الشقيقة.

# 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 مرة أخرى (remaining=3). ومن 2+2، جرّبوا 2 مرة أخرى (remaining=1). تكون 2>1، لذا يُقلَّم الفرع. جرّبوا 3: تكون 3>1، لذا يُقلَّم الفرع. تراجعوا. ومن 2+2، جرّبوا 3 (remaining=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)

التراجع مقابل القوة الغاشمة

تجرّب القوة الغاشمة جميع الحلول الكاملة الممكنة، ثم تتحقق من صحة كل منها. أما التراجع فيُجري التقليم أثناء البناء، ولا يُكمل المسارات غير الصالحة أبدًا. في مسألة N-queens عندما تكون 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]}')

الجمع مقابل العودة المبكرة

تنقسم مسائل التراجع إلى فئتين: تعداد جميع الحلول (جمع كل مسار مكتمل) أو العثور على حل واحد (إرجاع True فور نجاح أحد المسارات). عند التعداد، أضيفوا دائمًا الحل إلى قائمة نتائج. وعند البحث عن أي حل، أعيدوا True فورًا من الاستدعاء التكراري ومرّروها إلى الأعلى. يطبّق إرجاع 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

الحفظ المؤقت مع التراجع

يستكشف التراجع الخالص كل مسار دون تخزين مؤقت، وهذا مناسب عندما تكون هناك حاجة إلى جميع الحلول. ومع ذلك، تتضمن بعض مسائل التراجع مسائل فرعية متداخلة. فعلى سبيل المثال، يمكن حل Word Break 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']

التعقيد الزمني للتراجع

يعتمد التعقيد الزمني للتراجع على عدد الأوراق في شجرة القرارات مضروبًا في العمل لكل عقدة. بالنسبة إلى المجموعات الجزئية: O(n × 2ⁿ). وبالنسبة إلى التباديل: O(n × n!). وبالنسبة إلى مجموع التركيبات: O(target/min_candidate ^ 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) العثور على جميع التركيبات أو التباديل أو المجموعات الجزئية، أو توليدها جميعًا. (2) أن تتضمن المسألة وضع عناصر أو أشخاص وفق قيود (مثل N-queens وSudoku). (3) أن تكون فضاء الحلول أسّيًا، لكن القيود تلغي معظم الفروع مبكرًا. (4) أن تحتاجوا إلى استكشاف مسارات في رسم بياني أو شبكة قد تعيد زيارة الحالات. عند ملاحظة هذه المؤشرات، استخدموا قالب Choose-Explore-Unchoose.

# 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

تحقق سريع

اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep من هذا الدرس.

مراجعة الدرس

في هذا الدرس تعلّمتم: يتكون قالب التراجع من ثلاث خطوات — Choose وExplore وUnchoose — تقابل إضافة الاختيار وإجراء الاستدعاء التكراري وإزالته، وتلغي شروط التقليم الفروع مبكرًا، وهي ما يجعل التراجع عمليًا مقارنة بالقوة الغاشمة، ويجب استعادة الحالة بالكامل بعد كل استدعاء تكراري لتجنب إفساد الفروع الشقيقة. بعد ذلك سنطبّق القالب لتوليد جميع المجموعات الجزئية ومجموعة القوى.

الأسئلة الشائعة

هل درس «قالب التراجع: اختر واستكشف وتراجع» مجاني؟

نعم — نص درس «قالب التراجع: اختر واستكشف وتراجع» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «قالب التراجع: اختر واستكشف وتراجع»؟

طبّق الهيكل الأساسي للتراجع ذي الخطوات الثلاث، وتتّبعه على مثال صغير، وحدّد مواضع إدراج شروط التقليم. تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.

كم من الوقت يستغرق درس «قالب التراجع: اختر واستكشف وتراجع»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. قالب التراجع: اختر واستكشف وتراجع
  2. المجموعات الجزئية ومجموعة القوى
  3. التبديلات والتوافيق
  4. مسألة الملكات N وانتشار القيود
← العودة إلى Coding Interview Prep