Facebook Pixel

3466. Maximum Coin Collection 🔒

Problem Description

Mario is driving on a two-lane freeway where coins are placed at every mile marker. You're given two integer arrays, lane1 and lane2, where each index i represents the coins at mile i in that respective lane.

The coin rules are:

  • Positive values mean Mario gains that many coins
  • Negative values mean Mario pays a toll and loses that many coins (absolute value)

Mario's driving constraints:

  • He always starts in lane 1
  • He can enter the freeway at any mile marker
  • He must drive at least 1 mile before exiting
  • He can exit at any point after driving at least 1 mile
  • He can switch lanes at most 2 times total
  • Lane switches can happen immediately upon entering or just before exiting

A lane switch means moving from lane 1 to lane 2 or vice versa.

Your task is to find the maximum number of coins Mario can collect by choosing:

  1. Where to enter the freeway
  2. Where to exit the freeway
  3. When to switch lanes (up to 2 times)

For example, if lane1 = [2, -3, 5] and lane2 = [1, 4, -2], Mario could:

  • Enter at mile 0 in lane 1, collect 2 coins
  • Switch to lane 2, collect 4 coins at mile 1
  • Switch back to lane 1, collect 5 coins at mile 2
  • Exit after mile 2
  • Total: 2 + 4 + 5 = 11 coins (using 2 lane switches)

The goal is to maximize the total coins collected while respecting all constraints.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that Mario's journey can be broken down into a series of decisions at each mile: should he continue driving or exit, and should he switch lanes or stay in the current lane?

