Combination Sum II - Backtracking Guide
Combination Sum II - Complete Backtracking Guide
Introduction
Combination Sum II is a classic backtracking problem that builds upon the foundation of Combination Sum I. The primary challenge lies in two specific constraints:
- No Reuse of Elements: Each number in the
candidatesarray may only be used once in a combination. - Unique Combinations: The solution set must not contain duplicate combinations.
This guide will walk you through the logic, algorithm, and implementation of solving this problem efficiently using backtracking.
Problem Statement
Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target.
Constraints:
- The same number may be chosen from
candidatesan unlimited number of times. - Note: The solution set must not contain duplicate combinations.
- Each number in
candidatesmay only be used once in the combination.
Key Differences from Combination Sum I
| Feature | Combination Sum I | Combination Sum II |
| :--- | :--- | :--- |
| Element Usage | Unlimited times | At most once |
| Duplicates | Allowed in input | Allowed in input, but output must be unique |
| Sorting Requirement | Optional | Mandatory for optimization |
The Backtracking Strategy
Backtracking is a depth-first search (DFS) approach where we explore all possible paths (combinations) recursively. If a path leads to a solution, we keep it. If it exceeds the target or leads to a dead end, we backtrack (undo the last choice).
The Critical Sorting Step
Before starting the recursion, we must sort the candidates array. This allows us to:
- Prune branches early (if the current number exceeds the remaining target, we can stop the loop).
- Implement the logic to skip duplicate numbers efficiently.
The Duplicate Skipping Logic
The most common point of confusion is how to skip duplicates without losing valid combinations.
The Rule: If candidates[i] == candidates[i-1], we skip candidates[i] only if i > startIndex.
i > startIndex: This ensures we don't skip the first occurrence of a number in the current recursive level. If we skip the first one, we might miss a valid combination starting with that number.i > 0: This ensures we don't compare the first element with a non-existent previous element.
Algorithm Steps
- Sort the input array.
- Define a recursive function
backtrack(startIndex, path, remainingSum):
- Base Case: If
remainingSum === 0, add the currentpathto the result. - Pruning: If
remainingSum < 0, return immediately (no need to proceed).
- Iterate from
startIndexto the end of the array:
- Check Target: If
candidates[i] > remainingSum,break(since array is sorted, subsequent numbers will be larger). - Skip Duplicates: If
i > startIndex && candidates[i] === candidates[i-1],continue. - Action: Add
candidates[i]topathand callbacktrack(i + 1, path, remainingSum - candidates[i]). - Backtrack: Remove the last element from
path(pop) to explore other possibilities.
Code Implementation (TypeScript)
function combinationSum2(candidates: number[], target: number): number[][] {
const result: number[][] = [];
// Sort to handle duplicates and enable pruning
candidates.sort((a, b) => a - b);
const backtrack = (
startIndex: number,
path: number[],
remainingSum: number
) => {
// Base Case: Found a valid combination
if (remainingSum === 0) {
result.push([...path]);
return;
}
for (let i = startIndex; i < candidates.length; i++) {
// Pruning: If current number exceeds remaining sum, stop
if (candidates[i] > remainingSum) {
break;
}
// Skip duplicates: Only skip if it's not the first element in this level
// and it's the same as the previous element
if (i > startIndex && candidates[i] === candidates[i - 1]) {
continue;
}
// Include candidates[i]
path.push(candidates[i]);
// Recurse: Move to next index (i + 1) because we can't reuse current element
backtrack(i + 1, path, remainingSum - candidates[i]);
// Backtrack: Remove the last element to try the next candidate
path.pop();
}
};
backtrack(0, [], target);
return result;
}
// Example Usage
const candidates = [10, 1, 2, 7, 6, 1, 5];
const target = 8;
console.log(combinationSum2(candidates, target));
// Output: [[1,1,6], [1,2,5], [1,7], [2,6]]Complexity Analysis
Time Complexity: $O(2^N)$
In the worst case, we have to explore all subsets of the array. Since we are pruning branches where candidates[i] > remainingSum, the actual runtime is often better than this, but it remains exponential in the size of the input.
Space Complexity: $O(N)$
- Recursion Stack: The depth of the recursion tree is at most the length of the array, $O(N)$.
- Result Storage: The space required to store the output is not counted towards space complexity in standard Big O notation, but it is $O(K imes N)$ where $K$ is the number of valid combinations.
Common Pitfalls
- Forgetting to Sort: Without sorting, the duplicate skipping logic fails, and you will generate duplicate combinations.
- Incorrect Duplicate Check: Using
i > 0instead ofi > startIndexwill cause you to skip valid combinations. For example, if the array is[1, 1, 2], skipping the second1at the root level (i=0) is fine, but skipping the second1at the next level (i=1) would be wrong if the first1wasn't chosen. - Modifying the Input Array: Never modify the input array directly inside the function unless you restore it, as it affects the caller's data.
- Infinite Loops: Ensure you pass
i + 1to the recursive call, noti, to ensure each element is used at most once.
Real-World Applications
- Resource Allocation: Distributing a fixed budget across different projects where each project can be selected at most once.
- Scheduling: Finding all possible time slots that fit a specific duration constraint without reusing the same resource slot.
- Inventory Management: Selecting a subset of items to reach a specific total weight or value without reusing the same inventory item.
Summary
Combination Sum II is a robust exercise in mastering backtracking nuances. The two most important takeaways are:
- Sorting the array first to enable pruning and duplicate handling.
- Correctly identifying when to skip duplicates using the
i > startIndexcondition.
By following the structure above, you can systematically break down the problem and ensure your solution is both correct and efficient.