1396. Design Underground System
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:
-
checkIn(id, stationName, t)
: Records when a customer with card IDid
enters stationstationName
at timet
. Each customer can only be checked into one station at a time. -
checkOut(id, stationName, t)
: Records when a customer with card IDid
exits stationstationName
at timet
. -
getAverageTime(startStation, endStation)
: Returns the average time for all trips that went directly fromstartStation
toendStation
. A direct trip means a customer checked in atstartStation
and then checked out atendStation
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 timet2
, thent1 < 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.
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:
- One for tracking active journeys (customers who checked in but haven't checked out yet)
- 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:
-
__init__
method:- Initializes two empty dictionaries
self.ts = {}
for tracking active check-insself.d = {}
for storing route statistics
-
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
- When a customer checks in, we store their information in
-
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
- Use
- First, retrieve the customer's check-in information:
-
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
- Retrieve the route statistics:
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 EvaluatorExample 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 removedd = {("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 isO(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, bothO(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, requiringO(n)
spaceself.d
dictionary stores accumulated travel time and count for each unique station pair(startStation, endStation)
, requiringO(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
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!