Maximum Earnings From Taxi
MedPrompt
There are n points on a road you are driving your taxi on. The n points on the road are labeled from 1 to n in the direction you are going, and you want to drive from point 1 to point n to make money by picking up passengers. You cannot change the direction of the taxi.
The passengers are represented by a 0-indexed 2D integer array rides, where rides[i] = [starti, endi, tipi] denotes the ith passenger requesting a ride from point starti to point endi who is willing to give a tipi dollar tip.
For each passenger i you pick up, you earn endi - starti + tipi dollars. You may only drive at most one passenger at a time.
Given n and rides, return the maximum number of dollars you can earn by picking up the passengers optimally.
Note: You may drop off a passenger and pick up a different passenger at the same point.
Example 1:
Input: n = 5, rides = [[2,5,4],[1,5,1]]
Output: 7
Explanation: We can pick up passenger 0 to earn 5 - 2 + 4 = 7 dollars.Example 2:
Input: n = 20, rides = [[1,6,1],[3,10,2],[10,12,3],[11,12,2],[12,15,2],[13,18,1]]
Output: 20
Explanation: We will pick up the following passengers:
- Drive passenger 1 from point 3 to point 10 for a profit of 10 - 3 + 2 = 9 dollars.
- Drive passenger 2 from point 10 to point 12 for a profit of 12 - 10 + 3 = 5 dollars.
- Drive passenger 5 from point 13 to point 18 for a profit of 18 - 13 + 1 = 6 dollars.
We earn 9 + 5 + 6 = 20 dollars in total.
Constraints:
1 <= n <= 1051 <= rides.length <= 3 * 104rides[i].length == 31 <= starti < endi <= n1 <= tipi <= 105
Approaches
3 approaches with complexity analysis and trade-offs.
This is the most efficient approach for the given constraints. It uses dynamic programming where the state is tied to the points on the road. We build a dp array where dp[i] stores the maximum earnings we can have by the time we reach point i. The solution is built iteratively by considering the maximum earnings from the previous point and the potential earnings from any rides that drop off a passenger at the current point.
Algorithm
- Preprocessing: Group the rides by their
endpoint. AHashMap<Integer, List<int[]>>is an effective way to do this, mapping eachendpoint to a list of rides that finish there. - DP Initialization: Create a
dparray of sizen + 1.dp[i]will represent the maximum earnings achievable up to pointion the road. Initialize alldpvalues to 0. - DP Iteration: Iterate from
i = 1ton.- For each point
i, we have two main possibilities to determinedp[i]:- No ride ends at
i: In this case, we just drove from pointi-1toiwithout a passenger. The maximum earnings are the same as the earnings up to pointi-1. So, we setdp[i] = dp[i-1]. - One or more rides end at
i: For each ride[start, end, tip]whereend == i, we calculate a potential new maximum earning. This would be the earnings from this ride (end - start + tip) plus the maximum earnings we had accumulated up to thestartpoint of this ride (dp[start]).
- No ride ends at
- We update
dp[i]to be the maximum ofdp[i-1]and all potential earnings calculated from rides ending ati.
- For each point
- Result: The final answer is the value stored in
dp[n], which represents the maximum earnings after reaching the final destination.
Walkthrough
This dynamic programming approach defines its state based on the location on the road. We let dp[i] be the maximum possible earnings after arriving at point i. Our goal is to compute dp[n].
The transition for dp[i] is based on what happens at point i. To arrive at point i, we could have either driven from i-1 without a passenger, or we could have just completed a ride that ended at i.
- Base case:
dp[0] = 0. No earnings before starting. - Transition: For each point
ifrom 1 ton:- The default maximum earning is inherited from the previous point:
dp[i] = dp[i-1]. - We then check if any rides end at point
i. To do this efficiently, we first preprocess theridesinto a map where keys are end points. For every ride[start, end, tip]withend == i, we calculate a potential new maximum earning:dp[start] + (end - start + tip). This represents the earnings from an optimal path tostart, followed by taking this specific ride. - We update
dp[i]with the maximum value found amongdp[i-1]and all possibilities from rides ending ati.
- The default maximum earning is inherited from the previous point:
This ensures that at each point i, dp[i] holds the optimal earnings. The final answer is dp[n].
import java.util.*; class Solution { public long maxTaxiEarnings(int n, int[][] rides) { // Group rides by their end point for efficient lookup. // The map stores: endPoint -> List of {startPoint, tip} Map<Integer, List<int[]>> ridesByEnd = new HashMap<>(); for (int[] ride : rides) { ridesByEnd.computeIfAbsent(ride[1], k -> new ArrayList<>()).add(new int[]{ride[0], ride[2]}); } // dp[i] will store the maximum earnings up to point i. long[] dp = new long[n + 1]; for (int i = 1; i <= n; i++) { // Option 1: Don't end a ride at point i. Earnings are same as up to i-1. dp[i] = dp[i - 1]; // Option 2: Check if any rides end at point i. if (ridesByEnd.containsKey(i)) { for (int[] rideInfo : ridesByEnd.get(i)) { int start = rideInfo[0]; int tip = rideInfo[1]; long profit = (long)i - start + tip; // Compare with current max and update if this path is better. dp[i] = Math.max(dp[i], dp[start] + profit); } } } return dp[n]; }}Complexity
Time
O(n + R) - Preprocessing the `R` rides into a map takes O(R). The main loop runs `n` times. Inside the loop, we access the map. Over all `n` iterations, the inner loop (iterating over rides ending at `i`) will execute a total of `R` times. Therefore, the total time complexity is O(n + R).
Space
O(n + R) - Requires O(n) for the DP array and O(R) for the map to store the preprocessed rides.
Trade-offs
Pros
Most efficient time complexity for the given constraints.
The DP state and transition directly model the problem's progression along the road, making it intuitive.
Cons
The space complexity is O(n + R), which can be large if
nis very large.
This approach uses a straightforward recursive method to explore all possible combinations of rides. It defines a function that, for each point on the road, decides between not taking a ride or taking one of the available rides starting at that point. It recursively calculates the potential earnings for each choice and returns the maximum.
Algorithm
- Define a recursive function, let's call it
solve(currentPoint), which calculates the maximum earnings possible starting fromcurrentPointall the way ton. - Base Case: If
currentPointis greater thann, it means we are past the destination, so no more earnings can be made. Return 0. - Recursive Step: At any
currentPoint, we have several choices:- Don't pick up any passenger: We can simply drive to the next point,
currentPoint + 1. The earnings from this choice would be whatever we can make fromcurrentPoint + 1onwards, which issolve(currentPoint + 1). - Pick up a passenger: We look for all available rides that start at
currentPoint. For each such ride[start, end, tip]wherestart == currentPoint, we can choose to take it. The earnings for this choice would be the profit from this single ride (end - start + tip) plus the maximum earnings we can get after completing the ride, which issolve(end).
- Don't pick up any passenger: We can simply drive to the next point,
- The function
solve(currentPoint)returns the maximum value among all these choices. - The initial call to start the process would be
solve(1).
Walkthrough
The brute-force recursive solution attempts to find the optimal earnings by exploring every valid sequence of rides. We define a function solve(i) that computes the maximum earnings from point i to n. At each point i, the function considers two main possibilities: skipping any ride and moving to i+1, or taking one of the available rides that start at i. If a ride is taken from i to j, the total earnings for that path are the ride's profit plus the result of a recursive call solve(j). The function then returns the maximum earnings found among all these possibilities. This method leads to an exponential number of calls for the same subproblems (e.g., solve(k) might be called from different paths), making it highly inefficient.
import java.util.*; class Solution { Map<Integer, List<int[]>> ridesByStart; int N; public long maxTaxiEarnings(int n, int[][] rides) { this.N = n; this.ridesByStart = new HashMap<>(); for (int[] ride : rides) { // Store rides by their start point for easy lookup // rideInfo: [end, tip] ridesByStart.computeIfAbsent(ride[0], k -> new ArrayList<>()).add(new int[]{ride[1], ride[2]}); } return solve(1); } private long solve(int currentPoint) { if (currentPoint > N) { return 0; } // Option 1: Don't take any ride, move to the next point long maxEarnings = solve(currentPoint + 1); // Option 2: Take a ride starting at currentPoint if (ridesByStart.containsKey(currentPoint)) { for (int[] rideInfo : ridesByStart.get(currentPoint)) { int end = rideInfo[0]; int tip = rideInfo[1]; long earnings = (long)end - currentPoint + tip; maxEarnings = Math.max(maxEarnings, earnings + solve(end)); } } return maxEarnings; }}Complexity
Time
Exponential - O(2^n) in the worst-case scenarios. The function may recompute solutions for the same points multiple times, leading to an exponential growth in the number of operations.
Space
O(n) - The space complexity is determined by the maximum depth of the recursion stack, which can go up to `n` in the worst case.
Trade-offs
Pros
Simple to understand and implement.
Directly models the decision-making process at each point.
Cons
Extremely inefficient due to a massive number of redundant computations.
Will result in a 'Time Limit Exceeded' (TLE) error for the given constraints.
This approach reframes the problem as a classic dynamic programming pattern known as Weighted Interval Scheduling. Each ride is treated as a 'job' with a start time, end time, and a weight (profit). The goal is to select a set of non-overlapping jobs to maximize the total weight. By sorting the rides by their end points, we can build up a solution iteratively.
Algorithm
- Preprocessing: Calculate the profit for each ride, where
profit = end - start + tip. - Sort: Sort the
ridesarray based on theirendpoints in ascending order. - DP Initialization: Create a
dparray of size equal to the number of rides,R.dp[i]will store the maximum earnings considering the firsti+1rides from the sorted list. - DP Iteration: Iterate through the sorted rides from
i = 0toR-1.- For each ride
i, calculate the profit of taking this ride:profit_i. - Find the latest non-conflicting ride
jthat finishes before rideistarts (i.e.,rides[j].end <= rides[i].start). This can be done efficiently using binary search on theendpoints of rides0toi-1. - The total earnings if we take ride
iisprofit_i + dp[j](if such ajexists). - The earnings if we don't take ride
iisdp[i-1](the max earnings from previous rides). - Set
dp[i] = max(profit_with_ride_i, profit_without_ride_i).
- For each ride
- Result: The last element of the
dparray,dp[R-1], will hold the maximum possible earnings.
Walkthrough
In this method, we treat each ride as an interval with an associated profit. The core idea is to make a decision for each ride: either take it or leave it.
First, we sort all rides by their end point. This allows us to process rides in the order they finish. We use a DP array, dp, where dp[i] stores the maximum earnings considering rides up to index i in the sorted array.
For each ride i, we have two choices:
- Include ride
i: We get its profit (end - start + tip). To this, we must add the maximum profit from rides that are compatible, i.e., rides that end before rideibegins. Since the rides are sorted byendpoint, we need to find the maximum profit from rides0toi-1that finish byrides[i].start. This can be found by locating the indexjof the last ride ending before or atrides[i].startand takingdp[j]. This search forjis optimized using binary search. - Exclude ride
i: The maximum profit is simply the maximum profit obtainable from rides0toi-1, which isdp[i-1].
dp[i] is the maximum of these two choices. The final answer is the maximum earnings after considering all rides, which is dp[R-1].
import java.util.Arrays; class Solution { public long maxTaxiEarnings(int n, int[][] rides) { Arrays.sort(rides, (a, b) -> Integer.compare(a[1], b[1])); int numRides = rides.length; if (numRides == 0) return 0; long[] dp = new long[numRides]; for (int i = 0; i < numRides; i++) { int start = rides[i][0]; int end = rides[i][1]; int tip = rides[i][2]; long profit = (long)end - start + tip; // Option 1: Take this ride long currentProfit = profit; int prevRideIndex = findLastNonConflicting(rides, i); if (prevRideIndex != -1) { currentProfit += dp[prevRideIndex]; } // Option 2: Don't take this ride long prevMaxProfit = (i > 0) ? dp[i - 1] : 0; dp[i] = Math.max(currentProfit, prevMaxProfit); } return dp[numRides - 1]; } // Binary search to find the index of the last ride that ends at or before `startTime` of the current ride. private int findLastNonConflicting(int[][] rides, int currentIndex) { int low = 0; int high = currentIndex - 1; int result = -1; int targetStartTime = rides[currentIndex][0]; while (low <= high) { int mid = low + (high - low) / 2; if (rides[mid][1] <= targetStartTime) { result = mid; low = mid + 1; } else { high = mid - 1; } } return result; }}Complexity
Time
O(R log R) - Sorting the rides takes O(R log R). The DP calculation involves a loop of size `R`, and inside each iteration, we perform a binary search which takes O(log R). Thus, the total time is dominated by O(R log R).
Space
O(R) - We need an array of size `R` (the number of rides) to store the DP states.
Trade-offs
Pros
More efficient than the brute-force approach.
Space complexity is O(R), which is better than the DP on points approach if
nis much larger thanR.
Cons
Can be slower than the DP on points approach if
R log Ris greater thann + R.The logic involving sorting and binary search is slightly more complex to implement correctly.
Solutions
Solution
class Solution { private int m ; private int [][] rides ; private Long [] f ; public long maxTaxiEarnings ( int n , int [][] rides ) { Arrays . sort ( rides , ( a , b ) -> a [ 0 ] - b [ 0 ]); m = rides . length ; f = new Long [ m ]; this . rides = rides ; return dfs ( 0 ); } private long dfs ( int i ) { if ( i >= m ) { return 0 ; } if ( f [ i ] != null ) { return f [ i ]; } int [] r = rides [ i ]; int st = r [ 0 ], ed = r [ 1 ], tip = r [ 2 ]; int j = search ( ed , i + 1 ); return f [ i ] = Math . max ( dfs ( i + 1 ), dfs ( j ) + ed - st + tip ); } private int search ( int x , int l ) { int r = m ; while ( l < r ) { int mid = ( l + r ) >> 1 ; if ( rides [ mid ][ 0 ] >= x ) { r = mid ; } else { l = mid + 1 ; } } return l ; } }class Solution {public: long long maxTaxiEarnings(int n, vector<vector<int>> &rides) { sort(rides.begin(), rides.end()); int m = rides.size(); long long f[m]; memset(f, -1, sizeof(f)); function<long long(int)> dfs = [&](int i) -> long long { if (i >= m) { return 0; } if (f[i] != -1) { return f[i]; } auto &r = rides[i]; int st = r[0], ed = r[1], tip = r[2]; int j = lower_bound(rides.begin() + i + 1, rides.end(), ed, [](auto &a, int val) { return a[0] < val; }) - rides.begin(); return f[i] = max(dfs(i + 1), dfs(j) + ed - st + tip); }; return dfs(0); }};class Solution : def maxTaxiEarnings ( self , n : int , rides : List [ List [ int ]]) -> int : @ cache def dfs ( i : int ) -> int : if i >= len ( rides ): return 0 st , ed , tip = rides [ i ] j = bisect_left ( rides , ed , lo = i + 1 , key = lambda x : x [ 0 ]) return max ( dfs ( i + 1 ), dfs ( j ) + ed - st + tip ) rides . sort () return dfs ( 0 )Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.