Cherry Pickup

“Cherry Pickup” involves dynamic programming and navigating a 2D grid. Here are some problems to get comfortable with the relevant concepts:

  1. Maximum Subarray (LeetCode 53): This is a basic introduction to dynamic programming. The goal is to find the subarray with the largest sum.

  2. Climbing Stairs (LeetCode 70): Another dynamic programming problem that’s a bit more challenging. You’re tasked with finding the number of distinct ways you can climb a staircase.

  3. Unique Paths (LeetCode 62): This problem asks you to find the number of unique paths in a 2D grid, which is similar to “Cherry Pickup” but without the added complexity of the cherries.

  4. Unique Paths II (LeetCode 63): This problem adds obstacles to the 2D grid, making it more challenging.

  5. Minimum Path Sum (LeetCode 64): In this problem, you need to find the path from the top-left to the bottom-right of a 2D grid that minimizes the sum of the numbers along the path.

  6. Coin Change (LeetCode 322): This dynamic programming problem involves finding the fewest number of coins that you need to make up a certain amount of money.

  7. Longest Increasing Subsequence (LeetCode 300): This is a classic dynamic programming problem that asks for the length of the longest increasing subsequence.

  8. House Robber (LeetCode 198): In this problem, you need to maximize your profit from robbing houses, but you can’t rob two houses in a row.

  9. House Robber II (LeetCode 213): This problem is a more challenging version of the House Robber problem where the houses are arranged in a circle.

  10. Perfect Squares (LeetCode 279): This problem requires dynamic programming to find the least number of perfect square numbers that sum to a given number.

These cover dynamic programming and navigating a 2D grid, which are key to solving the “Cherry Pickup” problem.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from typing import List

class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        N = len(grid)
        memo = [[[None]*N for _1 in range(N)] for _2 in range(N)]
        return max(0, self.dp(0, 0, 0, grid, memo))

    def dp(self, r1, c1, c2, grid, memo):
        N = len(grid)
        r2 = r1 + c1 - c2
        if (N == r1 or N == r2 or N == c1 or N == c2 or
                grid[r1][c1] == -1 or grid[r2][c2] == -1):
            return float('-inf')
        elif r1 == c1 == N-1:
            return grid[r1][c1]
        elif memo[r1][c1][c2] is not None:
            return memo[r1][c1][c2]
        else:
            ans = grid[r1][c1] + (r1 != r2) * grid[r2][c2]
            ans += max(self.dp(r1, c1+1, c2+1, grid, memo),
                       self.dp(r1+1, c1, c2, grid, memo),
                       self.dp(r1, c1+1, c2, grid, memo),
                       self.dp(r1+1, c1, c2+1, grid, memo))
            memo[r1][c1][c2] = ans
            return ans

Clarification Questions

What are the clarification questions we can ask about this problem?

Identifying Problem Isomorphism

Can you help me with finding the isomorphism for this problem?

Which problem does this problem map to the corresponding isomorphic problem on Leetcode ?

Thought Process

The thought process for this problem is about searching for the maximum amount of cherries in all possible paths from the top left to the bottom right and back to the top left. The complexity here is that the grid changes after the first pass since cherries are being picked up. Therefore, a simple 2D dynamic programming approach would not work. However, because the number of steps in both paths is the same at any moment, we can instead think of this as two paths moving from top left to bottom right simultaneously. This would let us use a 3D dynamic programming approach.

Here are the steps for this problem:

  1. Define the State: Define dp[i][j][k] as the maximum total cherries that both persons can take starting from cell (i,j) and (k, l), where l = i+j-k. Notice i, j, k, l are indices of the 2D grid.

  2. Define the Base Case: Since both persons start at (0,0), dp[0][0][0] is equal to the number of cherries at (0,0), which is grid[0][0].

  3. Define the State Transition Function: dp[i][j][k] is max(dp[i-1][j][k-1], dp[i][j-1][k], dp[i-1][j][k], dp[i][j-1][k-1]) + grid[i][j] + (grid[k][l] if (i,j)!=(k,l) else 0). Here, (i-1,j), (i,j-1) are the possible previous positions for the person at (i,j), and similarly for the other person.

  4. Define the Final State: dp[N-1][N-1][N-1] will be our answer, where N is the size of the grid.

  5. Iterate Over the State Space: Iterate over the entire state space to compute the state transition and obtain the final answer.

