2106. Maximum Fruits Harvested After at Most K Steps


Problem Description

In this problem, we are given a sorted 2D integer array fruits where each entry fruits[i] consists of two integers, position_i and amount_i. position_i represents a unique position on an infinite x-axis where fruits are located, and amount_i is the number of fruits available at that position. Additionally, we have a starting position on the axis, startPos, and an integer k which represents the maximum number of steps we can take in total. The goal is to find the maximum total number of fruits one can harvest.

One can walk either left or right on the x-axis, and upon visiting a position, all fruits there are harvested and thus removed from that position. Walking one unit on the x-axis counts as one step, and the total number of steps cannot exceed k. The task is to devise a strategy for harvesting the most fruits under these constraints.

Intuition

To arrive at the solution, we first need to understand that walking too far in one direction might prevent collecting fruits in the other direction because of the step limit k. To maximize the harvest, we should consider walking in one direction to collect fruits and then potentially changing direction to collect more if the steps allow.

The intuition behind the solution is to use a sliding window technique to keep track of the total amount of fruits that can be collected within k steps from startPos. We slide the window across the sorted positions while ensuring the total number of steps including the return to startPos does not exceed k. If at any point the total distance of our sliding window exceeds k, we move the starting point of our sliding window to the right to shrink the harvesting range back within the allowed steps.

We calculate the maximum number of fruits that can be collected within this range and update our answer accordingly. At each step, we consider reaching the farthest point in our window from startPos and then returning to the nearest point. The heart of this approach relies on the optimal substructure of the problem—maximizing fruits within a range naturally contributes to maximizing the overall harvest.

Learn more about Binary Search, Prefix Sum and Sliding Window patterns.

Solution Approach

The solution implements a sliding window approach to maintain a range of positions we can visit to collect fruits within k steps. The fruits array gives us the positions and the number of fruits at each position, sorted by the positions. The implementation iterates over this array, expanding and shrinking the window to find the optimal total amount of fruits that can be harvested.

The maxTotalFruits function starts by initializing important variables: ans to store the maximum number of fruits we can harvest, i to denote the start of the sliding window, s to keep the sum of fruits within the window, and j to iterate over the fruit positions.

Here is a step-by-step explanation of the algorithm:

  1. Iterate through the fruit array using the index j and for each position pj with fruit count fj, add fj to the sum s.
  2. Check if the current window [i, j] exceeds k steps. This is done by calculating the distance pj - fruits[i][0] (the width of the window) and adding the smaller distance from startPos to either end of the window.
  3. If the window is too wide (exceeds k steps), we shrink the window from the left by incrementing i, effectively removing fruits at fruits[i][1] from the sum s.
  4. At each iteration, after adjusting the window, update ans with the maximum of itself or the current sum s.
  5. Continue this process until all positions have been checked.

The core concept here leverages the fact that, since the fruit positions are sorted and unique, we can efficiently determine the range of positions to harvest by only considering the endpoints of the current window.

The algorithm uses constant space for variables and O(n) time where n is the number of positions since it scans through the positions once.

Here is a portion of the code explaining the critical part of the algorithm:

for j, (pj, fj) in enumerate(fruits):
    s += fj
    while i <= j and pj - fruits[i][0] + min(abs(startPos - fruits[i][0]), abs(startPos - pj)) > k:
        s -= fruits[i][1]
        i += 1
    ans = max(ans, s)

The loop adds fruits to s and potentially contracts the window by advancing i if the condition (pj - fruits[i][0] + min(abs(startPos - fruits[i][0]), abs(startPos - pj))) > k is met, ensuring the total steps do not exceed k. The answer ans is updated to the maximum sum s found within the constraints.

This elegant approach effectively balances expanding and contracting the harvesting range on the fly to maximize fruit collection within the step limit.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's consider a small example to illustrate the solution approach. Suppose we have the following input:

  • fruits array: [[0,3], [2,1], [5,2]]
  • startPos: 1
  • k: 4

Here, we can move a maximum of 4 steps from the starting position, and positions [0, 2, 5] have [3, 1, 2] fruits respectively. We want to maximize the number of fruits we can pick within those 4 steps.

