Facebook Pixel

1396. Design Underground System

MediumDesignHash TableString
Leetcode Link

Problem Description

You need to design an underground railway system that tracks customer travel times between stations and calculates average travel times.

The system should implement a class called UndergroundSystem with three methods:

  1. checkIn(id, stationName, t): Records when a customer with card ID id enters station stationName at time t. Each customer can only be checked into one station at a time.

  2. checkOut(id, stationName, t): Records when a customer with card ID id exits station stationName at time t.

  3. getAverageTime(startStation, endStation): Returns the average time for all trips that went directly from startStation to endStation. A direct trip means a customer checked in at startStation and then checked out at endStation without intermediate stops.

Key points to note:

  • Travel time from station A to station B may differ from travel time from station B to station A (they are treated as different routes)
  • When getAverageTime is called, at least one customer will have completed the specified journey
  • All check-in and check-out operations are consistent: if a customer checks in at time t1 and checks out at time t2, then t1 < t2
  • All events occur in chronological order

The solution uses two hash tables:

  • ts: Maps customer ID to their check-in information (time, station)
  • d: Maps route (startStation, endStation) to aggregated travel data (totalTime, tripCount)

When a customer checks in, their information is stored. When they check out, the travel time is calculated and added to the route statistics. The average time is computed by dividing the total time by the number of trips for that specific route.

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

Intuition

To track travel times between stations, we need to solve two main challenges: tracking individual customer journeys and calculating averages for specific routes.

First, let's think about what happens during a customer's journey. When someone checks in, we don't know their destination yet - we only know their starting point and time. When they check out, we finally have the complete journey information. This suggests we need temporary storage for incomplete journeys.

For tracking incomplete journeys, we need to associate each customer's check-in information with their ID. A hash table with customer ID as the key is perfect for this - we can quickly store and retrieve check-in data when the customer checks out.

Once a customer checks out, we can calculate their travel time as checkOutTime - checkInTime. But here's the key insight: we don't need to store every individual journey. Since we only need averages, we can maintain running totals instead.

For calculating averages, we need two pieces of information for each route:

  • The total time spent by all customers on that route
  • The number of trips completed on that route

The average is simply totalTime / tripCount. By storing these aggregated values instead of individual trips, we save memory and make the average calculation instantaneous.

The route itself can be represented as a tuple (startStation, endStation). This is important because the problem states that A→B and B→A are different routes with potentially different average times.

