Facebook Pixel

2008. Maximum Earnings From Taxi

Problem Description

You are a taxi driver on a road with n points labeled from 1 to n. You start at point 1 and want to drive to point n in one direction only (you cannot go backwards).

Along the way, there are passengers waiting for rides. Each passenger is represented by [start_i, end_i, tip_i], meaning:

  • They want a ride from point start_i to point end_i
  • They will give you a tip_i dollar tip

When you pick up passenger i, you earn end_i - start_i + tip_i dollars (the distance traveled plus the tip).

Important constraints:

  • You can only carry one passenger at a time
  • You cannot change direction (always moving from point 1 toward point n)
  • You can drop off one passenger and immediately pick up another at the same point

Given n (the number of points) and a list of rides (passenger requests), find the maximum amount of money you can earn by choosing which passengers to pick up optimally.

Example scenario: If you have passengers A and B where A wants to go from point 2 to 5 (with tip 3) and B wants to go from point 3 to 7 (with tip 5), you cannot take both since their rides overlap. You must choose which combination of non-overlapping rides gives you the most money.

The solution uses dynamic programming with memoization. After sorting rides by start point, for each ride position, it decides whether to take the current ride or skip it. If taken, it uses binary search to find the next possible ride that doesn't overlap (starts at or after the current ride's end point) and calculates the maximum earnings recursively.

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

Intuition

Think of this problem as making a series of decisions: for each potential ride, should we take it or skip it?

The key insight is that once we sort the rides by their starting points, we're essentially moving through them in the order we'd encounter them on our journey. This transforms the problem into a classic dynamic programming scenario where we need to make optimal choices at each step.

For any given ride at position i, we face two choices:

  1. Skip this ride - Continue to the next available ride and see what maximum earnings we can get from there
  2. Take this ride - Earn money from this ride, then find the next compatible ride we can take after dropping off this passenger

The challenge with option 2 is finding which rides are "compatible" after taking the current one. Since we can't carry multiple passengers simultaneously, we need to find rides that start at or after our current ride's end point. Because we've sorted the rides by start time, we can use binary search to efficiently find the first compatible ride.

This naturally leads to a recursive structure: the maximum earnings from position i equals the maximum of:

  • Maximum earnings from position i + 1 (if we skip)
  • Current ride's earnings + maximum earnings from the next compatible position (if we take it)

The formula becomes: dfs(i) = max(dfs(i + 1), earnings_from_ride_i + dfs(j)) where j is the index of the first ride that starts at or after the current ride's end point.

By using memoization, we avoid recalculating the same subproblems multiple times, making our solution efficient. The sorting step ensures we process rides in a logical order, and binary search helps us quickly find compatible subsequent rides, bringing the overall approach together elegantly.

Learn more about Binary Search, Dynamic Programming and Sorting patterns.

Solution Approach

The solution implements a dynamic programming approach with memoization and binary search optimization.

Step 1: Sort the rides

rides.sort()

We sort all rides by their starting points in ascending order. This ensures we process rides in the order we'd encounter them while traveling from point 1 to n.

Step 2: Define the recursive function with memoization

@cache
def dfs(i: int) -> int:

The dfs(i) function calculates the maximum earnings possible starting from the i-th ride. The @cache decorator provides memoization to avoid redundant calculations.

Step 3: Base case

if i >= len(rides):
    return 0

When we've considered all rides (index out of bounds), there's no more money to be earned, so return 0.

Step 4: Extract current ride information

st, ed, tip = rides[i]

Get the start point, end point, and tip for the current ride.

Step 5: Find the next compatible ride using binary search

j = bisect_left(rides, ed, lo=i + 1, key=lambda x: x[0])

Use bisect_left to find the first ride whose start point is greater than or equal to the current ride's end point (ed). The search starts from i + 1 (rides after the current one) and uses key=lambda x: x[0] to compare based on start points. This gives us index j of the first compatible ride if we take the current one.