Here’s the Python code for the solution based on the above approach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from typing import List

class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        N = len(grid)
        memo = [[[None]*N for _1 in range(N)] for _2 in range(N)]
        return max(0, self.dp(0, 0, 0, grid, memo))

    def dp(self, r1, c1, c2, grid, memo):
        N = len(grid)
        r2 = r1 + c1 - c2
        if (N == r1 or N == r2 or N == c1 or N == c2 or
                grid[r1][c1] == -1 or grid[r2][c2] == -1):
            return float('-inf')
        elif r1 == c1 == N-1:
            return grid[r1][c1]
        elif memo[r1][c1][c2] is not None:
            return memo[r1][c1][c2]
        else:
            ans = grid[r1][c1] + (r1 != r2) * grid[r2][c2]
            ans += max(self.dp(r1, c1+1, c2+1, grid, memo),
                       self.dp(r1+1, c1, c2, grid, memo),
                       self.dp(r1, c1+1, c2, grid, memo),
                       self.dp(r1+1, c1, c2+1, grid, memo))
            memo[r1][c1][c2] = ans
            return ans

This solution works by taking advantage of the fact that any path can be uniquely identified by the total number of steps from the origin, and thus, we reduce the problem from finding two dependent paths to finding two independent paths.

Problem Classification

Analyzing the ‘What’ Components:

  1. Input: The input consists of an n x n grid, where each cell represents either an empty cell (represented by 0), a cell containing a cherry (represented by 1), or a cell with a thorn that blocks the way (represented by -1).

  2. Entity: The entity here is a player who can move around the grid cells starting from the position (0, 0) and can reach to the position (n - 1, n - 1) by moving right or down through valid path cells (cells with value 0 or 1). The player can then return back to (0, 0) by moving left or up through valid path cells.

  3. Objective: The objective is to maximize the number of cherries the player can collect. The player picks up a cherry when passing through a path cell containing it, causing the cell to become an empty cell (0).

  4. Constraints: There are certain constraints that govern the possible movements or actions of the player. If there is no valid path between (0, 0) and (n - 1, n - 1), then no cherries can be collected.

Based on the ‘What’ components, this problem can be further classified as a path-finding problem coupled with optimization. It falls under the category of dynamic programming problems, as the optimal solution involves finding solutions to overlapping subproblems (i.e., the maximum number of cherries that can be collected from a certain point). The problem requires both depth-first search (to explore all possible paths) and memoization (to remember results of subproblems) for an efficient solution.