This leads us to use two hash tables:

  1. One for tracking active journeys (customers who checked in but haven't checked out yet)
  2. One for storing aggregated route statistics (total time and count for each station pair)

This approach gives us O(1) time complexity for all operations and efficient memory usage since we only store aggregated data rather than individual trip records.

Solution Approach

The implementation uses two hash tables to efficiently manage the underground system data:

Data Structures:

  • self.ts: A dictionary that maps customer ID to their check-in information as a tuple (time, stationName)
  • self.d: A dictionary that maps route tuples (startStation, endStation) to aggregated travel data as (totalTime, tripCount)

Method Implementation:

  1. __init__ method:

    • Initializes two empty dictionaries
    • self.ts = {} for tracking active check-ins
    • self.d = {} for storing route statistics
  2. checkIn method:

    • When a customer checks in, we store their information in ts
    • self.ts[id] = (t, stationName)
    • This creates a temporary record that will be used when the customer checks out
  3. checkOut method:

    • First, retrieve the customer's check-in information: t0, station = self.ts[id]
    • Calculate the travel time: t - t0 (current time minus check-in time)
    • Create the route key: (station, stationName) representing the complete journey
    • Update the route statistics:
      • Use get() with default (0, 0) to handle first-time routes
      • Add the travel time to the existing total: x[0] + t - t0
      • Increment the trip count: x[1] + 1
      • Store the updated values back in the dictionary
  4. getAverageTime method:

    • Retrieve the route statistics: x = self.d[(startStation, endStation)]
    • Calculate the average: x[0] / x[1] (total time divided by trip count)
    • Return the result as a float

Key Implementation Details:

  • The solution leverages tuple keys (startStation, endStation) to uniquely identify routes, ensuring that A→B and B→A are treated as separate routes
  • The get() method with default value (0, 0) elegantly handles the case when a route is encountered for the first time
  • By storing aggregated data (totalTime, count) instead of individual trip times, we achieve constant time complexity for all operations
  • The check-in data in self.ts can be overwritten for the same customer ID since the problem guarantees a customer can only be checked into one place at a time

This approach ensures O(1) time complexity for all three operations and uses O(P + S²) space, where P is the number of passengers currently checked in and S is the number of stations.

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 to illustrate how the UndergroundSystem works:

Scenario: Track travel times for customers traveling between stations "A", "B", and "C".

Step 1: Initialize the system

system = UndergroundSystem()
ts = {}  # Empty check-in tracker
d = {}   # Empty route statistics

Step 2: Customer 1 checks in at station "A" at time 10

checkIn(1, "A", 10)
ts[1] = (10, "A")

State after operation:

  • ts = {1: (10, "A")}
  • d = {}

Step 3: Customer 2 checks in at station "B" at time 15

checkIn(2, "B", 15)
ts[2] = (15, "B")

State after operation:

  • ts = {1: (10, "A"), 2: (15, "B")}
  • d = {}

Step 4: Customer 1 checks out at station "B" at time 25

checkOut(1, "B", 25)
Retrieve ts[1] = (10, "A")
Travel time = 25 - 10 = 15
Route = ("A", "B")
d[("A", "B")] = (15, 1)  # First trip on this route

State after operation:

  • ts = {2: (15, "B")} # Customer 1's entry can be removed
  • d = {("A", "B"): (15, 1)}

Step 5: Customer 2 checks out at station "C" at time 35

checkOut(2, "C", 35)
Retrieve ts[2] = (15, "B")
Travel time = 35 - 15 = 20
Route = ("B", "C")
d[("B", "C")] = (20, 1)

State after operation:

  • ts = {}
  • d = {("A", "B"): (15, 1), ("B", "C"): (20, 1)}

Step 6: Customer 3 checks in at "A" at time 30 and checks out at "B" at time 40

checkIn(3, "A", 30)
ts[3] = (30, "A")

checkOut(3, "B", 40)
Retrieve ts[3] = (30, "A")
Travel time = 40 - 30 = 10
Route = ("A", "B")
Update existing route: d[("A", "B")] = (15 + 10, 1 + 1) = (25, 2)

State after operation:

  • ts = {}
  • d = {("A", "B"): (25, 2), ("B", "C"): (20, 1)}

Step 7: Get average times

getAverageTime("A", "B")
Retrieve d[("A", "B")] = (25, 2)
Average = 25 / 2 = 12.5

getAverageTime("B", "C")
Retrieve d[("B", "C")] = (20, 1)
Average = 20 / 1 = 20.0

This example demonstrates:

  • How check-ins are temporarily stored in ts until checkout
  • How route statistics accumulate in d with running totals
  • How different routes (like "A"→"B" vs "B"→"C") are tracked separately
  • How averages are calculated instantly from aggregated data

Solution Implementation

1class UndergroundSystem:
2    def __init__(self):
3        # Dictionary to store check-in information: {customer_id: (check_in_time, start_station)}
4        self.check_in_data = {}
5        # Dictionary to store trip statistics: {(start_station, end_station): (total_time, trip_count)}
6        self.trip_statistics = {}
7
8    def checkIn(self, id: int, stationName: str, t: int) -> None:
9        """
10        Record a customer checking in at a station.
11      
12        Args:
13            id: Customer ID
14            stationName: Name of the check-in station
15            t: Check-in time
16        """
17        # Store the check-in time and station for this customer
18        self.check_in_data[id] = (t, stationName)
19
20    def checkOut(self, id: int, stationName: str, t: int) -> None:
21        """
22        Record a customer checking out at a station and update trip statistics.
23      
24        Args:
25            id: Customer ID
26            stationName: Name of the check-out station
27            t: Check-out time
28        """
29        # Retrieve the check-in information for this customer
30        check_in_time, start_station = self.check_in_data[id]
31      
32        # Calculate the travel time for this trip
33        travel_time = t - check_in_time
34      
35        # Create the route key (start_station, end_station)
36        route = (start_station, stationName)
37      
38        # Update the trip statistics for this route
39        # Get existing statistics or initialize with (0, 0) if route doesn't exist
40        total_time, trip_count = self.trip_statistics.get(route, (0, 0))
41        self.trip_statistics[route] = (total_time + travel_time, trip_count + 1)
42
43    def getAverageTime(self, startStation: str, endStation: str) -> float:
44        """
45        Calculate the average travel time between two stations.
46      
47        Args:
48            startStation: Name of the starting station
49            endStation: Name of the ending station
50          
51        Returns:
52            Average travel time as a float
53        """
54        # Retrieve the total time and count for this route
55        route = (startStation, endStation)
56        total_time, trip_count = self.trip_statistics[route]
57      
58        # Calculate and return the average time
59        return total_time / trip_count
60
61
62# Your UndergroundSystem object will be instantiated and called as such:
63# obj = UndergroundSystem()
64# obj.checkIn(id,stationName,t)
65# obj.checkOut(id,stationName,t)
66# param_3 = obj.getAverageTime(startStation,endStation)
67
1class UndergroundSystem {
2    // Map to store check-in time for each passenger ID
3    private Map<Integer, Integer> checkInTimes = new HashMap<>();
4  
5    // Map to store check-in station name for each passenger ID
6    private Map<Integer, String> checkInStations = new HashMap<>();
7  
8    // Map to store travel data: key is "startStation-endStation", value is [totalTime, tripCount]
9    private Map<String, int[]> travelData = new HashMap<>();
10
11    public UndergroundSystem() {
12        // Constructor - maps are already initialized
13    }
14
15    public void checkIn(int id, String stationName, int t) {
16        // Record the check-in time for this passenger
17        checkInTimes.put(id, t);
18      
19        // Record the check-in station for this passenger
20        checkInStations.put(id, stationName);
21    }
22
23    public void checkOut(int id, String stationName, int t) {
24        // Build the route key using start and end stations
25        String routeKey = checkInStations.get(id) + "-" + stationName;
26      
27        // Get existing travel data for this route, or create new array if none exists
28        int[] routeStats = travelData.getOrDefault(routeKey, new int[2]);
29      
30        // Calculate travel time and add to total time (index 0)
31        routeStats[0] += t - checkInTimes.get(id);
32      
33        // Increment trip count (index 1)
34        routeStats[1]++;
35      
36        // Update the travel data map
37        travelData.put(routeKey, routeStats);
38    }
39
40    public double getAverageTime(String startStation, String endStation) {
41        // Build the route key
42        String routeKey = startStation + "-" + endStation;
43      
44        // Get the travel statistics for this route
45        int[] routeStats = travelData.get(routeKey);
46      
47        // Calculate and return average time: total time / number of trips
48        return (double) routeStats[0] / routeStats[1];
49    }
50}
51
52/**
53 * Your UndergroundSystem object will be instantiated and called as such:
54 * UndergroundSystem obj = new UndergroundSystem();
55 * obj.checkIn(id,stationName,t);
56 * obj.checkOut(id,stationName,t);
57 * double param_3 = obj.getAverageTime(startStation,endStation);
58 */
59
1class UndergroundSystem {
2public:
3    UndergroundSystem() {
4    }
5  
6    void checkIn(int id, string stationName, int t) {
7        // Store the check-in information for the passenger
8        // Map passenger ID to their check-in station and time
9        checkInData[id] = {stationName, t};
10    }
11  
12    void checkOut(int id, string stationName, int t) {
13        // Retrieve the passenger's check-in information
14        auto [startStation, checkInTime] = checkInData[id];
15      
16        // Create a unique key for the route (startStation -> endStation)
17        string routeKey = startStation + "-" + stationName;
18      
19        // Calculate travel time for this trip
20        int travelTime = t - checkInTime;
21      
22        // Update the route statistics (total time and trip count)
23        auto [totalTime, tripCount] = routeStatistics[routeKey];
24        routeStatistics[routeKey] = {totalTime + travelTime, tripCount + 1};
25    }
26  
27    double getAverageTime(string startStation, string endStation) {
28        // Create the route key
29        string routeKey = startStation + "-" + endStation;
30      
31        // Retrieve the route statistics
32        auto [totalTime, tripCount] = routeStatistics[routeKey];
33      
34        // Calculate and return the average travel time
35        return static_cast<double>(totalTime) / tripCount;
36    }
37
38private:
39    // Maps passenger ID to their check-in information (station name, check-in time)
40    unordered_map<int, pair<string, int>> checkInData;
41  
42    // Maps route (formatted as "startStation-endStation") to route statistics
43    // Statistics include: total travel time across all trips, number of trips
44    unordered_map<string, pair<int, int>> routeStatistics;
45};
46
47/**
48 * Your UndergroundSystem object will be instantiated and called as such:
49 * UndergroundSystem* obj = new UndergroundSystem();
50 * obj->checkIn(id,stationName,t);
51 * obj->checkOut(id,stationName,t);
52 * double param_3 = obj->getAverageTime(startStation,endStation);
53 */
54
1// Maps passenger ID to their check-in information (station name, check-in time)
2const checkInData: Map<number, [string, number]> = new Map();
3
4// Maps route (formatted as "startStation-endStation") to route statistics
5// Statistics include: total travel time across all trips, number of trips
6const routeStatistics: Map<string, [number, number]> = new Map();
7
8/**
9 * Records a passenger checking in at a station
10 * @param id - The passenger's unique ID
11 * @param stationName - The name of the check-in station
12 * @param t - The check-in time
13 */
14function checkIn(id: number, stationName: string, t: number): void {
15    // Store the check-in information for the passenger
16    // Map passenger ID to their check-in station and time
17    checkInData.set(id, [stationName, t]);
18}
19
20/**
21 * Records a passenger checking out at a station and updates route statistics
22 * @param id - The passenger's unique ID
23 * @param stationName - The name of the check-out station
24 * @param t - The check-out time
25 */
26function checkOut(id: number, stationName: string, t: number): void {
27    // Retrieve the passenger's check-in information
28    const checkInInfo = checkInData.get(id)!;
29    const startStation = checkInInfo[0];
30    const checkInTime = checkInInfo[1];
31  
32    // Create a unique key for the route (startStation -> endStation)
33    const routeKey = `${startStation}-${stationName}`;
34  
35    // Calculate travel time for this trip
36    const travelTime = t - checkInTime;
37  
38    // Update the route statistics (total time and trip count)
39    const currentStats = routeStatistics.get(routeKey) || [0, 0];
40    const totalTime = currentStats[0];
41    const tripCount = currentStats[1];
42    routeStatistics.set(routeKey, [totalTime + travelTime, tripCount + 1]);
43}
44
45/**
46 * Calculates the average travel time between two stations
47 * @param startStation - The starting station name
48 * @param endStation - The ending station name
49 * @returns The average travel time for the specified route
50 */
51function getAverageTime(startStation: string, endStation: string): number {
52    // Create the route key
53    const routeKey = `${startStation}-${endStation}`;
54  
55    // Retrieve the route statistics
56    const stats = routeStatistics.get(routeKey)!;
57    const totalTime = stats[0];
58    const tripCount = stats[1];
59  
60    // Calculate and return the average travel time
61    return totalTime / tripCount;
62}
63

Time and Space Complexity

Time Complexity: O(1) for all operations (checkIn, checkOut, and getAverageTime)

  • checkIn: Simply stores a tuple in a dictionary using the passenger ID as key, which is O(1)
  • checkOut: Retrieves check-in data using passenger ID (O(1)), gets existing travel data for the station pair (O(1)), and updates the dictionary (O(1))
  • getAverageTime: Retrieves the accumulated data for a station pair and performs a division operation, both O(1)

Space Complexity: O(n + m) where n is the number of passengers currently checked in and m is the number of unique station pairs

  • self.ts dictionary stores check-in information for each passenger currently in the system, requiring O(n) space
  • self.d dictionary stores accumulated travel time and count for each unique station pair (startStation, endStation), requiring O(m) space
  • In the worst case where all passengers are checked in but not checked out, the space complexity is dominated by the number of passengers, making it effectively O(n) as stated in the reference answer

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

Common Pitfalls

1. Not Removing Check-in Data After Check-out

One common pitfall is leaving the check-in data in memory after a customer checks out. While this doesn't affect correctness (since the problem guarantees proper check-in/check-out sequences), it can lead to unnecessary memory usage in a real-world system with millions of customers.

Problem Code:

def checkOut(self, id: int, stationName: str, t: int) -> None:
    check_in_time, start_station = self.check_in_data[id]
    travel_time = t - check_in_time
    route = (start_station, stationName)
    total_time, trip_count = self.trip_statistics.get(route, (0, 0))
    self.trip_statistics[route] = (total_time + travel_time, trip_count + 1)
    # Missing: del self.check_in_data[id]

Solution:

def checkOut(self, id: int, stationName: str, t: int) -> None:
    check_in_time, start_station = self.check_in_data[id]
    travel_time = t - check_in_time
    route = (start_station, stationName)
    total_time, trip_count = self.trip_statistics.get(route, (0, 0))
    self.trip_statistics[route] = (total_time + travel_time, trip_count + 1)
    # Clean up the check-in data to free memory
    del self.check_in_data[id]

2. Treating Bidirectional Routes as the Same

A critical mistake is assuming that the route from station A to station B is the same as the route from station B to station A. This would give incorrect averages since travel times can differ based on direction.

Problem Code:

def checkOut(self, id: int, stationName: str, t: int) -> None:
    check_in_time, start_station = self.check_in_data[id]
    travel_time = t - check_in_time
    # Wrong: Using sorted tuple or set to make routes bidirectional
    route = tuple(sorted([start_station, stationName]))  # INCORRECT!
    # This would treat A->B and B->A as the same route

Solution: Always maintain the order of stations in the route tuple:

def checkOut(self, id: int, stationName: str, t: int) -> None:
    check_in_time, start_station = self.check_in_data[id]
    travel_time = t - check_in_time
    # Correct: Preserve the order to distinguish A->B from B->A
    route = (start_station, stationName)

3. Integer Division Instead of Float Division

In Python 2 or when using integer division, you might accidentally truncate the average time to an integer, losing precision.

Problem Code:

def getAverageTime(self, startStation: str, endStation: str) -> float:
    route = (startStation, endStation)
    total_time, trip_count = self.trip_statistics[route]
    # In Python 2 or with // operator: returns integer
    return total_time // trip_count  # INCORRECT!

Solution: Use true division to get a float result:

def getAverageTime(self, startStation: str, endStation: str) -> float:
    route = (startStation, endStation)
    total_time, trip_count = self.trip_statistics[route]
    # Correct: Use / for float division
    return total_time / trip_count

4. Not Handling Missing Routes in getAverageTime

While the problem guarantees at least one trip exists when getAverageTime is called, defensive programming would check for this condition to avoid KeyError in production code.

Enhanced Solution:

def getAverageTime(self, startStation: str, endStation: str) -> float:
    route = (startStation, endStation)
    if route not in self.trip_statistics:
        return 0.0  # or raise an exception
    total_time, trip_count = self.trip_statistics[route]
    return total_time / trip_count
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

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

Load More