قالب التراجع: اختر واستكشف وتراجع
طبّق الهيكل الأساسي للتراجع ذي الخطوات الثلاث، وتتّبعه على مثال صغير، وحدّد مواضع إدراج شروط التقليم.
قالب التراجع: اختر واستكشف وتراجع درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA 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) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «قالب التراجع: اختر واستكشف وتراجع»؟
طبّق الهيكل الأساسي للتراجع ذي الخطوات الثلاث، وتتّبعه على مثال صغير، وحدّد مواضع إدراج شروط التقليم. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «قالب التراجع: اختر واستكشف وتراجع»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- قالب التراجع: اختر واستكشف وتراجع
- المجموعات الجزئية ومجموعة القوى
- التبديلات والتوافيق
- مسألة الملكات N وانتشار القيود