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, orx = 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)
.
- Drive one mile and then switch, which results in
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:
-
Recursive Function Design:
- We define a recursive function,
dfs(i, j, k)
, wherei
is the current mile,j
is the lane (0 or 1), andk
is the remaining allowable lane switches. - The function aims to calculate the maximum coins from the starting point
i
on lanej
withk
switches left.
- We define a recursive function,
-
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.
- If Mario reaches or exceeds the total number of miles (
-
Coin Calculation:
- Determine the coins on the current lane using
x = lane1[i]
ifj == 0
(lane 1) orx = lane2[i]
ifj == 1
(lane 2).
- Determine the coins on the current lane using
-
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)
.
- 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:
-
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: ifj
is 0 (lane 1),j ^ 1
will be 1 (lane 2), and vice versa.
- Mario can switch lanes either after driving 1 mile (
-
Memoization:
- We use Python's
functools.cache
to store previously computed results for states(i, j, k)
to prevent redundant calculations and improve efficiency.
- We use Python's
-
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 indexi
. - Finally, we return the maximum coins Mario can collect from any valid entry point.
- Iterate over all possible starting positions on the freeway. Since Mario must enter initially on lane 1, the computation is initialized as
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 EvaluatorExample 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:
-
Initialize the Recursive Function:
- Call
dfs(i, j, k)
wherei
is the starting mile,j
is the current lane (0 for lane1, 1 for lane2), andk
is the remaining lane changes.
- Call
-
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)
-
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
anddfs(1, 1, 1)
- Gain
-
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
anddfs(1, 0, 0)
- Gain
-
-
Mile 2 Calculations:
- Similar evaluations continue recursively for each subsequent mile, considering the gain from continuing on the same lane and potential switch options.
-
Optimization with Memoization:
- Store the results of
dfs(i, j, k)
to avoid recalculating and improve efficiency.
- Store the results of
-
Final Computation:
- Evaluate
dfs
starting at each possible mile withj=0 (lane1)
andk=2
switches remaining, and determine the maximum coins.
- Evaluate
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.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!