Here's how the solution approach would work on this example:

  1. Initialize ans as 0 (maximum number of fruits harvested), i as 0 (start of the sliding window), and s as 0 (sum of fruits within the window).
  2. Start iterating through fruits array with index j. As we add fj to sum s, the sum of fruits becomes 3 when j = 0.
  3. There is no need to shrink the window yet since we are within k steps range (the max steps we can move is 4).
  4. Move to the next item in the array, j = 1. The sum of fruits s becomes 3 + 1 = 4. The window [i, j] is now [0, 2], and the total steps required are abs(startPos - fruits[i][0]) + (fruits[j][0] - fruits[i][0]) = abs(1 - 0) + (2 - 0) = 3 steps to go from startPos to pj, which still does not exceed k.
  5. Move to the next item in the array, j = 2. Add fruit count at that position to s, so now s = 4 + 2 = 6. The window [i, j] is now [0, 5], and we check the steps: abs(startPos - fruits[i][0]) + (fruits[j][0] - fruits[i][0]) = abs(1 - 0) + (5 - 0) = 6. This exceeds k, so we need to shrink the window from the left by incrementing i.
  6. The new window [i, j] is now [2, 5]. We update s by subtracting the fruits at fruits[i][1] and calculate steps again: s = 6 - fruits[i][1] = 6 - 3 = 3. The steps are abs(startPos - fruits[i][0]) + (fruits[j][0] - fruits[i][0]) = abs(1 - 2) + (5 - 2) = 4, which is equal to k so we keep the window.
  7. As i moves up, s decreases to 2, having removed the fruits at position 0. Since this is still less than k, we continue.
  8. Now, ans is updated to a maximum of itself or current sum s, so ans = max(0, 3) = 3.
  9. No more positions left, we end with ans = 3, which is the amount of fruit we harvested.

Therefore, following this approach with our example, the maximum number of fruits that can be harvested is 3.

Solution Implementation

