Combination Sum
Question (LC.39)
Given an array of candidate numbers and a target, find all unique combinations from the candidate array that sum to the target.
Example
Input: candidates = [2, 3, 6, 7], target = 7
Output: [ [2, 2, 3], [7] ]
A candidate can be used unlimited number of times.
Analysis
Be careful with the input, it might contain duplicates.
Step 1 Remove duplicates
Step 2 Define DFS(parameter, return value, stop condition)
Step 3 Write DFS
Code
Main function
/*
How is this different from subsets?
1. dup
2. use each number unlimited number of times
3. stop at a target
*/
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> results = new ArrayList<>();
if (candidates == null || candidates.length == 0) {
return results;
}
Arrays.sort(candidates);
combDFS(results, candidates, target, 0, new ArrayList<Integer>());
return results;
}
DFS Search
private void combDFS( List<List<Integer>> results, int[] candidates,
int target, int startIndex, List<Integer> combination) {
// stop conditions
if (target < 0) {
return;
}
if (target == 0) {
results.add(new ArrayList<>(combination));
return;
}
// dfs
for (int i = startIndex; i < candidates.length; i++) {
combination.add(candidates[i]);
combDFS(results, candidates, target - candidates[i], i, combination);
combination.remove(combination.size() - 1);
}
}
We can improve a bit on the stopping conditions. We don't have to go down one more level until the target becomes negative. We can simply skip the candidates in the for loop that is bigger than the current target.
// dfs
for (int i = startIndex; i < candidates.length; i++) {
if (target < candidates[i]) {
break;
}
combination.add(candidates[i]);
combDFS(results, candidates, target - candidates[i], i, combination);
combination.remove(combination.size() - 1);
}
Combination Sums II
i+1 each element can only be used once
skip dup
Code
// stop condition
if (target == 0) {
results.add(new ArrayList<>(combination));
return;
}
// dfs
for (int i = startIndex; i < candidates.length; i++) {
// no need to stay in this loop, since the candidates array is sorted
// anything after this candidate is bigger
if (target < candidates[i]) {
break;
}
// not first iteration and check the previous candidate,
// we only want to skip the current candidate, so don't break just continue
if (i != startIndex && candidates[i] == candidates[i-1]) {
continue;
}
combination.add(candidates[i]);
combDFS(results, candidates, target - candidates[i], i + 1, combination);
combination.remove(combination.size() - 1);
}
Combination Sum III (LC.216)
Find all possible combinations of k numbers that add up to a target. Only numbers from 1 to 9 can be used and each combination should be a unique set of numbers.
Example
Always run examples FIRST. Running examples can give you another chance and read and understand the question better. It can also clarify the edge cases.
I: k = 3, target = 7
O: [[1,2,4]]
I: k = 3, target = 9
O: [[1,2,6], [1,3,5], [2,3,4]]
Analysis
We want to find all combinations 9 choose k, such that the combSum == target
.
Approach
1. parameter
(k, target, results, startNum, combination, combSum)
we can do k-1 or just use combination.size()
combSum is not necessary since we can do target - num
2. dfs
for num from startNum to n
previsit: combination.add(num), combSum[0] += num
dfs
postvisit: combination.remove(num), combSum[0] -= num
end for
3. stop condition (brute force then optimize)
if (k == 0)
check combSum == target
Code
private static final int N = 9;
public List<List<Integer>> combinationSum3(int k, int target) {
List<List<Integer>> results = new ArrayList<>();
if (k > N) {
return results;
}
combineDFS(k, target, results, 1, new ArrayList<>(), new int[] {0});
return results;
}
private void combineDFS(int k, int target, List<List<Integer>> results,
int startNum, List<Integer> combination, int[] combSum) {
// stop condition
if (combination.size() == k) {
if (combSum[0] == target) {
results.add(new ArrayList<>(combination));
}
return;
}
// dfs
for (int num = startNum; num <= N; num++) {
combination.add(num);
combSum[0] += num;
combineDFS(k, target, results, num + 1, combination, combSum);
combination.remove(combination.size() - 1);
combSum[0] -= num;
}
}
Optimization
Adding this simple if statement will boost the performance from 12 to 58 percentile.
if (combSum[0] > target) {
return;
}
We can also do the pruning inside the for loop (save one level/stack frame more efficient)
if (combSum[0] + num > target) {
break;
}
Combination Sum IV (LC.377)
Given an integer array with all positive numbers and no duplicates, find the number of possible combinations that add up to a positive integer target.
Example
I: nums = [1, 2, 3], target = 4
O: 7 because
The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
Analysis
I originally thought this is combination with repetition. But the example shows [1,1,2]
, [1,2,1]
, and [2,1,1]
are three different "combinations". In fact, under this definition, there's an infinite number of "combinations". For example, given a set [1,2,3]
, we can have {[1-3],[1-3],[1-3],[1-3], ...}
so 3x3x3x3x3...
number of combinations.
Brute Force Approach
find all combinations (where sum < target
)
add it to results if combSum == target
- parameter
(nums, target)
don't need startIndex, always start from index 0
- dfs
for i from o to n-1
dfs(nums, target - nums[i])
end for
- stop condition
if (target < 0)
return;
if (target == 0)
results++;
return;
Code
private int results = 0;
public int combinationSum4(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return results;
}
combineDFS(nums, target);
return results;
}
private void combineDFS(int[] nums, int target) {
// stop conditions
if (target < 0) {
return;
}
if (target == 0) {
results++;
return;
}
// dfs
for (int i = 0; i < nums.length; i++) {
combineDFS(nums, target - nums[i]);
}
}
But Time Limit Exceeded ٩ʕ•͡וʔ۶
Last executed input:
Expected answer: 181997601
Analysis 2
Draw the recursion tree and we can observe starting from [2]
has overlapping subproblems as starting from [1]
.
DFS(nums, 4)
DFS(nums, 3)
DFS(nums, 2)
DFS(nums, 1)
DFS(nums, 0)
end
end
DFS(nums, 0)
end
DFS(nums, 2) // what if we can memoize this
DP Approach
In addition to the overlapping subproblems, the question is only asking for returning a count instead of a collection of all combinations. Also, it is unreasonable to search for all 181997601 combinations.
DP Solution
1. define subproblem
DFS[i] is the number of combinations we have for target i
2. write recurrence
solve by prefix
DFS[n] += DFS[n - nums[x]] for x from 0 to nums.length-1
3. topo order
for n from 1 to target
4. base case
DP[negative targets] = 0
DP[0] = 1
5. return result
DFS[target]
Top-Down (Memoization)
public int combinationSum4(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[target + 1];
return combineDFS(nums, target, dp);
}
private int combineDFS(int[] nums, int target, int[] dp) {
// stop conditions
// move this inside for loop
if (target < 0) {
return 0;
}
// init base case for dp
if (target == 0) {
return 1;
}
// what if there are targets that we have actually 0 combinations
if (dp[target] != 0) {
return dp[target];
}
// dfs
for (int i = 0; i < nums.length; i++) {
dp[target] += combineDFS(nums, target - nums[i], dp);
}
// return value
return dp[target];
}
Now we can handle [1,2,3], 32
case. But [3,33,333], 10000
makes the program TLE again.
Updated Version (See comments above)
public int combinationSum4(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[target + 1];
Arrays.fill(dp, -1);
dp[0] = 1;
Arrays.sort(nums); // so we can break instead of continue
return combineDFS(nums, target, dp);
}
private int combineDFS(int[] nums, int target, int[] dp) {
// memoization
if (dp[target] != -1) {
return dp[target];
}
// dfs since we label unvisitd as -1
// we need to have a count that starts from 0
int count = 0;
for (int i = 0; i < nums.length; i++) {
int nextTarget = target - nums[i];
if (nextTarget < 0) {
break;
}
count += combineDFS(nums, nextTarget, dp);
}
// return value
dp[target] = count;
return dp[target];
}
Bottom-Up (Tabulation)
Simplicity is harder to achieve than complexity.
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
// init base case
dp[0] = 1;
Arrays.sort(nums); // so we can break instead of continue
// topo order
for (int i = 1; i <= target; i++) {
for (int num : nums) {
if (i - num < 0) {
break;
}
dp[i] += dp[i - num];
}
}
// return result
return dp[target];
}
Follow Up
What if negative numbers are allowed in the given array?
Then we need to add an extra constraint on the max length of a combination. Otherwise, we can run into the an infinite long combination such as [...,-1,-1,-1,1,1,1,...]
.
Reference
Combination Sum IV by Grand Yang