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 <= 302 <= candidates[i] <= 40- All elements of
candidatesare 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_sumexceeds the target, backtrack. - If
cur_sumequals the target, add the current combination to the result list.
Algorithm​
- Initialize an empty list
ansto store the results. - Define a recursive function
dfs(cur, cur_sum, idx)to explore combinations. - In
dfs, ifcur_sumexceeds the target, return. - If
cur_sumequals the target, addcurtoansand return. - Loop through candidates starting from
idx, and recursively calldfswith updatedcurandcur_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
dpwheredp[i]contains combinations that sum up toi. - Build the
dplist 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_dto map each candidate to its index. - Initialize a list
dpwith empty lists for all sums from 0 to the target. - For each sum
ifrom 1 to the target:
- Iterate through all previous sums
jfrom 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
dpwheredp[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
dpwith empty lists for all sums from 0 to the target. - For each candidate
c: - For each sum
ifromcto the target:
- If
iequalsc, 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.