Backtracking explores everything; pruning makes it explore only what CAN still work.
“Combination sum / subsets with constraints” → sort + cut branches early
Pattern 1: Combination Sum (candidates sorted, prune on remaining)
public List<List<Integer>> comboSum(int[] c, int target) {
Arrays.sort(c); // enables the ✂ rule
List<List<Integer>> res = new ArrayList<>();
dfs(c, 0, target, new ArrayList<>(), res);
return res;
}
void dfs(int[] c, int i, int rem,
List<Integer> cur, List<List<Integer>> res) {
if (rem == 0) { res.add(new ArrayList<>(cur)); return; }
for (int j = i; j < c.length; j++) {
if (c[j] > rem) break; // ✂ PRUNE: rest are bigger
cur.add(c[j]);
dfs(c, j, rem - c[j], cur, res);
cur.remove(cur.size() - 1);
}
}def combo_sum(candidates, target):
candidates.sort() # enables the ✂ rule
res, cur = [], []
def dfs(i, rem):
if rem == 0:
res.append(cur[:])
return
for j in range(i, len(candidates)):
if candidates[j] > rem:
break # ✂ PRUNE: rest are bigger
cur.append(candidates[j])
dfs(j, rem - candidates[j])
cur.pop()
dfs(0, target)
return resvector<vector<int>> res;
vector<int> cur;
void dfs(vector<int>& c, int i, int rem) {
if (rem == 0) { res.push_back(cur); return; }
for (int j = i; j < (int)c.size(); j++) {
if (c[j] > rem) break; // ✂ PRUNE: rest are bigger
cur.push_back(c[j]);
dfs(c, j, rem - c[j]);
cur.pop_back();
}
}
vector<vector<int>> comboSum(vector<int> c, int target) {
sort(c.begin(), c.end());
dfs(c, 0, target);
return res;
}function comboSum(candidates, target) {
candidates.sort((a, b) => a - b); // enables the ✂ rule
const res = [],
cur = [];
const dfs = (i, rem) => {
if (rem === 0) {
res.push([...cur]);
return;
}
for (let j = i; j < candidates.length; j++) {
if (candidates[j] > rem) break; // ✂ PRUNE: rest are bigger
cur.push(candidates[j]);
dfs(j, rem - candidates[j]);
cur.pop();
}
};
dfs(0, target);
return res;
}Three classic prunes:
| Prune | Where it fires | Why valid |
|---|---|---|
c[j] > rem → break | loop start | array sorted ⇒ all later candidates also too big |
i (not j+1) recursion | recursive call | combinations not permutations — kills duplicate orderings |
skip duplicates if j > i && c[j]==c[j-1] continue | loop start | identical values at same level produce identical subtrees |
Pattern 2: Subsets with Sum Bound
Watch the +2 branch get fully explored while the pruner kills junk before it’s born. Press ▶.
⚠️ Animation & Content Notice
The animation work is not fully finished — some animations may have slight errors.
If there is a major error in the content or if the animation or content is difficult to understand, please contact us at rayyancodingschool@gmail.com.
Subset Sum with Pruning
Find subsets of [2,3,5] that sum to a target (8) using backtracking with pruning.
Each node is a take/skip decision on the next candidate. As soon as a partial sum exceeds the target (or, with sorted candidates, exceeds what's left), the whole branch is cut — sums only grow, so nothing later can rescue it. Sorting enables a total prune; the exponential tree collapses to almost nothing.
1
dfs(i, remaining):
2
if remaining == 0: record subset ✓
3
if remaining < 0 or i == n: return # PRUNE / leaf
4
# only if candidates sorted can we prune harder:
5
if candidates[i] > remaining: return # ✂ no future fits
6
dfs(i+1, remaining - c[i]) # take
7
dfs(i+1, remaining) # skip
void subsets(int[] nums, int i, int sum, int limit,
List<Integer> cur, List<List<Integer>> res) {
if (sum > limit) return; // ✂ whole branch is dead
if (i == nums.length) { res.add(new ArrayList<>(cur)); return; }
cur.add(nums[i]); // take
subsets(nums, i+1, sum + nums[i], limit, cur, res);
cur.remove(cur.size() - 1); // BACKTRACK
subsets(nums, i+1, sum, limit, cur, res); // skip
}def bounded_subsets(nums, limit):
res, cur = [], []
def dfs(i, total):
if total > limit:
return # ✂ whole branch is dead
if i == len(nums):
res.append(cur[:])
return
cur.append(nums[i]) # take
dfs(i + 1, total + nums[i])
cur.pop() # BACKTRACK
dfs(i + 1, total) # skip
dfs(0, 0)
return resvoid subsets(vector<int>& nums, int i, int sum, int limit,
vector<int>& cur, vector<vector<int>>& res) {
if (sum > limit) return; // ✂ whole branch is dead
if (i == (int)nums.size()) { res.push_back(cur); return; }
cur.push_back(nums[i]); // take
subsets(nums, i+1, sum + nums[i], limit, cur, res);
cur.pop_back(); // BACKTRACK
subsets(nums, i+1, sum, limit, cur, res); // skip
}function boundedSubsets(nums, limit) {
const res = [],
cur = [];
const dfs = (i, total) => {
if (total > limit) return; // ✂ whole branch is dead
if (i === nums.length) {
res.push([...cur]);
return;
}
cur.push(nums[i]); // take
dfs(i + 1, total + nums[i]);
cur.pop(); // BACKTRACK
dfs(i + 1, total); // skip
};
dfs(0, 0);
return res;
}Even stronger: sort first, then prune when sum + suffixSum(i) can never reach a minimum bound — you can prove entire levels unreachable without visiting them.
Every prune must be provably safe: “nothing below this node can succeed.” Guessy cuts lose answers.
Common Mistakes
- Pruning before sorting (the
breaktrick silently skips valid answers). - Cutting branches that could still succeed (
rem == 0vsrem <= 0confusion). - Duplicate results from reusing elements or unsorted candidate arrays.
- Deep-copying into results incorrectly (
res.add(cur)stores a reference!).
Complexity
| Metric | No pruning | With pruning |
|---|---|---|
| Time | O(2^n) always | O(2^n) worst, typically far less |
| Space | O(n) depth | O(n) |
Premium Content
Unlock Backtracking & Pruning and all premium lessons with a subscription.
From ₹199.99/year — See plans