Start learning free

LESSON 296 OF 414 · MEMOIZATION

Jump Game in Scala: O(n^2) with Memoization

Practice O(n2) - Memoization Read the full explanation

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Your goal is to reach the last index in the minimum number of jumps.

Example:

Input: [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2.
             Jump 1 step from index 0 to 1, then 3 steps to the last index.
			

Note:

Apply memoization to the previous solution. Your algorithm should run in O(n^2) time and use O(n) extra space.


STEP 1 OF 10
    Ctrl + Enter to check

    SCALA · MEMOIZATION

    Jump Game in Scala, explained

    Problem overview

    The method jump receives an array nums of non-negative integers. You start at index 0, and nums(i) is the maximum length of a jump from index i. The goal is to reach the last index in the minimum number of jumps, and the method returns that number.

    For [2, 3, 1, 1, 4] the answer is 2: jump one step from index 0 to index 1, then three steps from index 1 to the last index. No single jump from index 0 reaches index 4, so one jump is not enough.

    The statement sets a budget of O(n^2) time and O(n) extra space, and asks you to add memoization to the plain recursive solution.

    Approach: recursion that remembers its answers

    Define minJumps(nums, i, cache) as the minimum number of jumps needed to reach the last cell starting from cell i. From i you can land on any cell from i + 1 to i + nums(i), as long as it is inside the array. The best plan that starts with a jump to cell j costs 1 jump plus minJumps(nums, j, cache), and the answer for i is the smallest of these over every reachable j. When i is the last index, the answer is 0.

    Without memoization, the recursion reaches the same cell by many different routes and recomputes its answer each time, which takes exponential time. The fix is a cache from index to answer. There are only n distinct cells, and each is computed once. Computing one cell loops over up to n reachable cells, so the total is O(n^2).

    The lesson also has an iterative version of this idea and a faster greedy one; here the focus is the top-down recursion with a cache.

    Step by step

    1. Create the cache in jump as a new empty scala.collection.mutable.Map[Int, Int], declared with val. It must be the mutable map so the helper can add entries.
    2. Return minJumps(nums, 0, cache), since the answer starts from cell 0. The helper doesn't exist yet; the next steps write it.
    3. Declare minJumps above jump, taking the array, the index i and the cache, and returning an Int.
    4. Base case. When i == nums.length - 1, you are already on the last cell, so return 0. Every recursive call moves to a larger index, so all chains of calls end here or at a cell with nowhere to go.
    5. Cache hit. If the cache contains i, return cache(i) straight away.
    6. Start result at nums.length, as a var. No path needs that many jumps (at most nums.length - 1), so it works as infinity.
    7. Walk the reachable cells with var j = i + 1 and a while loop that runs while j < nums.length and j <= i + nums(i), ending the body with j += 1.
    8. Take the minimum of result and 1 + minJumps(nums, j, cache) with Math.min, passing the same cache down.
    9. Store the answer with cache(i) = result before returning it, so this cell is never computed again.
    10. Return result.

    The solution in Scala

    class Solution {
        def minJumps(nums: Array[Int], i: Int, cache: scala.collection.mutable.Map[Int, Int]): Int = {
            if (i == nums.length - 1) {
                return 0
            }
            if (cache.contains(i)) {
                return cache(i)
            }
            var result = nums.length
            var j = i + 1
            while (j < nums.length && j <= i + nums(i)) {
                result = Math.min(result, 1 + minJumps(nums, j, cache))
                j += 1
            }
            cache(i) = result
            return result
        }
    
        def jump(nums: Array[Int]): Int = {
            val cache = scala.collection.mutable.Map[Int, Int]()
            return minJumps(nums, 0, cache)
        }
    }
    

    The Scala details worth a closer look:

    • The cache is a mutable map. Scala's default Map is immutable. scala.collection.mutable.Map supports cache(i) = result, which calls the map's update method, and reading with cache(i).
    • Check before reading. cache(i) on a missing key throws NoSuchElementException. That is why contains comes first. Alternatively, cache.get(i) returns an Option.
    • Array indexing uses parentheses. nums(i), not nums[i]; nums.length has no parentheses.
    • var versus val. result and j change, so they are var; the cache reference never changes, so it is a val, even though the map's contents do.
    • Short-circuit &&. The loop condition checks j < nums.length first, and j <= i + nums(i) second, so the two bounds are tested in order.
    • Explicit return. The lesson uses it; in idiomatic Scala the last expression of a block is already its value.

    Complexity

    Time: O(n^2). There are at most n distinct values of i, and each is computed once. A computation loops over at most n - 1 cells, and every recursive call inside it either returns at once (cache hit or base case) or computes a new cell. The total is at most n * n loop steps.

    Space: O(n). The cache holds one entry per cell, and the recursion can go as deep as n calls (for example when every nums(i) is 1), so the stack is O(n) as well. A deep chain of calls is also the reason the iterative version is useful for large arrays.

    Edge cases and common mistakes

    • A single element. [0] is already at the last index, so the answer is 0.
    • The example. [2, 3, 1, 1, 4] returns 2.
    • All ones. [1, 1, 1, 1] needs 3 jumps, one per step.
    • One long jump. [5, 0, 0, 0, 0, 0] returns 1: one jump of 5 lands on the last index. If the number were larger, the check j < nums.length would keep the loop inside the array.
    • Stuck cells. A cell with nums(i) = 0 has no loop iterations, so its answer is nums.length, the "infinity" value. For [1, 0, 3] the last cell can't be reached, and the method returns nums.length, 3, rather than a real jump count. The lesson's tests don't use such arrays.
    • Forgetting to store the result. Without cache(i) = result, the cache never fills and the running time returns to exponential.
    • Letting the loop pass the end. Without j < nums.length, a large nums(i) would read past the array and throw ArrayIndexOutOfBoundsException.

    Testing

    • [2, 3, 1, 1, 4] gives 2.
    • [0] gives 0.
    • [1, 1, 1, 1] gives 3.
    • [5, 0, 0, 0, 0, 0] gives 1.
    • [2, 0, 1] gives 1.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free