Skip to content

Commit

Permalink
Add "Combination Sum" backtracking algorithm.
Browse files Browse the repository at this point in the history
  • Loading branch information
trekhleb committed Jun 30, 2018
1 parent b41cffe commit b33f1d5
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ a set of rules that precisely define a sequence of operations.
* `A` [Shortest Common Supersequence](src/algorithms/sets/shortest-common-supersequence) (SCS)
* `A` [Knapsack Problem](src/algorithms/sets/knapsack-problem) - "0/1" and "Unbound" ones
* `A` [Maximum Subarray](src/algorithms/sets/maximum-subarray) - "Brute Force" and "Dynamic Programming" (Kadane's) versions
* `A` [Combination Sum](src/algorithms/sets/combination-sum) - find all combinations that form specific sum
* **Strings**
* `A` [Levenshtein Distance](src/algorithms/string/levenshtein-distance) - minimum edit distance between two sequences
* `B` [Hamming Distance](src/algorithms/string/hamming-distance) - number of positions at which the symbols are different
Expand Down Expand Up @@ -156,6 +157,7 @@ different path of finding a solution. Normally the DFS traversal of state-space
* `A` [Hamiltonian Cycle](src/algorithms/graph/hamiltonian-cycle) - Visit every vertex exactly once
* `A` [N-Queens Problem](src/algorithms/uncategorized/n-queens)
* `A` [Knight's Tour](src/algorithms/uncategorized/knight-tour)
* `A` [Combination Sum](src/algorithms/sets/combination-sum) - find all combinations that form specific sum
* **Branch & Bound** - remember the lowest-cost solution found at each stage of the backtracking
search, and use the cost of the lowest-cost solution found so far as a lower bound on the cost of
a least-cost solution to the problem, in order to discard partial solutions with costs larger than the
Expand Down
60 changes: 60 additions & 0 deletions src/algorithms/sets/combination-sum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Combination Sum Problem

Given a **set** of candidate numbers (`candidates`) **(without duplicates)** and
a target number (`target`), find all unique combinations in `candidates` where
the candidate numbers sums to `target`.

The **same** repeated number may be chosen from `candidates` unlimited number
of times.

**Note:**

- All numbers (including `target`) will be positive integers.
- The solution set must not contain duplicate combinations.

## Examples

```
Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
[7],
[2,2,3]
]
```

```
Input: candidates = [2,3,5], target = 8,
A solution set is:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
```

## Explanations

Since the problem is to get all the possible results, not the best or the
number of result, thus we don’t need to consider DP (dynamic programming),
backtracking approach using recursion is needed to handle it.

Here is an example of decision tree for the situation when `candidates = [2, 3]` and `target = 6`:

```
0
/ \
+2 +3
/ \ \
+2 +3 +3
/ \ / \ \
+2 ✘ ✘ ✘ ✓
/ \
✓ ✘
```

## References

- [LeetCode](https://leetcode.com/problems/combination-sum/description/)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import combinationSum from '../combinationSum';

describe('combinationSum', () => {
it('should find all combinations with specific sum', () => {
expect(combinationSum([1], 4)).toEqual([
[1, 1, 1, 1],
]);

expect(combinationSum([2, 3, 6, 7], 7)).toEqual([
[2, 2, 3],
[7],
]);

expect(combinationSum([2, 3, 5], 8)).toEqual([
[2, 2, 2, 2],
[2, 3, 3],
[3, 5],
]);

expect(combinationSum([2, 5], 3)).toEqual([]);

expect(combinationSum([], 3)).toEqual([]);
});
});
65 changes: 65 additions & 0 deletions src/algorithms/sets/combination-sum/combinationSum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @param {number[]} candidates - candidate numbers we're picking from.
* @param {number} remainingSum - remaining sum after adding candidates to currentCombination.
* @param {number[][]} finalCombinations - resulting list of combinations.
* @param {number[]} currentCombination - currently explored candidates.
* @param {number} startFrom - index of the candidate to start further exploration from.
* @return {number[][]}
*/
function combinationSumRecursive(
candidates,
remainingSum,
finalCombinations = [],
currentCombination = [],
startFrom = 0,
) {
if (remainingSum < 0) {
// By adding another candidate we've gone below zero.
// This would mean that last candidate was not acceptable.
return finalCombinations;
}

if (remainingSum === 0) {
// In case if after adding the previous candidate out remaining sum
// became zero we need to same current combination since it is one
// of the answer we're looking for.
finalCombinations.push(currentCombination.slice());

return finalCombinations;
}

// In case if we haven't reached zero yet let's continue to add all
// possible candidates that are left.
for (let candidateIndex = startFrom; candidateIndex < candidates.length; candidateIndex += 1) {
const currentCandidate = candidates[candidateIndex];

// Let's try to add another candidate.
currentCombination.push(currentCandidate);

// Explore further option with current candidate being added.
combinationSumRecursive(
candidates,
remainingSum - currentCandidate,
finalCombinations,
currentCombination,
candidateIndex,
);

// BACKTRACKING.
// Let's get back, exclude current candidate and try another ones later.
currentCombination.pop();
}

return finalCombinations;
}

/**
* Backtracking algorithm of finding all possible combination for specific sum.
*
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
export default function combinationSum(candidates, target) {
return combinationSumRecursive(candidates, target);
}

0 comments on commit b33f1d5

Please sign in to comment.