From 3b9a3580d8dca2e1ba1c654e96edf48c74500b50 Mon Sep 17 00:00:00 2001 From: casca <8927157+casca@users.noreply.github.com> Date: Thu, 30 Jan 2020 21:31:04 +0100 Subject: [PATCH 1/3] Refactor combinationSum to an iterative algo - Refactor combinationSum to use an iterative algorithm (to avoid stack overflows). - Ignore candidates equal to zero (to avoid loops). --- .../sets/combination-sum/combinationSum.js | 100 ++++++++---------- 1 file changed, 43 insertions(+), 57 deletions(-) diff --git a/src/algorithms/sets/combination-sum/combinationSum.js b/src/algorithms/sets/combination-sum/combinationSum.js index dd396a8c..d37c521c 100644 --- a/src/algorithms/sets/combination-sum/combinationSum.js +++ b/src/algorithms/sets/combination-sum/combinationSum.js @@ -1,65 +1,51 @@ /** - * @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 the last candidate was not acceptable. - return finalCombinations; - } - - if (remainingSum === 0) { - // If after adding the previous candidate our remaining sum - // became zero - we need to save the current combination since it is one - // of the answers we're looking for. - finalCombinations.push(currentCombination.slice()); - - return finalCombinations; - } - - // 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. + * Iterative algorithm to find all combinations (repetitions allowed) + * that sum up to a given number (target) using elements + * from a set of positive integers (candidates). * * @param {number[]} candidates * @param {number} target * @return {number[][]} */ export default function combinationSum(candidates, target) { - return combinationSumRecursive(candidates, target); + const combinations = []; + + const nonZeroCandidates = Array.from(new Set(candidates.filter(c => c > 0).slice().reverse())); + const stack = nonZeroCandidates + .map((candidate, index) => ({ candidateIndex: index, sum: candidate, prev: null })); + + while (stack.length > 0) { + const node = stack.pop(); + + if (node.sum === target) { + /* + If the cumulative sum matches the target value + then we build the corresponding candidates combination + by traversing the current branch back to its root... + */ + const combination = []; + let currentNode = node; + while (currentNode !== null) { + const candidate = nonZeroCandidates[currentNode.candidateIndex]; + combination.push(candidate); + currentNode = currentNode.prev; + } + combinations.push(combination); + } else if (node.sum < target) { + /* + ...otherwise we combine the current branch + with any other candidate (as long as it is + less or equal than the current candidate) + and evaluate the new branches. + */ + for (let i = node.candidateIndex; i < nonZeroCandidates.length; i += 1) { + stack.push({ + candidateIndex: i, + sum: node.sum + nonZeroCandidates[i], + prev: node, + }); + } + } + } + return combinations; } From f6408ffb57a13a5f5acdc8dc121f38fda1b6341b Mon Sep 17 00:00:00 2001 From: casca <8927157+casca@users.noreply.github.com> Date: Thu, 30 Jan 2020 21:32:49 +0100 Subject: [PATCH 2/3] Add test cases mentioned in #308 --- .../combination-sum/__test__/combinationSum.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/algorithms/sets/combination-sum/__test__/combinationSum.test.js b/src/algorithms/sets/combination-sum/__test__/combinationSum.test.js index 7b196bf2..8cb1c4ad 100644 --- a/src/algorithms/sets/combination-sum/__test__/combinationSum.test.js +++ b/src/algorithms/sets/combination-sum/__test__/combinationSum.test.js @@ -2,6 +2,16 @@ import combinationSum from '../combinationSum'; describe('combinationSum', () => { it('should find all combinations with specific sum', () => { + expect(combinationSum([1], 100000)).toHaveLength(1); + + expect( + combinationSum([1], 100000)[0] + .every(el => el === 1), + ) + .toBe(true); + + expect(combinationSum([0, 2], 6)).toEqual([[2, 2, 2]]); + expect(combinationSum([1], 4)).toEqual([ [1, 1, 1, 1], ]); From 2f60cab8351dced1c72e016b31cf20ad38b0b0cb Mon Sep 17 00:00:00 2001 From: casca <8927157+casca@users.noreply.github.com> Date: Thu, 30 Jan 2020 21:33:47 +0100 Subject: [PATCH 3/3] Update combinationSum README to reflect new implementation --- src/algorithms/sets/combination-sum/README.md | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/algorithms/sets/combination-sum/README.md b/src/algorithms/sets/combination-sum/README.md index cb14f1ba..379e0590 100644 --- a/src/algorithms/sets/combination-sum/README.md +++ b/src/algorithms/sets/combination-sum/README.md @@ -38,21 +38,22 @@ A solution set is: ## 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. +number of result, we don’t need to consider dynamic programming. +We do instead a depth-first traversal of the decision tree, +using an iterative implementation to avoid stack overflows. Here is an example of decision tree for the situation when `candidates = [2, 3]` and `target = 6`: ``` - 0 - / \ - +2 +3 - / \ \ - +2 +3 +3 - / \ / \ \ - +2 ✘ ✘ ✘ ✓ - / \ - ✓ ✘ + 0 + / \ + +3 +2 + / \ \ + +3 +2 +2 + / \ \ + ✓ +2 +2 + \ \ + ✘ ✓ ``` ## References