Combination Sum(LeetCode)
Problem Statement​
Given an array of distinct integers candidates
and a target integer target
, return a list of all unique combinations of candidates
where the chosen numbers
sum to target
. You may return the combinations in any order.
The same number may be chosen from candidates
an unlimited number of times. Two combinations are unique if the
frequency
of at least one of the chosen numbers is different.
The test cases are generated such that the number of unique combinations that sum up to target
is less than 150
combinations for the given input.
Examples​
Example 1:
Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
Explanation:
2 and 3 are candidates, and 2 + 2 + 3 = 7. Note that 2 can be used multiple times.
7 is a candidate, and 7 = 7.
These are the only two combinations.
Example 2:
Input: candidates = [2,3,5], target = 8
Output: [[2,2,2,2],[2,3,3],[3,5]]
Example 3:
Input: candidates = [2], target = 1
Output: []
Constraints​
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
- All elements of
candidates
are distinct. 1 <= target <= 40
Solution​
The Combination Sum problem involves finding all unique combinations in a list of candidates where the candidate numbers sum to a given target. Each number in the list may be used an unlimited number of times. Below, we discuss three approaches to solve this problem: DFS (Backtracking), Dynamic Programming (Slow), and Dynamic Programming (Fast).
Approach 1: DFS (Backtracking)​
Explanation:​
- Use a recursive function to explore all possible combinations.
- Track the current combination (
cur
) and its sum (cur_sum
). - If
cur_sum
exceeds the target, backtrack. - If
cur_sum
equals the target, add the current combination to the result list.
Algorithm​
- Initialize an empty list
ans
to store the results. - Define a recursive function
dfs(cur, cur_sum, idx)
to explore combinations. - In
dfs
, ifcur_sum
exceeds the target, return. - If
cur_sum
equals the target, addcur
toans
and return. - Loop through candidates starting from
idx
, and recursively calldfs
with updatedcur
andcur_sum
. - Start the DFS with an empty combination, a sum of 0, and starting index 0.
- Return the list
ans
.
Implementation​
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
ans = []
n = len(candidates)
def dfs(cur, cur_sum, idx):
if cur_sum > target: return
if cur_sum == target:
ans.append(cur)
return
for i in range(idx, n):
dfs(cur + [candidates[i]], cur_sum + candidates[i], i)
dfs([], 0, 0)
return ans
Complexity Analysis​
- Time complexity: O(N^(M/min_cand + 1)), where N is the number of candidates, M is the target, and min_cand is the smallest candidate.
- Space complexity: O(M/min_cand), as the recursion depth can go up to the target divided by the smallest candidate.
Approach 2: Dynamic Programming (Slow)​
Explanation:​
- Use a list
dp
wheredp[i]
contains combinations that sum up toi
. - Build the
dp
list by iterating through all possible sums up to the target. - For each sum, update the combinations by considering each candidate.
Algorithm​
- Create a dictionary
idx_d
to map each candidate to its index. - Initialize a list
dp
with empty lists for all sums from 0 to the target. - For each sum
i
from 1 to the target:
- Iterate through all previous sums
j
from 0 toi-1
. - For each combination in
dp[j]
, update combinations indp[i]
. - Add combinations directly from candidates if they equal
i
.
- Return the list
dp[-1]
.
Implementation​
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
idx_d = {val: idx for idx, val in enumerate(candidates)}
n = len(candidates)
dp = [[] for _ in range(target + 1)]
for i in range(1, target + 1):
for j in range(i):
for comb in dp[j]:
start_idx = idx_d[comb[-1]]
for val in candidates[start_idx:]:
if val + j == i:
dp[i].append(comb + [val])
for candidate in candidates:
if candidate == i:
dp[i].append([candidate])
return dp[-1]
Complexity Analysis​
- Time complexity: O(MMM*N), where N is the number of candidates and M is the target.
- Space complexity: O(M*M), due to the storage of combinations for each sum up to the target.
Approach 3: Dynamic Programming (Fast)​
Explanation:​
- Use a list
dp
wheredp[i]
contains combinations that sum up toi
. - For each candidate, update the combinations for all possible sums up to the target.
Algorithm​
- Initialize a list
dp
with empty lists for all sums from 0 to the target. - For each candidate
c
: - For each sum
i
fromc
to the target:
- If
i
equalsc
, add[c]
todp[i]
. - For each combination in
dp[i - c]
, addcomb + [c]
todp[i]
.
- Return the list
dp[-1]
.
Implementation​
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
dp = [[] for _ in range(target + 1)]
for c in candidates:
for i in range(c, target + 1):
if i == c:
dp[i].append([c])
for comb in dp[i - c]:
dp[i].append(comb + [c])
return dp[-1]
Complexity Analysis​
- Time complexity: O(MMN), where N is the number of candidates and M is the target.
- Space complexity: O(M*M), due to the storage of combinations for each sum up to the target.
Conclusion​
In solving the Combination Sum problem:
- DFS (Backtracking) is simple and intuitive but can be slow for large inputs due to its exponential time complexity.
- Dynamic Programming (Slow) uses a structured approach but has higher time complexity (O(MMM*N)) and memory usage.
- Dynamic Programming (Fast) is more efficient (O(MMN)), making it suitable for larger inputs, though it still uses significant memory.
Choose DFS (Backtracking) for smaller datasets, DP (Slow) for a structured approach within manageable limits, and DP (Fast) for larger datasets where efficiency is critical.