1class Solution:
2    def maxTotalFruits(self, fruits: List[List[int]], startPos: int, k: int) -> int:
3        # Initialize variables for the answer, starting index of the window, and the sum of fruits within the window
4        max_fruits = window_start = total_fruits = 0
5      
6        # Iterate over the fruits list with index 'j' and unpack positions and fruits as 'position_j' and 'fruits_at_j'
7        for window_end, (position_j, fruits_at_j) in enumerate(fruits):
8            # Add the fruits at position 'j' to the running total within the window
9            total_fruits += fruits_at_j
10          
11            # Shrink the window from the left as long as the condition is met
12            while (
13                window_start <= window_end
14                and position_j
15                - fruits[window_start][0]  # Distance between the current end of the window and its start
16                + min(abs(startPos - fruits[window_start][0]), abs(startPos - position_j))  # Minimum distance to startPos from either end of the window
17                > k  # Check if the total distance is within 'k'
18            ):
19                # If the window exceeds 'k' distance, subtract the fruits at 'window_start' and move window start to the right
20                total_fruits -= fruits[window_start][1]
21                window_start += 1
22          
23            # Update the 'max_fruits' if the current window's total fruits is greater than the previously recorded maximum
24            max_fruits = max(max_fruits, total_fruits)
25      
26        # Return the maximum number of fruits that can be collected within 'k' units of movement
27        return max_fruits
28
1class Solution {
2    public int maxTotalFruits(int[][] fruits, int startPos, int k) {
3        int maxFruits = 0; // Stores the maximum number of fruits we can collect
4        int currentSum = 0; // Stores the current sum of fruits between two points
5      
6        // Two pointers approach: i is for the start point and j is for the end point
7        for (int i = 0, j = 0; j < fruits.length; ++j) {
8            int positionJ = fruits[j][0]; // The position of the j-th tree
9            int fruitsAtJ = fruits[j][1]; // The number of fruits at the j-th tree
10            currentSum += fruitsAtJ; // Add fruits at the j-th tree to the current sum
11
12            // Adjust the starting point i to not exceed the maximum distance k
13            while (i <= j &&
14                   positionJ - fruits[i][0] +
15                   Math.min(Math.abs(startPos - fruits[i][0]), Math.abs(startPos - positionJ))
16                   > k) {
17                // Subtract the number of fruits at the i-th tree as we move the start point forward
18                currentSum -= fruits[i][1];
19                i++; // Increment the start point
20            }
21          
22            // Update maxFruits with the maximum of current sum and previously calculated maxFruits
23            maxFruits = Math.max(maxFruits, currentSum);
24        }
25        return maxFruits; // Return the maximum number of fruits that can be collected
26    }
27}
28
1#include <vector>
2#include <algorithm>
3
4class Solution {
5public:
6    // Function to calculate the maximum total number of fruits that can be collected
7    // "fruits" vector holds pairs of positions and fruit counts, "startPos" is the starting position,
8    // "k" is the maximum number of steps that can be taken.
9    int maxTotalFruits(vector<vector<int>>& fruits, int startPos, int k) {
10        int maxFruits = 0; // Store the maximum total fruits that can be collected.
11        int currentSum = 0; // Store the current sum of fruits being considered.
12
13        // Use a sliding window defined by the indices [i, j].
14        for (int i = 0, j = 0; j < fruits.size(); ++j) {
15            int positionJ = fruits[j][0]; // Position of the j-th fruit tree
16            int fruitCountJ = fruits[j][1]; // Number of fruits at the j-th fruit tree
17            currentSum += fruitCountJ; // Add the fruits from the j-th tree to the current sum
18
19            // If the distance from start to the current fruit is more than k
20            // after adjusting for the closest path, shrink the window from the left.
21            while (i <= j && positionJ - fruits[i][0] + 
22                   min(abs(startPos - fruits[i][0]), abs(startPos - positionJ)) > k) {
23                currentSum -= fruits[i][1]; // Remove the fruits from the i-th tree from the current sum
24                i++; // Move the start of the window to the right
25            }
26
27            // Update maxFruits with the maximum of the current value and currentSum.
28            maxFruits = max(maxFruits, currentSum);
29        }
30
31        return maxFruits; // Return the maximum number of fruits that can be collected
32    }
33};
34
1/**
2 * Calculates the maximum total number of fruits that can be collected from fruit trees lined up in a row.
3 * The farmer starts at a given position and can move a maximum total distance k.
4 * 
5 * @param {number[][]} fruitPositions - An array where each element is a pair [position, fruitCount];
6 *                                      position is the position of a tree, fruitCount is the number of
7 *                                      fruits at that tree.
8 * @param {number} startPos - The starting position of the farmer.
9 * @param {number} k - The maximum total distance the farmer can move.
10 * @returns {number} The maximum total fruits that can be collected.
11 */
12function maxTotalFruits(fruitPositions: number[][], startPos: number, k: number): number {
13    let maxFruits = 0;        // The current maximum number of fruits that can be collected
14    let currentFruits = 0;    // The running total of fruits collected within the current window
15
16    // Two-pointer approach to find the optimal range of trees to collect fruits from
17    for (let leftIndex = 0, rightIndex = 0; rightIndex < fruitPositions.length; rightIndex++) {
18        // Current fruit position and count
19        const [currentPosition, fruitCount] = fruitPositions[rightIndex];
20        currentFruits += fruitCount; // Add the fruits from the right pointer's current tree
21
22        // Adjust the left pointer while the total distance required to collect fruits
23        // from the range exceeds the maximum distance k
24        while (
25            leftIndex <= rightIndex &&
26            currentPosition - fruitPositions[leftIndex][0] + 
27            Math.min(Math.abs(startPos - fruitPositions[leftIndex][0]), Math.abs(startPos - currentPosition)) > k
28        ) {
29            // Subtract the fruits from the left pointer's current tree and move the left pointer to the right
30            currentFruits -= fruitPositions[leftIndex++][1];
31        }
32
33        // Update the maximum fruits collected if the current total is greater
34        maxFruits = Math.max(maxFruits, currentFruits);
35    }
36
37    return maxFruits; // Return the maximum fruits collected after checking all ranges
38}
39

Time and Space Complexity

Time Complexity

The time complexity of the given code is O(N), where N represents the number of pairs in the fruits list. Although the code features a nested loop, each fruit in the fruits list is processed only once due to the use of two-pointer technique. The inner while loop only advances the i pointer and does not perform excessive iterations for each j index in the outer loop. Therefore, every element is visited at most twice, keeping the overall time complexity linear with respect to the number of fruit positions.

Space Complexity

The space complexity of the given code is O(1). The only extra space used is for variables to keep track of the current maximum fruit total (ans), the current sum of fruits (s), and the pointers (i, j) used for traversing the fruits list. This use of space does not scale with the size of the input, and as such, remains constant.

Learn more about how to find time and space complexity quickly using problem constraints.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!


Load More