Since Mario can enter at any position and has limited lane switches (at most 2), we need to explore all possible starting points and track how many lane switches remain at each step. This naturally leads to a recursive approach where we model the state as:

  • Current position i (which mile we're at)
  • Current lane j (0 for lane 1, 1 for lane 2)
  • Remaining lane switches k (starts at 2, decreases with each switch)

At each position, Mario faces multiple choices:

  1. Exit immediately: Collect coins at current position and stop (represented by just returning x)
  2. Continue in same lane: Collect coins and move to next mile without switching (dfs(i + 1, j, k) + x)
  3. Switch lanes then continue: If switches remain, collect coins at current position then switch for the next mile (dfs(i + 1, j ^ 1, k - 1) + x)
  4. Switch lanes immediately: If switches remain, switch to the other lane at the same position (dfs(i, j ^ 1, k - 1))

The XOR operation j ^ 1 elegantly toggles between lanes (0 becomes 1, 1 becomes 0).

Since we don't know the optimal starting position beforehand, we try all possible starting points (from mile 0 to mile n-1) and take the maximum result. Each starting point explores its own subtree of possibilities with 2 lane switches available.

Memoization is crucial here because the same state (i, j, k) can be reached through different paths. For instance, starting at mile 0 and driving to mile 3 with 1 switch remaining could happen via multiple switch patterns, but the optimal solution from that state onward remains the same.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution implements a memoized depth-first search using Python's @cache decorator to avoid redundant calculations.

Function Definition: The core function dfs(i, j, k) represents:

  • i: Current position (mile marker)
  • j: Current lane (0 for lane1, 1 for lane2)
  • k: Number of lane switches remaining

Base Case: When i >= n, Mario has gone beyond the freeway, so we return 0.

State Transitions: At each valid position, we first determine the coins at the current location:

x = lane1[i] if j == 0 else lane2[i]

Then we calculate the maximum coins possible from this state:

  1. Without switching lanes:

    ans = max(x, dfs(i + 1, j, k) + x)
    • x: Exit immediately after collecting coins at current position
    • dfs(i + 1, j, k) + x: Continue to next mile in same lane
  2. With lane switching (if k > 0):

    ans = max(ans, dfs(i + 1, j ^ 1, k - 1) + x)
    ans = max(ans, dfs(i, j ^ 1, k - 1))
    • dfs(i + 1, j ^ 1, k - 1) + x: Collect coins at current position, then switch lanes for next mile
    • dfs(i, j ^ 1, k - 1): Switch lanes immediately without collecting coins at current position (essentially entering the other lane at this mile)

The XOR operation j ^ 1 toggles between lanes: if j = 0 (lane1), then j ^ 1 = 1 (lane2), and vice versa.

Main Loop: Since Mario can enter at any mile marker, we iterate through all possible starting positions:

for i in range(n):
    ans = max(ans, dfs(i, 0, 2))

Each call starts at position i in lane 1 (j = 0) with 2 lane switches available (k = 2).

Memoization: The @cache decorator automatically stores results for each unique combination of (i, j, k). This prevents recalculating the same subproblem multiple times, reducing time complexity from exponential to polynomial.

Time Complexity: O(n × 2 × 3) = O(n) where n is the length of the arrays. We have at most n positions, 2 lanes, and 3 possible values for remaining switches (0, 1, 2).

Space Complexity: O(n) for the memoization cache and recursion stack.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a small example with lane1 = [2, -3, 5, 1] and lane2 = [1, 4, -2, 3].

We'll explore one optimal path to understand how the DFS works:

Starting at mile 0 in lane 1 (i=0, j=0, k=2):

  • Current position has 2 coins in lane 1
  • Options:
    1. Exit immediately: collect 2 coins → total = 2
    2. Continue in lane 1: collect 2 + dfs(1, 0, 2)
    3. Switch to lane 2 for next mile: collect 2 + dfs(1, 1, 1)
    4. Switch to lane 2 immediately: dfs(0, 1, 2)

Let's follow option 3: collect 2 coins and switch to lane 2.

At mile 1 in lane 2 (i=1, j=1, k=1):

  • Current position has 4 coins in lane 2
  • Options:
    1. Exit immediately: collect 4 coins → total = 2 + 4 = 6
    2. Continue in lane 2: collect 4 + dfs(2, 1, 1)
    3. Switch to lane 1 for next mile: collect 4 + dfs(2, 0, 0)
    4. Switch to lane 1 immediately: dfs(1, 0, 0)

Let's follow option 3: collect 4 coins and switch back to lane 1.

At mile 2 in lane 1 (i=2, j=0, k=0):

  • Current position has 5 coins in lane 1
  • No more switches available, so options are:
    1. Exit immediately: collect 5 coins → total = 2 + 4 + 5 = 11
    2. Continue in lane 1: collect 5 + dfs(3, 0, 0)

Let's check option 2:

At mile 3 in lane 1 (i=3, j=0, k=0):

  • Current position has 1 coin in lane 1
  • Options:
    1. Exit immediately: collect 1 coin → total = 2 + 4 + 5 + 1 = 12
    2. Continue (but i+1 = 4 >= n, so returns 0): total = 12

So this path gives us 12 coins total.

The algorithm explores all such paths from every starting position. The memoization ensures that when we encounter the same state (i, j, k) again through a different path, we don't recalculate it. For instance, if another path reaches mile 2 in lane 1 with 0 switches remaining, we already know the optimal value from that state.

The final answer would be the maximum value found across all starting positions and all possible paths, which in this case would be 12 coins (or potentially higher if another path yields more).

Solution Implementation

1class Solution:
2    def maxCoins(self, lane1: List[int], lane2: List[int]) -> int:
3        from functools import cache
4      
5        @cache
6        def dfs(position: int, current_lane: int, switches_left: int) -> int:
7            """
8            Dynamic programming function to find maximum coins.
9          
10            Args:
11                position: Current position in the lanes
12                current_lane: Current lane (0 for lane1, 1 for lane2)
13                switches_left: Number of lane switches remaining
14          
15            Returns:
16                Maximum coins that can be collected from this state
17            """
18            # Base case: reached end of lanes
19            if position >= n:
20                return 0
21          
22            # Get coin value at current position based on current lane
23            current_coin = lane1[position] if current_lane == 0 else lane2[position]
24          
25            # Option 1: Collect current coin and continue in same lane
26            # Option 2: Just collect current coin (end here)
27            max_coins = max(current_coin, dfs(position + 1, current_lane, switches_left) + current_coin)
28          
29            # If we have switches left, explore switching options
30            if switches_left > 0:
31                # Option 3: Switch lane and collect coin at next position
32                max_coins = max(max_coins, dfs(position + 1, current_lane ^ 1, switches_left - 1) + current_coin)
33                # Option 4: Switch lane at current position (don't collect current coin)
34                max_coins = max(max_coins, dfs(position, current_lane ^ 1, switches_left - 1))
35          
36            return max_coins
37      
38        n = len(lane1)
39        result = float('-inf')
40      
41        # Try starting from each position in lane1 (lane 0)
42        for start_position in range(n):
43            result = max(result, dfs(start_position, 0, 2))
44      
45        return result
46
1class Solution {
2    // Total number of positions in the lanes
3    private int totalPositions;
4  
5    // Arrays representing coins in each lane
6    private int[] topLane;
7    private int[] bottomLane;
8  
9    // Memoization table: [position][current lane (0=top, 1=bottom)][remaining switches]
10    private Long[][][] memo;
11
12    /**
13     * Finds the maximum coins that can be collected from two lanes
14     * with at most 2 lane switches allowed.
15     * 
16     * @param lane1 Array of coins in the first lane
17     * @param lane2 Array of coins in the second lane
18     * @return Maximum coins that can be collected
19     */
20    public long maxCoins(int[] lane1, int[] lane2) {
21        // Initialize instance variables
22        totalPositions = lane1.length;
23        this.topLane = lane1;
24        this.bottomLane = lane2;
25      
26        // Initialize memoization table
27        // Dimensions: [position][lane (0 or 1)][switches remaining (0, 1, or 2)]
28        memo = new Long[totalPositions][2][3];
29      
30        // Try starting from each position and find the maximum
31        long maxCoins = Long.MIN_VALUE;
32        for (int startPosition = 0; startPosition < totalPositions; ++startPosition) {
33            // Start from position i, lane 0 (top), with 2 switches available
34            maxCoins = Math.max(maxCoins, findMaxCoins(startPosition, 0, 2));
35        }
36      
37        return maxCoins;
38    }
39
40    /**
41     * Recursively finds maximum coins using dynamic programming with memoization.
42     * 
43     * @param position Current position index
44     * @param currentLane Current lane (0 for top, 1 for bottom)
45     * @param switchesRemaining Number of lane switches still available
46     * @return Maximum coins achievable from current state
47     */
48    private long findMaxCoins(int position, int currentLane, int switchesRemaining) {
49        // Base case: reached beyond the last position
50        if (position >= totalPositions) {
51            return 0;
52        }
53      
54        // Check if this state has been computed before
55        if (memo[position][currentLane][switchesRemaining] != null) {
56            return memo[position][currentLane][switchesRemaining];
57        }
58      
59        // Get coin value at current position and lane
60        int currentCoinValue = (currentLane == 0) ? topLane[position] : bottomLane[position];
61      
62        // Option 1: Take current coin only (no further moves)
63        long maxResult = currentCoinValue;
64      
65        // Option 2: Take current coin and continue in the same lane
66        maxResult = Math.max(maxResult, 
67                           findMaxCoins(position + 1, currentLane, switchesRemaining) + currentCoinValue);
68      
69        // If switches are available, explore switching options
70        if (switchesRemaining > 0) {
71            // Option 3: Take current coin, then switch lane and continue
72            int oppositeLane = currentLane ^ 1;  // XOR to toggle between 0 and 1
73            maxResult = Math.max(maxResult, 
74                               findMaxCoins(position + 1, oppositeLane, switchesRemaining - 1) + currentCoinValue);
75          
76            // Option 4: Switch lane immediately without taking current coin
77            maxResult = Math.max(maxResult, 
78                               findMaxCoins(position, oppositeLane, switchesRemaining - 1));
79        }
80      
81        // Store result in memoization table and return
82        memo[position][currentLane][switchesRemaining] = maxResult;
83        return maxResult;
84    }
85}
86
1class Solution {
2public:
3    long long maxCoins(vector<int>& lane1, vector<int>& lane2) {
4        int n = lane1.size();
5        long long maxResult = -1e18;
6      
7        // dp[position][currentLane][remainingSwitches] = maximum coins collectible
8        // position: current position in lanes (0 to n-1)
9        // currentLane: 0 for lane1, 1 for lane2
10        // remainingSwitches: number of lane switches remaining (0 to 2)
11        vector<vector<vector<long long>>> dp(n, vector<vector<long long>>(2, vector<long long>(3, -1e18)));
12      
13        // Recursive function with memoization to find maximum coins
14        auto findMaxCoins = [&](this auto&& findMaxCoins, int position, int currentLane, int remainingSwitches) -> long long {
15            // Base case: reached end of lanes
16            if (position >= n) {
17                return 0LL;
18            }
19          
20            // Return memoized result if already computed
21            if (dp[position][currentLane][remainingSwitches] != -1e18) {
22                return dp[position][currentLane][remainingSwitches];
23            }
24          
25            // Get coin value at current position and lane
26            int currentCoin = (currentLane == 0) ? lane1[position] : lane2[position];
27          
28            // Option 1: Take current coin only (start collecting from here)
29            long long result = static_cast<long long>(currentCoin);
30          
31            // Option 2: Take current coin and continue collecting in same lane
32            result = max(result, findMaxCoins(position + 1, currentLane, remainingSwitches) + currentCoin);
33          
34            // If we have switches remaining, consider switching lanes
35            if (remainingSwitches > 0) {
36                // Option 3: Take current coin, then switch lane for next position
37                result = max(result, findMaxCoins(position + 1, currentLane ^ 1, remainingSwitches - 1) + currentCoin);
38              
39                // Option 4: Switch lane immediately without taking current coin
40                result = max(result, findMaxCoins(position, currentLane ^ 1, remainingSwitches - 1));
41            }
42          
43            // Memoize and return the result
44            return dp[position][currentLane][remainingSwitches] = result;
45        };
46      
47        // Try starting from each position in lane1 (lane 0) with 2 switches available
48        for (int startPos = 0; startPos < n; ++startPos) {
49            maxResult = max(maxResult, findMaxCoins(startPos, 0, 2));
50        }
51      
52        return maxResult;
53    }
54};
55
1/**
2 * Calculates the maximum coins that can be collected from two lanes
3 * @param lane1 - Array of coins in the first lane
4 * @param lane2 - Array of coins in the second lane
5 * @returns Maximum coins that can be collected
6 */
7function maxCoins(lane1: number[], lane2: number[]): number {
8    const n: number = lane1.length;
9    const NEGATIVE_INFINITY: number = -1e18;
10  
11    // Initialize memoization table
12    // dp[position][lane][remainingSwitches] = maximum coins from this state
13    const dp: number[][][] = Array.from({ length: n }, () =>
14        Array.from({ length: 2 }, () => Array(3).fill(NEGATIVE_INFINITY))
15    );
16  
17    /**
18     * Depth-first search with memoization to find maximum coins
19     * @param position - Current position in the lanes
20     * @param currentLane - Current lane (0 for lane1, 1 for lane2)
21     * @param remainingSwitches - Number of lane switches remaining
22     * @returns Maximum coins that can be collected from current state
23     */
24    const dfs = (position: number, currentLane: number, remainingSwitches: number): number => {
25        // Base case: reached end of lanes
26        if (position >= n) {
27            return 0;
28        }
29      
30        // Return memoized result if already computed
31        if (dp[position][currentLane][remainingSwitches] !== NEGATIVE_INFINITY) {
32            return dp[position][currentLane][remainingSwitches];
33        }
34      
35        // Get coin value at current position and lane
36        const currentCoinValue: number = currentLane === 0 ? lane1[position] : lane2[position];
37      
38        // Option 1: Take current coin and continue in same lane
39        // Option 2: Just take current coin (end collection here)
40        let maxCoinsFromHere: number = Math.max(
41            currentCoinValue,
42            dfs(position + 1, currentLane, remainingSwitches) + currentCoinValue
43        );
44      
45        // If we have switches remaining, consider switching lanes
46        if (remainingSwitches > 0) {
47            // Option 3: Take current coin and switch to other lane for next position
48            maxCoinsFromHere = Math.max(
49                maxCoinsFromHere,
50                dfs(position + 1, currentLane ^ 1, remainingSwitches - 1) + currentCoinValue
51            );
52          
53            // Option 4: Switch lane immediately without taking current coin
54            maxCoinsFromHere = Math.max(
55                maxCoinsFromHere,
56                dfs(position, currentLane ^ 1, remainingSwitches - 1)
57            );
58        }
59      
60        // Store result in memoization table
61        dp[position][currentLane][remainingSwitches] = maxCoinsFromHere;
62        return maxCoinsFromHere;
63    };
64  
65    // Try starting from each position in lane 0 with 2 switches available
66    let maxTotalCoins: number = NEGATIVE_INFINITY;
67    for (let startPosition = 0; startPosition < n; startPosition++) {
68        maxTotalCoins = Math.max(
69            maxTotalCoins,
70            dfs(startPosition, 0, 2)
71        );
72    }
73  
74    return maxTotalCoins;
75}
76

Time and Space Complexity

The time complexity is O(n) and the space complexity is O(n), where n represents the length of the lanes.

Time Complexity Analysis:

  • The function uses memoization with @cache decorator on the dfs function
  • The state space for dfs(i, j, k) is:
    • i: ranges from 0 to n-1 (n possible values)
    • j: can be 0 or 1 (2 possible values)
    • k: can be 0, 1, or 2 (3 possible values)
  • Total number of unique states = n × 2 × 3 = 6n
  • Each state is computed at most once due to memoization
  • Within each state computation, the operations are O(1)
  • The outer loop in the main function runs n times, each calling dfs(i, 0, 2)
  • Overall time complexity: O(n) + O(6n) = O(n)

Space Complexity Analysis:

  • The memoization cache stores at most 6n states
  • The recursion depth is at most n (when traversing from index 0 to n-1)
  • Space for cache: O(6n) = O(n)
  • Space for recursion stack: O(n)
  • Overall space complexity: O(n)

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

Common Pitfalls

1. Incorrect Interpretation of Lane Switching at Entry/Exit

Pitfall: A common misunderstanding is how lane switches work when entering or exiting the freeway. The problem states that Mario "always starts in lane 1" but "lane switches can happen immediately upon entering." This might lead to confusion about whether switching upon entry counts as using one of the two allowed switches.

Issue in Code: The recursive call dfs(position, current_lane ^ 1, switches_left - 1) allows switching without collecting coins at the current position, which effectively means entering at that position in the other lane. However, this contradicts the constraint that Mario "always starts in lane 1."

Solution: When starting the journey (in the main loop), Mario must always begin in lane 1. The ability to switch "immediately upon entering" means he can switch right after collecting the first coin, not before. The code should be modified to ensure the initial entry is always in lane 1:

# Remove the option to switch without collecting at current position
if switches_left > 0:
    # Only allow switching AFTER collecting current coin
    max_coins = max(max_coins, dfs(position + 1, current_lane ^ 1, switches_left - 1) + current_coin)
    # Remove: max_coins = max(max_coins, dfs(position, current_lane ^ 1, switches_left - 1))

2. Not Enforcing Minimum Drive Distance

Pitfall: The problem states "He must drive at least 1 mile before exiting," which means Mario must collect coins from at least 2 positions (entry point + 1 more mile). The current code allows exiting immediately after entering by returning just current_coin.

Solution: Modify the base case and ensure at least two positions are visited:

@cache
def dfs(position: int, current_lane: int, switches_left: int, miles_driven: int) -> int:
    if position >= n:
        return 0 if miles_driven >= 1 else float('-inf')  # Invalid if haven't driven at least 1 mile
  
    current_coin = lane1[position] if current_lane == 0 else lane2[position]
  
    # Can only exit if we've driven at least 1 mile
    if miles_driven >= 1:
        max_coins = current_coin  # Option to exit here
    else:
        max_coins = float('-inf')  # Can't exit yet
  
    # Continue driving
    max_coins = max(max_coins, dfs(position + 1, current_lane, switches_left, miles_driven + 1) + current_coin)
  
    # Lane switching options...

3. Edge Case: Empty or Single-Element Arrays

Pitfall: If the lanes have only one element, Mario cannot satisfy the "drive at least 1 mile" requirement since he needs to visit at least 2 positions.

Solution: Add validation at the beginning:

def maxCoins(self, lane1: List[int], lane2: List[int]) -> int:
    n = len(lane1)
    if n < 2:
        return 0  # Cannot drive at least 1 mile with fewer than 2 positions
    # ... rest of the code
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?


Recommended Readings

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

Load More