From b33f1d52dceb80da149541142a4a9dfa1314be51 Mon Sep 17 00:00:00 2001 From: Oleksii Trekhleb Date: Sat, 30 Jun 2018 10:19:14 +0300 Subject: [PATCH] Add "Combination Sum" backtracking algorithm. --- README.md | 2 + src/algorithms/sets/combination-sum/README.md | 60 +++++++++++++++++ .../__test__/combinationSum.test.js | 24 +++++++ .../sets/combination-sum/combinationSum.js | 65 +++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 src/algorithms/sets/combination-sum/README.md create mode 100644 src/algorithms/sets/combination-sum/__test__/combinationSum.test.js create mode 100644 src/algorithms/sets/combination-sum/combinationSum.js diff --git a/README.md b/README.md index c1cd52a1d4..fddf28a433 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/algorithms/sets/combination-sum/README.md b/src/algorithms/sets/combination-sum/README.md new file mode 100644 index 0000000000..cb14f1bae8 --- /dev/null +++ b/src/algorithms/sets/combination-sum/README.md @@ -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/) diff --git a/src/algorithms/sets/combination-sum/__test__/combinationSum.test.js b/src/algorithms/sets/combination-sum/__test__/combinationSum.test.js new file mode 100644 index 0000000000..7b196bf2a2 --- /dev/null +++ b/src/algorithms/sets/combination-sum/__test__/combinationSum.test.js @@ -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([]); + }); +}); diff --git a/src/algorithms/sets/combination-sum/combinationSum.js b/src/algorithms/sets/combination-sum/combinationSum.js new file mode 100644 index 0000000000..c36b9f8a22 --- /dev/null +++ b/src/algorithms/sets/combination-sum/combinationSum.js @@ -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); +}