Step 6: Make the optimal decision

return max(dfs(i + 1), dfs(j) + ed - st + tip)

Compare two options:

  • dfs(i + 1): Skip the current ride and consider rides starting from the next position
  • dfs(j) + ed - st + tip: Take the current ride (earning ed - st + tip dollars) and then optimally handle rides starting from position j

The function returns the maximum of these two choices.

Step 7: Start the recursion

return dfs(0)

Begin the process from the first ride (index 0) to get the maximum possible earnings.

The time complexity is O(n log n) for sorting plus O(m^2 log m) for the memoized recursion with binary search, where m is the number of rides. The space complexity is O(m) for the memoization cache.

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 n = 7 and the following rides:

  • Ride A: [1, 4, 2] - From point 1 to 4, tip 2(earns41+2=2 (earns 4-1+2 = 5)
  • Ride B: [2, 5, 3] - From point 2 to 5, tip 3(earns52+3=3 (earns 5-2+3 = 6)
  • Ride C: [5, 7, 1] - From point 5 to 7, tip 1(earns75+1=1 (earns 7-5+1 = 3)

Step 1: Sort rides by start point After sorting: [[1,4,2], [2,5,3], [5,7,1]] Let's index them as positions 0, 1, and 2.

Step 2: Begin recursion from position 0 We call dfs(0) to find the maximum earnings starting from the first ride.

Step 3: At position 0 (Ride A: [1,4,2]) We have two choices:

  • Skip it: Call dfs(1) to see what we can earn from position 1 onward
  • Take it: Earn $5, then find the next compatible ride
    • Using binary search: Find first ride starting at or after point 4
    • This gives us position 2 (Ride C starts at point 5)
    • So we calculate: $5 + dfs(2)

Step 4: Evaluate dfs(1) - Position 1 (Ride B: [2,5,3]) Two choices:

  • Skip it: Call dfs(2)
  • Take it: Earn $6, then find next compatible ride
    • Binary search finds position 2 (Ride C starts at point 5)
    • Calculate: $6 + dfs(2)

Step 5: Evaluate dfs(2) - Position 2 (Ride C: [5,7,1]) Two choices:

  • Skip it: Call dfs(3) which returns 0 (no more rides)
  • Take it: Earn $3, then call dfs(3) which returns 0
  • Result: max(0, 3) = 3

Step 6: Backtrack and compute

  • dfs(2) = 3
  • dfs(1) = max(dfs(2), 6 + dfs(2)) = max(3, 6 + 3) = 9
  • dfs(0) = max(dfs(1), 5 + dfs(2)) = max(9, 5 + 3) = 9

Final Answer: $9

The optimal strategy is to skip Ride A and take both Ride B and Ride C, earning 6+6 + 3 = 9.NotethatifwehadtakenRideA(earning9. Note that if we had taken Ride A (earning 5), we couldn't take Ride B (they overlap), and could only add Ride C for a total of $8.

Solution Implementation

1from typing import List
2from functools import cache
3from bisect import bisect_left
4
5class Solution:
6    def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
7        """
8        Calculate maximum earnings from taxi rides.
9      
10        Args:
11            n: The destination point (1 to n points on the road)
12            rides: List of rides where each ride is [start, end, tip]
13      
14        Returns:
15            Maximum possible earnings from completing non-overlapping rides
16        """
17      
18        @cache
19        def dfs(ride_index: int) -> int:
20            """
21            Recursively find maximum earnings starting from ride_index.
22          
23            Args:
24                ride_index: Current index in the sorted rides list
25          
26            Returns:
27                Maximum earnings achievable from ride_index onwards
28            """
29            # Base case: no more rides to consider
30            if ride_index >= len(rides):
31                return 0
32          
33            # Extract current ride details
34            start_point, end_point, tip_amount = rides[ride_index]
35          
36            # Find the next compatible ride (first ride that starts at or after current ride ends)
37            # Using binary search for efficiency
38            next_compatible_ride = bisect_left(
39                rides, 
40                end_point, 
41                lo=ride_index + 1, 
42                key=lambda ride: ride[0]  # Compare based on start point
43            )
44          
45            # Two choices for current ride:
46            # 1. Skip current ride and move to next
47            skip_current = dfs(ride_index + 1)
48          
49            # 2. Take current ride and add compatible rides
50            # Earnings = distance traveled + tip
51            take_current = dfs(next_compatible_ride) + (end_point - start_point) + tip_amount
52          
53            # Return maximum of both choices
54            return max(skip_current, take_current)
55      
56        # Sort rides by start point for binary search to work
57        rides.sort()
58      
59        # Start recursion from first ride
60        return dfs(0)
61
1class Solution {
2    private int totalRides;
3    private int[][] rides;
4    private Long[] memo;
5
6    /**
7     * Calculates maximum earnings from taxi rides
8     * @param n - maximum distance/time point
9     * @param rides - array of rides where each ride is [start, end, tip]
10     * @return maximum possible earnings
11     */
12    public long maxTaxiEarnings(int n, int[][] rides) {
13        // Sort rides by start time for processing in order
14        Arrays.sort(rides, (a, b) -> a[0] - b[0]);
15      
16        // Initialize instance variables
17        totalRides = rides.length;
18        this.rides = rides;
19        memo = new Long[totalRides];
20      
21        // Start dynamic programming from first ride
22        return dfs(0);
23    }
24
25    /**
26     * Dynamic programming function to find maximum earnings starting from ride index i
27     * @param i - current ride index to consider
28     * @return maximum earnings possible from ride i onwards
29     */
30    private long dfs(int i) {
31        // Base case: no more rides available
32        if (i >= totalRides) {
33            return 0;
34        }
35      
36        // Return memoized result if already computed
37        if (memo[i] != null) {
38            return memo[i];
39        }
40      
41        // Extract current ride information
42        int[] currentRide = rides[i];
43        int startPoint = currentRide[0];
44        int endPoint = currentRide[1];
45        int tipAmount = currentRide[2];
46      
47        // Find next compatible ride (first ride that starts at or after current ride ends)
48        int nextCompatibleRideIndex = binarySearchNextRide(endPoint, i + 1);
49      
50        // Choose maximum between:
51        // 1. Skip current ride and process next ride
52        // 2. Take current ride (earn fare + tip) and process next compatible ride
53        long skipCurrentRide = dfs(i + 1);
54        long takeCurrentRide = dfs(nextCompatibleRideIndex) + (endPoint - startPoint) + tipAmount;
55      
56        // Memoize and return the maximum earnings
57        return memo[i] = Math.max(skipCurrentRide, takeCurrentRide);
58    }
59
60    /**
61     * Binary search to find the first ride that starts at or after given time
62     * @param targetStartTime - minimum start time for the next ride
63     * @param leftIndex - starting index for search
64     * @return index of first ride with start time >= targetStartTime, or totalRides if none exists
65     */
66    private int binarySearchNextRide(int targetStartTime, int leftIndex) {
67        int rightIndex = totalRides;
68      
69        while (leftIndex < rightIndex) {
70            int midIndex = (leftIndex + rightIndex) >> 1;
71          
72            if (rides[midIndex][0] >= targetStartTime) {
73                // Mid ride starts at or after target, search left half including mid
74                rightIndex = midIndex;
75            } else {
76                // Mid ride starts before target, search right half excluding mid
77                leftIndex = midIndex + 1;
78            }
79        }
80      
81        return leftIndex;
82    }
83}
84
1class Solution {
2public:
3    long long maxTaxiEarnings(int n, vector<vector<int>>& rides) {
4        // Sort rides by start time to enable binary search
5        sort(rides.begin(), rides.end());
6      
7        int totalRides = rides.size();
8      
9        // Dynamic programming memoization array
10        // dp[i] represents the maximum earnings starting from ride i
11        long long dp[totalRides];
12        memset(dp, -1, sizeof(dp));
13      
14        // Recursive function with memoization to calculate maximum earnings
15        function<long long(int)> calculateMaxEarnings = [&](int currentIndex) -> long long {
16            // Base case: no more rides available
17            if (currentIndex >= totalRides) {
18                return 0;
19            }
20          
21            // Return memoized result if already computed
22            if (dp[currentIndex] != -1) {
23                return dp[currentIndex];
24            }
25          
26            // Extract current ride information
27            auto& currentRide = rides[currentIndex];
28            int startPoint = currentRide[0];
29            int endPoint = currentRide[1];
30            int tipAmount = currentRide[2];
31          
32            // Find the next compatible ride (first ride that starts at or after current ride's end)
33            int nextCompatibleIndex = lower_bound(
34                rides.begin() + currentIndex + 1, 
35                rides.end(), 
36                endPoint,
37                [](auto& ride, int value) { return ride[0] < value; }
38            ) - rides.begin();
39          
40            // Calculate earnings for current ride: distance + tip
41            long long earningsWithCurrentRide = (endPoint - startPoint) + tipAmount + calculateMaxEarnings(nextCompatibleIndex);
42          
43            // Calculate earnings if we skip current ride
44            long long earningsWithoutCurrentRide = calculateMaxEarnings(currentIndex + 1);
45          
46            // Store and return the maximum of both options
47            return dp[currentIndex] = max(earningsWithoutCurrentRide, earningsWithCurrentRide);
48        };
49      
50        // Start calculation from the first ride
51        return calculateMaxEarnings(0);
52    }
53};
54
1/**
2 * Calculates the maximum earnings from taxi rides
3 * @param n - The number of points on the road (1 to n)
4 * @param rides - Array of rides where each ride is [start, end, tip]
5 * @returns Maximum earnings possible
6 */
7function maxTaxiEarnings(n: number, rides: number[][]): number {
8    // Sort rides by start point in ascending order
9    rides.sort((a: number[], b: number[]) => a[0] - b[0]);
10  
11    const totalRides: number = rides.length;
12    // Memoization array for dynamic programming (-1 indicates uncomputed)
13    const memo: number[] = Array(totalRides).fill(-1);
14  
15    /**
16     * Binary search to find the first ride that starts at or after the given point
17     * @param targetStart - The start point to search for
18     * @param leftIndex - Starting index for the search
19     * @returns Index of the first ride starting at or after targetStart
20     */
21    const binarySearchNextRide = (targetStart: number, leftIndex: number): number => {
22        let rightIndex: number = totalRides;
23      
24        while (leftIndex < rightIndex) {
25            const midIndex: number = (leftIndex + rightIndex) >> 1;
26          
27            if (rides[midIndex][0] >= targetStart) {
28                rightIndex = midIndex;
29            } else {
30                leftIndex = midIndex + 1;
31            }
32        }
33      
34        return leftIndex;
35    };
36  
37    /**
38     * Dynamic programming function to calculate maximum earnings from index i onwards
39     * @param rideIndex - Current ride index being considered
40     * @returns Maximum earnings from this ride index onwards
41     */
42    const calculateMaxEarnings = (rideIndex: number): number => {
43        // Base case: no more rides available
44        if (rideIndex >= totalRides) {
45            return 0;
46        }
47      
48        // Return memoized result if already computed
49        if (memo[rideIndex] === -1) {
50            const [startPoint, endPoint, tipAmount] = rides[rideIndex];
51          
52            // Find the next available ride after completing current ride
53            const nextRideIndex: number = binarySearchNextRide(endPoint, rideIndex + 1);
54          
55            // Choose maximum between:
56            // 1. Skip current ride and take earnings from next ride
57            // 2. Take current ride (fare + tip) and add earnings from next available ride
58            const skipCurrentRide: number = calculateMaxEarnings(rideIndex + 1);
59            const takeCurrentRide: number = calculateMaxEarnings(nextRideIndex) + endPoint - startPoint + tipAmount;
60          
61            memo[rideIndex] = Math.max(skipCurrentRide, takeCurrentRide);
62        }
63      
64        return memo[rideIndex];
65    };
66  
67    // Start calculating from the first ride
68    return calculateMaxEarnings(0);
69}
70

Time and Space Complexity

Time Complexity: O(m × log m) where m is the length of rides.

The time complexity breaks down as follows:

  • Sorting the rides array takes O(m × log m) time
  • The DFS function with memoization visits each ride at most once due to the @cache decorator, resulting in O(m) unique function calls
  • For each DFS call, we perform a binary search using bisect_left to find the next compatible ride, which takes O(log m) time
  • Therefore, the total time for all DFS operations is O(m × log m)
  • The overall time complexity is dominated by O(m × log m) from both sorting and the DFS traversal with binary search

Space Complexity: O(m) where m is the length of rides.

The space complexity consists of:

  • The recursion call stack can go up to O(m) depth in the worst case when processing all rides sequentially
  • The @cache decorator stores memoization results for up to O(m) different states (one for each possible starting index)
  • The sorted rides array doesn't require additional space as it sorts in-place
  • Therefore, the total space complexity is O(m)

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

Common Pitfalls

1. Incorrect Binary Search Key Function

A common mistake is using the wrong comparison in the binary search when finding the next compatible ride.

Incorrect Implementation:

# Wrong: Comparing entire ride tuples instead of just start points
next_compatible_ride = bisect_left(rides, end_point, lo=ride_index + 1)

This fails because bisect_left would try to compare end_point (an integer) with entire ride tuples [start, end, tip], causing a TypeError or incorrect comparisons.

Correct Implementation:

# Correct: Using key function to extract start point for comparison
next_compatible_ride = bisect_left(
    rides, 
    end_point, 
    lo=ride_index + 1, 
    key=lambda ride: ride[0]
)

2. Forgetting to Sort the Rides

The binary search relies on rides being sorted by start point. Forgetting this step breaks the algorithm.

Wrong:

def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
    # Missing rides.sort()!
    @cache
    def dfs(ride_index: int) -> int:
        # ... rest of the code

Correct:

def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
    rides.sort()  # Essential for binary search to work
    @cache
    def dfs(ride_index: int) -> int:
        # ... rest of the code

3. Using Wrong Search Boundary

Using i instead of i + 1 as the lower bound in binary search would include the current ride in the search range, potentially finding itself as "compatible."

Wrong:

# This could return the current ride index if end_point > start_point
next_compatible_ride = bisect_left(rides, end_point, lo=i, key=lambda x: x[0])

Correct:

# Start searching from the next ride after current
next_compatible_ride = bisect_left(rides, end_point, lo=ride_index + 1, key=lambda x: x[0])

4. Incorrect Earnings Calculation

Mixing up the earnings formula or forgetting components leads to wrong results.

Common Mistakes:

# Wrong: Forgetting the tip
take_current = dfs(next_compatible_ride) + (end_point - start_point)

# Wrong: Adding start point instead of subtracting
take_current = dfs(next_compatible_ride) + (end_point + start_point) + tip_amount

# Wrong: Using just the tip without distance
take_current = dfs(next_compatible_ride) + tip_amount

Correct:

# Earnings = distance + tip = (end - start) + tip
take_current = dfs(next_compatible_ride) + (end_point - start_point) + tip_amount

5. Cache Invalidation with Mutable Data

If rides list is modified after sorting (though unlikely in this problem), the cache would become invalid.

Prevention:

# Convert to tuple if there's any risk of modification
rides_tuple = tuple(map(tuple, rides))
# Or clear cache if rides change
dfs.cache_clear()
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More