Facebook Pixel

3466. Maximum Coin Collection 🔒


Problem Description

Mario drives on a two-lane freeway where each mile has a certain number of coins. The number of coins Mario gains or loses at each mile is given by two integer arrays, lane1 and lane2. For lane1[i] and lane2[i], a positive value indicates the number of coins Mario gains, while a negative value indicates the coins lost as a toll.

Mario must start on lane 1 but can switch to lane 2 and back, not exceeding two lane switches. He can enter the freeway at any point and must travel at least one mile before exiting. The goal is to determine the maximum number of coins Mario can collect with at most two switches.

Intuition

To solve this problem, we use a dynamic programming approach with memoization. We define a function dfs(i, j, k) that returns the maximum number of coins Mario can collect starting from mile i, at lane j (where 0 indicates lane 1 and 1 indicates lane 2), with k lane changes remaining.

Here's the breakdown of the solution approach:

  • Base Case: If i >= n, we've reached the end of the freeway, return 0 coins.

  • Current Gain: For the current lane j, calculate the gain or loss of coins, x = lane1[i] if on lane 1, or x = lane2[i] if on lane 2.

  • Continue on the Same Lane: Consider staying on the same lane for one more mile: either collect the current mile's coins and exit, or move further down the same lane, which is max(x, dfs(i + 1, j, k) + x).

  • Switch Lanes: If allowed, consider switching lanes:

    • Drive one mile and then switch, which results in dfs(i + 1, j ^ 1, k - 1) + x.
    • Switch lanes without advancing, resulting in dfs(i, j ^ 1, k - 1).

To avoid recalculating solutions for the same state (i, j, k), we use caching. The final answer is the maximum value of the initial dfs invocation for every possible starting mile on lane 1.

Learn more about Dynamic Programming patterns.

Solution Approach

The problem is tackled using a recursive depth-first search with memoization to efficiently track and compute the maximum coins collected. Here's how it's structured:

  1. Recursive Function Design:

    • We define a recursive function, dfs(i, j, k), where i is the current mile, j is the lane (0 or 1), and k is the remaining allowable lane switches.
    • The function aims to calculate the maximum coins from the starting point i on lane j with k switches left.
  2. Base Case:

    • If Mario reaches or exceeds the total number of miles (i >= n), the recursion stops, returning 0 coins because there's no more freeway to traverse.
  3. Coin Calculation:

    • Determine the coins on the current lane using x = lane1[i] if j == 0 (lane 1) or x = lane2[i] if j == 1 (lane 2).
  4. Options Without Lane Change:

    • Mario can stay in the current lane: either collect coins and exit, or continue down the freeway on the same lane. This is computed as: max(x, dfs(i + 1, j, k) + x).
  5. Options With Lane Change (if k > 0):

    • Mario can switch lanes either after driving 1 mile (dfs(i + 1, j ^ 1, k - 1) + x) or switch immediately without moving forward (dfs(i, j ^ 1, k - 1)).
    • The j ^ 1 operation flips the lane: if j is 0 (lane 1), j ^ 1 will be 1 (lane 2), and vice versa.
  6. Memoization:

    • We use Python's functools.cache to store previously computed results for states (i, j, k) to prevent redundant calculations and improve efficiency.
  7. Final Computation:

    • Iterate over all possible starting positions on the freeway. Since Mario must enter initially on lane 1, the computation is initialized as dfs(i, 0, 2) for each mile index i.
    • Finally, we return the maximum coins Mario can collect from any valid entry point.

This approach effectively balances between dynamically evaluating each option Mario has and caching results for efficiency.

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 simple example where the freeway is 5 miles long with the following coins on each lane:

  • lane1 = [2, -3, 5, 1, -2]
  • lane2 = [-1, 4, -2, 3, 7]

Mario must maximize his coin collection starting on lane 1 with a maximum of two lane switches.

Step-by-Step Walkthrough:

  1. Initialize the Recursive Function:

    • Call dfs(i, j, k) where i is the starting mile, j is the current lane (0 for lane1, 1 for lane2), and k is the remaining lane changes.
  2. Starting at Mile 0 on Lane 1 (i=0, j=0, k=2):

    • Base Case Check: Not at the end, continue.
    • Current Gain: x = lane1[0] = 2.
    • Stay on Lane 1: Calculate max(2, dfs(1, 0, 2) + 2).
    • Switch to Lane 2: Consider both switching while advancing and switching immediately:
      • dfs(1, 1, 1) + 2 (switch after advancing)
      • dfs(0, 1, 1) (switch immediately)
  3. Mile 1 Calculations:

    • On Lane 1:

      • Gain x = lane1[1] = -3.
      • Stay on Lane 1: max(-3, dfs(2, 0, 2) - 3)
      • Switch to Lane 2: dfs(2, 1, 1) - 3 and dfs(1, 1, 1)
    • On Lane 2:

      • Gain x = lane2[1] = 4.
      • Stay on Lane 2: max(4, dfs(2, 1, 1) + 4)
      • Switch to Lane 1: dfs(2, 0, 0) + 4 and dfs(1, 0, 0)
  4. Mile 2 Calculations:

    • Similar evaluations continue recursively for each subsequent mile, considering the gain from continuing on the same lane and potential switch options.
  5. Optimization with Memoization:

    • Store the results of dfs(i, j, k) to avoid recalculating and improve efficiency.
  6. Final Computation:

    • Evaluate dfs starting at each possible mile with j=0 (lane1) and k=2 switches remaining, and determine the maximum coins.

