From 549759757d08851af6edaa9409aafe5153650ebc Mon Sep 17 00:00:00 2001 From: Oleksii Trekhleb Date: Tue, 10 Jul 2018 07:56:28 +0300 Subject: [PATCH] Add bottom-up dynamic programming solution to Jump Game. --- .../__test__/dpBottomUpJumpGame.test.js | 17 +++++++ .../jump-game/backtrackingJumpGame.js | 6 +++ .../jump-game/dpBottomUpJumpGame.js | 46 +++++++++++++++++++ .../jump-game/dpTopDownJumpGame.js | 12 +++-- .../uncategorized/jump-game/greedyJumpGame.js | 13 ++++++ 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 src/algorithms/uncategorized/jump-game/__test__/dpBottomUpJumpGame.test.js create mode 100644 src/algorithms/uncategorized/jump-game/dpBottomUpJumpGame.js diff --git a/src/algorithms/uncategorized/jump-game/__test__/dpBottomUpJumpGame.test.js b/src/algorithms/uncategorized/jump-game/__test__/dpBottomUpJumpGame.test.js new file mode 100644 index 00000000..934c6deb --- /dev/null +++ b/src/algorithms/uncategorized/jump-game/__test__/dpBottomUpJumpGame.test.js @@ -0,0 +1,17 @@ +import dpBottomUpJumpGame from '../dpBottomUpJumpGame'; + +describe('dpBottomUpJumpGame', () => { + it('should solve Jump Game problem in bottom-up dynamic programming manner', () => { + expect(dpBottomUpJumpGame([1, 0])).toBeTruthy(); + expect(dpBottomUpJumpGame([100, 0])).toBeTruthy(); + expect(dpBottomUpJumpGame([2, 3, 1, 1, 4])).toBeTruthy(); + expect(dpBottomUpJumpGame([1, 1, 1, 1, 1])).toBeTruthy(); + expect(dpBottomUpJumpGame([1, 1, 1, 10, 1])).toBeTruthy(); + expect(dpBottomUpJumpGame([1, 5, 2, 1, 0, 2, 0])).toBeTruthy(); + + expect(dpBottomUpJumpGame([1, 0, 1])).toBeFalsy(); + expect(dpBottomUpJumpGame([3, 2, 1, 0, 4])).toBeFalsy(); + expect(dpBottomUpJumpGame([0, 0, 0, 0, 0])).toBeFalsy(); + expect(dpBottomUpJumpGame([5, 4, 3, 2, 1, 0, 0])).toBeFalsy(); + }); +}); diff --git a/src/algorithms/uncategorized/jump-game/backtrackingJumpGame.js b/src/algorithms/uncategorized/jump-game/backtrackingJumpGame.js index 843de882..272c4b26 100644 --- a/src/algorithms/uncategorized/jump-game/backtrackingJumpGame.js +++ b/src/algorithms/uncategorized/jump-game/backtrackingJumpGame.js @@ -1,6 +1,12 @@ /** * BACKTRACKING approach of solving Jump Game. * + * This is the inefficient solution where we try every single jump + * pattern that takes us from the first position to the last. + * We start from the first position and jump to every index that + * is reachable. We repeat the process until last index is reached. + * When stuck, backtrack. + * * @param {number[]} numbers - array of possible jump length. * @param {number} startIndex - index from where we start jumping. * @param {number[]} currentJumps - current jumps path. diff --git a/src/algorithms/uncategorized/jump-game/dpBottomUpJumpGame.js b/src/algorithms/uncategorized/jump-game/dpBottomUpJumpGame.js new file mode 100644 index 00000000..cfd67fe6 --- /dev/null +++ b/src/algorithms/uncategorized/jump-game/dpBottomUpJumpGame.js @@ -0,0 +1,46 @@ +/** + * DYNAMIC PROGRAMMING BOTTOM-UP approach of solving Jump Game. + * + * This comes out as an optimisation of DYNAMIC PROGRAMMING TOP-DOWN approach. + * + * The observation to make here is that we only ever jump to the right. + * This means that if we start from the right of the array, every time we + * will query a position to our right, that position has already be + * determined as being GOOD or BAD. This means we don't need to recurse + * anymore, as we will always hit the memo table. + * + * We call a position in the array a "good" one if starting at that + * position, we can reach the last index. Otherwise, that index + * is called a "bad" one. + * + * @param {number[]} numbers - array of possible jump length. + * @return {boolean} + */ +export default function dpBottomUpJumpGame(numbers) { + // Init cells goodness table. + const cellsGoodness = Array(numbers.length).fill(undefined); + // Mark the last cell as "good" one since it is where we ultimately want to get. + cellsGoodness[cellsGoodness.length - 1] = true; + + // Go throw all cells starting from the one before the last + // one (since the last one is "good" already) and fill cellsGoodness table. + for (let cellIndex = numbers.length - 2; cellIndex >= 0; cellIndex -= 1) { + const maxJumpLength = Math.min( + numbers[cellIndex], + numbers.length - 1 - cellIndex, + ); + + for (let jumpLength = maxJumpLength; jumpLength > 0; jumpLength -= 1) { + const nextIndex = cellIndex + jumpLength; + if (cellsGoodness[nextIndex] === true) { + cellsGoodness[cellIndex] = true; + // Once we detected that current cell is good one we don't need to + // do further cells checking. + break; + } + } + } + + // Now, if the zero's cell is good one then we can jump from it to the end of the array. + return cellsGoodness[0] === true; +} diff --git a/src/algorithms/uncategorized/jump-game/dpTopDownJumpGame.js b/src/algorithms/uncategorized/jump-game/dpTopDownJumpGame.js index d44506b1..0e23736c 100644 --- a/src/algorithms/uncategorized/jump-game/dpTopDownJumpGame.js +++ b/src/algorithms/uncategorized/jump-game/dpTopDownJumpGame.js @@ -2,11 +2,14 @@ * DYNAMIC PROGRAMMING TOP-DOWN approach of solving Jump Game. * * This comes out as an optimisation of BACKTRACKING approach. - * Optimisation is done by using memo table where we store information - * about each cell whether it is "good" or "bad" or "unknown". + * + * It relies on the observation that once we determine that a certain + * index is good / bad, this result will never change. This means that + * we can store the result and not need to recompute it every time. * * We call a position in the array a "good" one if starting at that - * position, we can reach the last index. + * position, we can reach the last index. Otherwise, that index + * is called a "bad" one. * * @param {number[]} numbers - array of possible jump length. * @param {number} startIndex - index from where we start jumping. @@ -30,8 +33,7 @@ export default function dpTopDownJumpGame( const currentCellsGoodness = [...cellsGoodness]; if (!currentCellsGoodness.length) { numbers.forEach(() => currentCellsGoodness.push(undefined)); - // Mark the last cell as "good" one since it is where - // we ultimately want to get. + // Mark the last cell as "good" one since it is where we ultimately want to get. currentCellsGoodness[cellsGoodness.length - 1] = true; } diff --git a/src/algorithms/uncategorized/jump-game/greedyJumpGame.js b/src/algorithms/uncategorized/jump-game/greedyJumpGame.js index b9d85a0a..df527891 100644 --- a/src/algorithms/uncategorized/jump-game/greedyJumpGame.js +++ b/src/algorithms/uncategorized/jump-game/greedyJumpGame.js @@ -1,6 +1,19 @@ /** * GREEDY approach of solving Jump Game. * + * This comes out as an optimisation of DYNAMIC PROGRAMMING BOTTOM_UP approach. + * + * Once we have our code in the bottom-up state, we can make one final, + * important observation. From a given position, when we try to see if + * we can jump to a GOOD position, we only ever use one - the first one. + * In other words, the left-most one. If we keep track of this left-most + * GOOD position as a separate variable, we can avoid searching for it + * in the array. Not only that, but we can stop using the array altogether. + * + * We call a position in the array a "good" one if starting at that + * position, we can reach the last index. Otherwise, that index + * is called a "bad" one. + * * @param {number[]} numbers - array of possible jump length. * @return {boolean} */