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:
- Where to enter the freeway
- Where to exit the freeway
- 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.
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:
- Exit immediately: Collect coins at current position and stop (represented by just returning
x
) - Continue in same lane: Collect coins and move to next mile without switching (
dfs(i + 1, j, k) + x
) - 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
) - 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:
-
Without switching lanes:
ans = max(x, dfs(i + 1, j, k) + x)
x
: Exit immediately after collecting coins at current positiondfs(i + 1, j, k) + x
: Continue to next mile in same lane
-
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 miledfs(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 EvaluatorExample 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:
- Exit immediately: collect 2 coins → total = 2
- Continue in lane 1: collect 2 + dfs(1, 0, 2)
- Switch to lane 2 for next mile: collect 2 + dfs(1, 1, 1)
- 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:
- Exit immediately: collect 4 coins → total = 2 + 4 = 6
- Continue in lane 2: collect 4 + dfs(2, 1, 1)
- Switch to lane 1 for next mile: collect 4 + dfs(2, 0, 0)
- 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:
- Exit immediately: collect 5 coins → total = 2 + 4 + 5 = 11
- 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:
- Exit immediately: collect 1 coin → total = 2 + 4 + 5 + 1 = 12
- 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 thedfs
function - The state space for
dfs(i, j, k)
is:i
: ranges from0
ton-1
(n possible values)j
: can be0
or1
(2 possible values)k
: can be0
,1
, or2
(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 callingdfs(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 index0
ton-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
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!