Leetcode 40 - Combination Sum II
Understanding the Problem
The goal is to find all unique combinations in a collection of candidate numbers (candidates) where the candidate numbers sum up to a target number (target). Unlike Combination Sum I, each number in candidates may only be used once in the combination, and the input array may contain duplicate numbers. The solution set must not contain duplicate combinations.
To solve this, we can model the decision-making process using a Backtracking (DFS) tree over a sorted array. Sorting is crucial because it allows us to easily skip duplicate elements and prune unnecessary search paths.
- State Tracking: We use an index
idxto track our current position in the array, a dynamic listcurr_listto hold the combination we are building, and acurr_sumto monitor the running total. - Base Cases: 1. If
curr_sumequalstarget, a valid combination is found, so a copy ofcurr_listis appended to the final results. 2. Ifcurr_sumexceedstarget, we return immediately (pruning the branch). - Avoiding Duplicates: Within the loop, if an element is the same as the previous element (
candidates[i] == candidates[i-1]) and it is not the first element of the current recursive level (i > idx), we skip it. This ensures we don't start identical combination pathways. - Backtracking Mechanism: We add the element to
curr_list, recurse forward to the next index (i + 1), and then remove the element (curr_list.pop()) to clean up the state before the next iteration.
Solution Implementation
Code
from typing import List
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
result = []
def dfs(idx, curr_list, curr_sum):
if curr_sum == target:
result.append(curr_list.copy())
return
if curr_sum > target:
return
for i in range(idx, len(candidates)):
# Skip duplicate elements at the same depth level
if i > idx and candidates[i] == candidates[i-1]:
continue
# Pruning: early exit if the current element exceeds the remaining target
if curr_sum + candidates[i] > target:
break
curr_list.append(candidates[i])
# Move to i + 1 to ensure each element is used only once
dfs(i + 1, curr_list, curr_sum + candidates[i])
curr_list.pop() # Backtrack
dfs(0, [], 0)
return result
Time Complexity
Sorting the array takes \(O(n \log n)\) time. In the worst-case scenario (e.g., all elements are \(1\) and the target is \(n\)), the algorithm generates up to \(2^n\) state combinations. For every valid combination, creating a copy of the list takes \(O(k)\) time, where \(k\) is the length of the combination (bounded by \(n\)).
Overall time complexity — \(O\left(k \cdot 2^n\right)\)
Space Complexity
- Recursion Stack: The maximum depth of the recursion tree is bounded by \(n\) when picking elements one by one. This requires \(O(n)\) space.
- State Tracking: The
curr_listused to keep track of the current combination requires a maximum of \(O(n)\) auxiliary space.
Overall space complexity — \(O(n)\)
Additional Resources
- https://neetcode.io/problems/combination-target-sum-ii
- https://leetcode.com/problems/combination-sum-ii/