Language Agnostic Coding Drills

  1. Dissection of Code and Identification of Concepts:

    • Basic Programming Constructs: The code utilizes basic constructs such as variables, if-else conditions, and loops. This is a fundamental level in coding and is required in any programming task.

    • Python Lists: The code uses Python lists and multi-dimensional lists. Understanding list comprehension is essential to create and manipulate lists in Python.

    • Function Definitions and Calls: The ‘dp’ function is defined within the ‘Solution’ class and is called recursively. Understanding how functions work is a core concept in programming.

    • Recursion: The solution to the problem is found using a recursive function. Recursion is a more complex concept where a function calls itself to solve a problem.

    • Memoization (Dynamic Programming): This is an optimization technique used to speed up programs by storing the results of expensive function calls and reusing them when the same inputs occur again.

  2. List of Concepts in Increasing Difficulty:

    • Basic Programming Constructs: The basics of programming, such as declaring variables, using conditional (if-else) statements, and for-loops are required. This is the first step in learning any programming language.

    • Python Lists: Python lists and multi-dimensional lists are slightly more complex. This requires understanding Python’s data structures and how to manipulate them.

    • Function Definitions and Calls: While not particularly difficult, it builds upon the previous concepts and introduces the idea of code modularity and reusability.

    • Recursion: This concept is a bit harder to grasp, especially for beginners. It requires an understanding that a function can call itself to solve smaller parts of a problem.

    • Memoization (Dynamic Programming): This is a more advanced concept. It requires an understanding of both recursion and data structures, and is a common strategy for optimizing code that solves similar subproblems repeatedly.

  3. Problem-Solving Approach:

    The problem at hand is a classic case of a pathfinding problem with a slight modification - the need to make a round trip and the ability to change the environment (grid). This adds an additional layer of complexity to the problem. Here is how the identified coding drills contribute to the overall solution:

    • The program starts by initializing a 3-dimensional list, memo, for storing intermediate results, and calls the recursive function ‘dp’ with the initial parameters.

    • The ‘dp’ function, based on the current positions on the grid, makes a series of checks (using basic programming constructs and Python list indexing) to decide the base cases for recursion.

    • If none of the base cases match, it calculates the maximum cherries it can collect from the current positions and four possible movements (right or down for both persons). It does this by calling itself with the new positions (recursion). The results of these calls are stored in the memo list (memoization) for future use.

    • By exploring all possible moves and storing intermediate results, the function finally arrives at the solution, which is the maximum number of cherries that can be collected. This whole process is an implementation of a dynamic programming approach.

Targeted Drills in Python

  1. Python-Based Coding Drills for Each Identified Concept:

    • Basic Programming Constructs: Understanding and using variables, conditional statements, and loops. For example:

      1
      2
      3
      4
      5
      
      x = 10  # variable declaration
      if x > 5:  # if statement
          print("x is greater than 5")
      for i in range(x):  # for loop
          print(i)
      
    • Python Lists: Creation and manipulation of lists and multi-dimensional lists. For example:

      1
      2
      3
      4
      
      list_1d = [1, 2, 3, 4, 5]  # 1D list
      list_2d = [[1, 2], [3, 4]]  # 2D list
      list_1d.append(6)  # append operation
      list_2d[0][0] = 0  # access and modify element in 2D list
      
    • Function Definitions and Calls: Writing a function and calling it. For example:

      1
      2
      3
      4
      
      def say_hello(name):  # function definition
          print(f"Hello, {name}!")
      
      say_hello("World")  # function call
      
    • Recursion: Implementing a recursive function, such as a factorial function:

      1
      2
      3
      4
      5
      6
      7
      
      def factorial(n):
          if n == 0:  # base case
              return 1
          else:  # recursive case
              return n * factorial(n-1)
      
      print(factorial(5))  # prints: 120
      
    • Memoization (Dynamic Programming): Using memoization to optimize a function with overlapping subproblems, like the Fibonacci sequence:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      def fib(n, memo = {}):
          if n in memo:
              return memo[n]
          if n <= 2:
              return 1
          memo[n] = fib(n-1, memo) + fib(n-2, memo)
          return memo[n]
      
      print(fib(50))  # prints: 12586269025
      
  2. Problem-Specific Concepts and Drills:

    • Pathfinding in Grid: The problem involves finding a path in a grid, which is a common concept in many programming problems. A simple drill for this would be to move from the top-left to the bottom-right of a 2D grid.

    • Dynamic Programming for Pathfinding: The specific challenge in this problem is the need to remember the maximum cherries collected at each point in the grid for both directions of the round trip. A drill for this would involve storing and reusing previously calculated results in a pathfinding problem.

  3. Integration of the Drills:

    • Start with defining the problem-specific grid and initializing the memoization structure.
    • Implement the basic logic for grid navigation using conditionals and loops.
    • Develop the recursive function that calls itself based on the grid navigation logic. Ensure the function has a terminating condition.
    • Implement the memoization within the recursive function to avoid repeated calculations.
    • Test the entire solution with various inputs to ensure all pieces are working cohesively to solve the problem. Each component plays a critical role in the overall solution and contributes to optimizing the final implementation.

AI beats runtime: 54.43%, memory: 91.14%.