Recursion and Backtracking — Named Patterns¶
সাতটা pattern, এমনভাবে সাজানো যেন প্রতিটা আগেরটার উপর দাঁড়ায়। প্রতিটা pattern-এর জন্য: trigger words, core idea, thinking tweak, একটা ছোট worked example, কী থেকে inherit করে, আর practice problem।
Pattern 1: Decrease-and-conquer¶
- Trigger words: "n in terms of n-1", "একটা element process করো তারপর বাকিটা", linear structure (string, list, countdown)।
- Core idea: একটা টুকরো খুলে নাও, বাকিটার উপর recurse করো, জোড়া লাগাও।
- Inherits from: loops — decrease-by-one যেকোনো recursion-এর একটা loop-যমজ আছে; এই pattern সেই recursive ভাষাটা শেখায়, যেটা loop-এ আর না কুলালে লাগবে।
- Thinking tweak:
n-এর উত্তরটাn-1-এর উত্তর দিয়ে বর্ণনা করো, তারপরn-1কীভাবে solve হয় সেটা ভাবা বন্ধ করো (leap of faith)।
Worked example — digit-এর যোগফল:
472-এর উপর dry run: 2 + digit_sum(47) = 2 + (7 + digit_sum(4)) = 2 + (7 + 4) = 13।
- Problems: Fibonacci Number https://leetcode.com/problems/fibonacci-number/, Reverse String https://leetcode.com/problems/reverse-string/, Climbing Stairs https://leetcode.com/problems/climbing-stairs/।
Pattern 2: Divide-and-conquer¶
- Trigger words: "halves", "merge", "O(n log n) expected", "independent subparts"।
- Core idea: input-কে 2+ INDEPENDENT টুকরোয় ভাগ করো, প্রতিটা recursively solve করো, result-গুলো জোড়া লাগাও।
- Inherits from: decrease-and-conquer, upgrade হয়ে — 1 করে ছোট করার (depth n) বদলে অর্ধেক করে ছোট করো (depth log n)।
- Thinking tweak: speedup-টা থাকে COMBINE step-এ: অর্ধেক জোড়া লাগাতে O(n) লাগলে আর depth log n হলে, মোট হয় O(n log n)।
Worked example — fast power, O(log n):
def power(x, n):
if n == 0:
return 1
half = power(x, n // 2)
return half * half if n % 2 == 0 else half * half * x
power(3, 4)-এর dry run: power(3,2) → power(3,1) → power(3,0)=1 → উপরে: 1·1·3=3 → 3·3=9 → 9·9=81। Exponent 4-এর জন্য তিনটা recursive call।
- Problems: Pow(x, n) https://leetcode.com/problems/powx-n/, Merge Sort (concept — নিজে implement করো, তুলনা করো https://leetcode.com/problems/sort-an-array/-এর সাথে), Maximum Subarray https://leetcode.com/problems/maximum-subarray/ (D&C variant)।
Pattern 3: Subsets — the include/exclude branch¶
- Trigger words: "all subsets", "power set", "প্রতিটা item নেওয়া বা না-নেওয়ার সব combination", "2^n"।
- Core idea: item-গুলোতে বাঁ থেকে ডানে হাঁটো; প্রতিটা item-এর জন্য ঠিক দুটো recursive call করো — একটায় নাও, একটায় বাদ দাও। 2 × 2 × ... × 2 leaf-গুলোই হলো 2^n subset।
- Inherits from: decrease-and-conquer (এখনো একবারে একটা item) — কিন্তু এবার single call-এর বদলে BRANCHING, যেটা call-এর লাইনটাকে call-এর tree বানিয়ে দেয়।
- Thinking tweak: "কোন কোন subset আছে?" ভাবা বন্ধ করো, ভাবো "এই একটা item-এর জন্য আমার option কী?" Locally দুটো option; globally সব subset বেরিয়ে আসে।
Worked example — [1, 2]-এর subsets:
def subsets(nums):
out = []
def go(i, path):
if i == len(nums):
out.append(path[:]) # snapshot!
return
path.append(nums[i]); go(i + 1, path); path.pop() # include
go(i + 1, path) # exclude
go(0, [])
return out
subsets([1, 2]) # [[1, 2], [1], [2], []]
Dry run: include 1 → include 2 → record [1,2] → pop → exclude 2 → record [1] → pop 1 → exclude 1 → include 2 → record [2] → pop → exclude → record []। (পুরো frame-by-frame আছে visual-explanation.md-তে।)
- Problems: Subsets https://leetcode.com/problems/subsets/, Subsets II https://leetcode.com/problems/subsets-ii/ (sort করো, তারপর EXCLUDE পাশে সমান প্রতিবেশীদের skip করো)।
Pattern 4: Permutations — choose each remaining¶
- Trigger words: "all orderings", "arrangements", "n!", "প্রতিটা item ঠিক একবার করে ব্যবহার করা প্রতিটা sequence"।
- Core idea: উত্তরটা slot ধরে ধরে বানাও; প্রতিটা slot-এ, এখনো ব্যবহার-না-হওয়া প্রতিটা item-এর উপর loop চালাও, বসাও, recurse করো, সরাও।
- Inherits from: subsets-এর branching — কিন্তু branching factor এখন "সব unused item" (n, তারপর n-1, ...), fixed 2 না, আর একটা used-tracker লাগে (একটা set — hello,
../05-hashing/-এর seen-set pattern)। - Thinking tweak: recursion ঠিক করে তুমি কোন SLOT ভরছ; loop ঠিক করে তাতে কী যাচ্ছে। দুটো dimension, পরিষ্কারভাবে আলাদা।
Worked example — [1, 2, 3]-এর permutations:
def permutations(nums):
out, used = [], set()
def go(path):
if len(path) == len(nums):
out.append(path[:])
return
for x in nums:
if x in used:
continue
used.add(x); path.append(x) # choose
go(path) # explore
path.pop(); used.discard(x) # un-choose
go([])
return out
Dry run-এর শুরু: choose 1 → choose 2 → choose 3 → record [1,2,3] → unwind করে [1] → choose 3 → choose 2 → record [1,3,2] → ... মোট 6টা result (3! = 6)।
- Problems: Permutations https://leetcode.com/problems/permutations/, Permutations II https://leetcode.com/problems/permutations-ii/, Letter Combinations of a Phone Number https://leetcode.com/problems/letter-combinations-of-a-phone-number/ (digit প্রতি choose, used-set লাগে না)।
Pattern 5: Combinations with pruning¶
- Trigger words: "combinations summing to target", "choose k of n", "আলাদা order-এ duplicate চলবে না", এমন time limit যেটা brute force উড়িয়ে দিত।
- Core idea: subsets-এর মতো, কিন্তু (a) একটা START INDEX চাপাও যাতে প্রতিটা combination একটাই canonical order-এ তৈরি হয়, আর (b) যে মুহূর্তে কোনো branch প্রমাণসহ ব্যর্থ, সেটা কেটে ফেলো।
- Inherits from: subsets (branch structure) +
../05-hashing/-এর complement thinking ("remaining target"-টাই সেই complement যেটা তোমার এখনো দরকার)। - Thinking tweak: প্রতিটা node-এ জিজ্ঞেস করো "এই branch-এর কোনো descendant কি এখনো জিততে পারে?" না হলে — সাথে সাথে return। ভালো prune পুরো subtree মুছে দেয়, একটা একটা leaf না।
Worked example — [2, 3, 5] থেকে 8 sum-এর combinations (Combination Sum, নিজের ভাষায়: সংখ্যা বাছো, reuse চলবে, target-এ ঠিকঠাক পৌঁছাও):
def combination_sum(cands, target):
cands.sort()
out = []
def go(start, path, remaining):
if remaining == 0:
out.append(path[:]); return
for i in range(start, len(cands)):
if cands[i] > remaining: # PRUNE: sorted, so all later fail too
break
path.append(cands[i])
go(i, path, remaining - cands[i]) # i, not i+1: reuse allowed
path.pop()
go(0, [], target)
return out
combination_sum([2, 3, 5], 8) # [[2,2,2,2], [2,3,3], [3,5]]
Dry run-এর highlight: path=[2,2,2], remaining=2 → choose 2 → remaining 0 → record; remaining=2-তে ফিরে এসে candidate 3 trigger করে 3 > 2 → break এক কোপে 3-branch আর 5-branch দুটোকেই মেরে ফেলে। এটাই pruning-এর ভাড়া শোধ করা।
- Problems: Combination Sum https://leetcode.com/problems/combination-sum/, Combination Sum II https://leetcode.com/problems/combination-sum-ii/, Combinations https://leetcode.com/problems/combinations/।
Pattern 6: Constraint backtracking (N-Queens style)¶
- Trigger words: "place items so no two conflict", "board/grid validly ভরো", "satisfy all constraints", puzzles।
- Core idea: প্রতি level-এ একটা decision (প্রতি row-তে একটা queen, প্রতি step-এ একটা cell); recurse করার আগে CHECK করো partial state এখনো legal কি না; dead end-এ undo করে পরের option try করো।
- Inherits from: permutations (queen-দের column-গুলোই একটা permutation!) + pruning (legality check-টা আসলে প্রতিটা node-এ apply করা একটা prune)।
- Thinking tweak: validate করো তাড়াতাড়ি — কোনো partial placement নিয়ম ভাঙার মুহূর্তেই reject করো, পুরো candidate বানিয়ে শেষে test করার বদলে। Early check tree-টাকে n^n থেকে feasibility-র দিকে ছোট করে আনে।
Worked example — row r-এর queen কি column c-তে বসতে পারে? Attacked line-গুলো set-এ track করো (আবার সেই seen-set pattern):
cols, diag1, diag2 = set(), set(), set()
# queen at (r, c) controls: column c, diagonal r-c, anti-diagonal r+c
def safe(r, c):
return c not in cols and (r - c) not in diag1 and (r + c) not in diag2
4x4-এর উপর dry run: queen আছে (0,0) আর (1,2)-তে → row 2-র candidate: c=0 col-blocked, c=1 কি (1,2)-র diag-এ পড়ে? r-c=1 vs 1-2=-1 না, r+c=3 vs 1+2=3 হ্যাঁ blocked, c=2 col-blocked, c=3 কি (1,2)-র anti-diag? 2+3=5 না, কিন্তু (0,0)-র diag: 2-3=-1 vs 0... safe? 2-3=-1, 0-0=0, 1-2=-1 → (1,2)-র কারণে blocked। চারটাই fail → backtrack (ঠিক এই dead end-টাই visual-explanation.md-তে animate করা)। পুরো solver আছে implementation.py-তে।
- Problems: N-Queens https://leetcode.com/problems/n-queens/, Sudoku Solver https://leetcode.com/problems/sudoku-solver/, Word Search https://leetcode.com/problems/word-search/, CSES "Chessboard and Queens" — https://cses.fi/problemset/।
Pattern 7: Memoization — the bridge to dynamic programming¶
- Trigger words: "overlapping subproblems", "recursion tree-তে node repeat হয়", "count the ways" ("list the ways" না), এমন recursion যেটা সঠিক কিন্তু খুব slow।
- Core idea:
f(args)compute করার আগে একটা cache check করো; compute করার পর সেখানে store করো। প্রতিটা আলাদা subproblem ঠিক একবার solve হয়। - Inherits from: recursion tree-র ছবি +
../05-hashing/-এর hash-map lookup — cache-টা আসলে call-এর argument দিয়ে key করা একটা dict। - Thinking tweak: জিজ্ঞেস করো "উত্তরটা কি পুরোপুরি argument-গুলো দিয়েই নির্ধারিত?" হ্যাঁ হলে (pure function) আর argument repeat করলে, memoize করো। Listing-all problem-গুলো (subsets, permutations) লাভ পায় না — তাদের output-গুলো সব আলাদা; counting/optimum problem-গুলো পায়।
Worked example — fib, তিন লাইনের বদলে exponential থেকে linear:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
fib(5)-এর dry run: fib(0..5) প্রতিটা একবার করে compute হয় = 6টা আসল computation; naive version n=5-এ 15টা call করে আর n=30-এ ~2.7 million। Code-এর আকার একই, শুধু dict-আকৃতির memory লাগানো।
Manual version (যাতে জাদুটা দেখা যায়):
def fib_memo(n, cache={}):
if n in cache:
return cache[n]
if n <= 1:
return n
cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
return cache[n]
এটাই "top-down DP"। এটাকে উল্টে bottom-up table-ভরা একটা loop বানাও — তুমি পৌঁছে গেছ ../12-dynamic-programming/-এ; ওই পুরো section এই pattern-এরই বড় হয়ে ওঠা রূপ।
- Problems: Climbing Stairs https://leetcode.com/problems/climbing-stairs/, Fibonacci Number https://leetcode.com/problems/fibonacci-number/, তারপর graduate করো House Robber https://leetcode.com/problems/house-robber/-এ, DP section-এ।
The inheritance map¶
loops
│
▼
decrease-and-conquer ──► divide-and-conquer (shrink by half)
│
▼ (branch instead of single call)
subsets (include/exclude)
│
├──► permutations (branch = all unused; needs seen-set from 05-hashing)
│ │
│ ▼
│ constraint backtracking (permutations + early validity pruning)
│
└──► combinations with pruning (subsets + start index + cut hopeless branches)
recursion tree with repeats ──► memoization ──► ../12-dynamic-programming/
পরের ধাপ: implementation.py চালাও, তারপর খোলো problems/README.md।