Using this approach, Mario calculates the maximum coins he can collect by optimizing his lane choices and the timing of lane switches. The result will affirm the best starting point and lane-switch strategy.

Solution Implementation

1from functools import lru_cache
2from typing import List
3
4class Solution:
5    def maxCoins(self, lane1: List[int], lane2: List[int]) -> int:
6        # Use lru_cache to memoize the dfs function results
7        @lru_cache(None)
8        def dfs(index: int, current_lane: int, switches: int) -> int:
9            # If the current index exceeds bounds, no more coins can be collected
10            if index >= n:
11                return 0
12          
13            # Coins collected from the current position
14            current_coins = lane1[index] if current_lane == 0 else lane2[index]
15          
16            # Max coins collected without switching lanes
17            best_coins = dfs(index + 1, current_lane, switches) + current_coins
18          
19            # Consider switching lanes if switches are left
20            if switches > 0:
21                # Calculate coins if switched to the other lane at the next index
22                best_coins = max(best_coins, dfs(index + 1, current_lane ^ 1, switches - 1) + current_coins)
23                # Calculate coins if switched to the other lane immediately
24                best_coins = max(best_coins, dfs(index, current_lane ^ 1, switches - 1))
25          
26            return best_coins
27      
28        n = len(lane1)
29        max_coins = float('-inf')
30      
31        # Explore starting point on both lanes with 2 initial switches allowed
32        for start in range(n):
33            max_coins = max(max_coins, dfs(start, 0, 2))
34      
35        return max_coins
36
1class Solution {
2    private int n; // Length of the lanes
3    private int[] lane1; // Coins in lane1
4    private int[] lane2; // Coins in lane2
5    private Long[][][] f; // Memoization table to store intermediate results
6
7    public long maxCoins(int[] lane1, int[] lane2) {
8        this.n = lane1.length; // Initialize the lanes' length
9        this.lane1 = lane1; // Assign lane1 coins
10        this.lane2 = lane2; // Assign lane2 coins
11        this.f = new Long[n][2][3]; // Create memoization table with dimensions
12        long maxCoins = Long.MIN_VALUE; // Initialize the maximum coins collected
13      
14        // Iterate over each starting position in lane1
15        for (int i = 0; i < n; ++i) {
16            maxCoins = Math.max(maxCoins, dfs(i, 0, 2)); // Maximize coins collected
17        }
18      
19        return maxCoins; // Return the max coins that can be collected
20    }
21
22    // Recursive function to calculate the max coins dynamically
23    private long dfs(int index, int lane, int remainingSwitches) {
24        if (index >= n) { // Base case: If index out of bounds, return 0
25            return 0;
26        }
27        if (f[index][lane][remainingSwitches] != null) { // Return cached result
28            return f[index][lane][remainingSwitches];
29        }
30      
31        int coinsAtCurrent = (lane == 0) ? lane1[index] : lane2[index]; // Coins at current position
32      
33        // Select max coins between staying on the same lane or switching
34        long maxCoinsCollected = Math.max(coinsAtCurrent, 
35                                          dfs(index + 1, lane, remainingSwitches) + coinsAtCurrent);
36      
37        // Check possibilities if we have remaining switches
38        if (remainingSwitches > 0) {
39            // Switch lanes and collect coins, or switch without collecting
40            maxCoinsCollected = Math.max(maxCoinsCollected, 
41                                         dfs(index + 1, lane ^ 1, remainingSwitches - 1) + coinsAtCurrent);
42            maxCoinsCollected = Math.max(maxCoinsCollected, 
43                                         dfs(index, lane ^ 1, remainingSwitches - 1));
44        }
45      
46        // Cache and return the calculated result
47        return f[index][lane][remainingSwitches] = maxCoinsCollected;
48    }
49}
50
1class Solution {
2public:
3    long long maxCoins(vector<int>& lane1, vector<int>& lane2) {
4        int n = lane1.size(); // Get the number of elements in each lane.
5        long long answer = -1e18; // Initialize the answer with a very large negative number.
6      
7        // Create a 3D vector for memoization initialized with the large negative number.
8        vector<vector<vector<long long>>> memo(n, vector<vector<long long>>(2, vector<long long>(3, -1e18)));
9
10        // Define a recursive function using lambda for Depth First Search (DFS).
11        auto dfs = [&](auto&& self, int index, int lane, int swapsLeft) -> long long {
12            if (index >= n) {
13                return 0LL; // Base case: If index is out of bounds, no more coins can be collected.
14            }
15          
16            // If we have already computed this state, return the stored value.
17            if (memo[index][lane][swapsLeft] != -1e18) {
18                return memo[index][lane][swapsLeft];
19            }
20          
21            int currentCoin = lane == 0 ? lane1[index] : lane2[index]; // Get the coin value from the current lane.
22          
23            // Calculate the maximum coins by continuing in the same lane.
24            long long maxCoins = max((long long)currentCoin, self(self, index + 1, lane, swapsLeft) + currentCoin);
25          
26            // If we have swaps left, consider switching lanes.
27            if (swapsLeft > 0) {
28                // Try switching lanes at the next step, or right now.
29                maxCoins = max(maxCoins, self(self, index + 1, lane ^ 1, swapsLeft - 1) + currentCoin);
30                maxCoins = max(maxCoins, self(self, index, lane ^ 1, swapsLeft - 1));
31            }
32          
33            // Save the computed value for future use and return it.
34            return memo[index][lane][swapsLeft] = maxCoins;
35        };
36
37        // Try starting from every possible starting position in lane1 or lane2.
38        for (int i = 0; i < n; ++i) {
39            answer = max(answer, dfs(dfs, i, 0, 2)); // Start from lane1.
40        }
41
42        return answer; // Return the maximum coins that can be collected.
43    }
44};
45
1// Define a function `maxCoins` that computes the maximum number of coins collected from two lanes.
2// `lane1` and `lane2` represent arrays of coins in each respective lane.
3function maxCoins(lane1: number[], lane2: number[]): number {
4    const n = lane1.length; // Length of the lanes, assuming both have equal length.
5    const NEG_INF = -1e18; // A constant representing negative infinity for initialization.
6
7    // Create a 3D array 'f' to store computed results for dynamic programming.
8    // Dimensions: n x 2 x 3, initialized with NEG_INF.
9    const f: number[][][] = Array.from({ length: n }, () =>
10        Array.from({ length: 2 }, () => Array(3).fill(NEG_INF))
11    );
12
13    // Define a recursive function 'dfs' with memoization to explore all paths.
14    // This function takes the index `i`, lane `j`, and remaining switches `k`.
15    const dfs = (dfs: Function, i: number, j: number, k: number): number => {
16        if (i >= n) { // Boundary condition: If index exceeds bounds, return 0.
17            return 0;
18        }
19        if (f[i][j][k] !== NEG_INF) { // If already computed, return the stored value.
20            return f[i][j][k];
21        }
22
23        const x = j === 0 ? lane1[i] : lane2[i]; // Current coin value based on the lane.
24        // Calculate the max coins collected by either picking the current or moving forward without switching.
25        let ans = Math.max(x, dfs(dfs, i + 1, j, k) + x);
26
27        if (k > 0) { // If switches are available, explore switching lanes.
28            ans = Math.max(ans, dfs(dfs, i + 1, j ^ 1, k - 1) + x); // Switch and collect current.
29            ans = Math.max(ans, dfs(dfs, i, j ^ 1, k - 1)); // Switch without collecting current.
30        }
31
32        f[i][j][k] = ans; // Memoize the result.
33        return ans;
34    };
35
36    let ans = NEG_INF; // Variable to keep track of the maximum result.
37  
38    // Initiate DFS from both lanes at each position with 2 switches.
39    for (let i = 0; i < n; ++i) {
40        ans = Math.max(ans, dfs(dfs, i, 0, 2)); // Start from lane 1.
41    }
42
43    return ans; // Return the maximum number of coins collected.
44}
45

Time and Space Complexity

The time complexity of the code is O(n). This is because each call to dfs processes one position of the lanes, and thanks to memoization with @cache, each combination of (i, j, k) is computed at most once. Since the input lane1 or lane2 can have n elements and considering the maximum of 2 lane switches allowed (k=2), this processing remains O(n).

The space complexity of the code is O(n). The cache decorator uses additional space to store intermediate results for each possible combination of (i, j, k) values. The space needed for the cache depends linearly on the number of unique subproblems, which is proportional to the input size n.

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


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

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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


Load More