Problem overview
An underground railway records trips and wants average travel times between stations. The class UndergroundSystem has three methods. checkIn(id, stationName, t) records that the card id entered a station at time t. checkOut(id, stationName, t) ends that card's trip at another station. getAverageTime(startStation, endStation) returns the average time of all finished trips that went from the first station to the second.
Only finished trips count, and the direction matters: a trip from B to A belongs to a different route from A to B. The tests print each average with exactly two decimals, so a value of 2.5 appears as 2.50.
Take the first example. Card 45 goes from Leyton to Waterloo from time 3 to 15 (12), card 27 on the same route from 10 to 20 (10), and card 32 from Paradise to Cambridge from 8 to 22 (14). Then getAverageTime("Paradise", "Cambridge") is 14 and getAverageTime("Leyton", "Waterloo") is (12 + 10) / 2 = 11. Card 10 then checks in at Leyton but has not checked out, so its trip is not counted and the average stays 11. The three answers, printed to two decimals:
14.00 11.00
11.00
Approach: two maps and a running total per route
The obvious design stores every finished trip in a list and, on each getAverageTime, walks through the list, picks the trips on that route and averages them. That is O(n) per question, where n is the number of trips so far.
An average is a total divided by a count, so it is enough to keep those two numbers for each route and update them whenever a trip ends. The class keeps two maps:
this.checkIns: the trips in progress. For each card id, the pair[stationName, t]where its passenger checked in. An entry is needed only until the same card checks out.this.routes: the finished trips, per route. The key is the route as one string,"start,end", and the value is the pair[total time, number of trips].
Now checkIn is one map write. checkOut reads and deletes the card's check-in, builds the route key, and adds the trip's time to the route's total and one to its count. getAverageTime is one lookup and one division. The only work that depends on anything but the number of operations is building the route string from the two station names, which is short.
After the first example, the maps hold:
routes: Leyton,Waterloo => [22, 2] Paradise,Cambridge => [14, 1]
checkIns: 10 => ["Leyton", 24]
The route "Leyton,Waterloo" has total 22 over 2 trips, so its average is 11. Card 10's open trip sits in checkIns and has not touched routes.
Step by step
- Constructor, first map: set
this.checkInsto a new emptyMapfor the trips in progress. - Constructor, second map: set
this.routesto a new emptyMapfor the finished trips. The pair stored per route letsgetAverageTimeavoid walking through every trip. - checkIn: store
[stationName, t]under the keyidinthis.checkIns. The trip's time is not known until check-out, so that is all it does. - checkOut: read the passenger's check-in and unpack it with
let [startStation, startTime] = this.checkIns.get(id). - Delete
idfromthis.checkIns. The trip is over, and the same card can check in again later. - Build the route's name:
startStation + "," + stationName. The order matters, since A to B and B to A are different routes. The comma keeps names apart: without it, "A" and "BC" would give the same route as "AB" and "C". - Unpack the route's current pair into
totalandcount, withthis.routes.get(route) || [0, 0]. For a route with no finished trip yet,getreturnsundefinedand||picks[0, 0]. - Set the route to a new pair:
[total + t - startTime, count + 1]. Only these two numbers change, so a check-out isO(1)however many trips came before. - getAverageTime: unpack the pair stored for
startStation + "," + endStation, building the name exactly ascheckOutdoes. - Return
total / count. The statement promises the route has at least one finished trip.
The solution in JavaScript
class UndergroundSystem {
constructor() {
this.checkIns = new Map();
this.routes = new Map();
}
checkIn(id, stationName, t) {
this.checkIns.set(id, [stationName, t]);
}
checkOut(id, stationName, t) {
let [startStation, startTime] = this.checkIns.get(id);
this.checkIns.delete(id);
let route = startStation + "," + stationName;
let [total, count] = this.routes.get(route) || [0, 0];
this.routes.set(route, [total + t - startTime, count + 1]);
}
getAverageTime(startStation, endStation) {
let [total, count] = this.routes.get(startStation + "," + endStation);
return total / count;
}
}
JavaScript details:
- Division keeps the fraction.
(5 + 6) / 2is 5.5, with no integer truncation. The method returns a number such as2.5; the two decimals (2.50) come from the way the result is printed, for example withtoFixed(2). - Destructuring.
let [a, b] = pairunpacks an array into variables, in order. It throws aTypeErrorif the right side isundefined. That is whycheckOutuses|| [0, 0], and whygetAverageTimerelies on the promise that the route exists: for a route with no finished trip, it would throw. - String keys. A route is a string such as
"A,B", because aMapcompares array keys by identity: a new array["A", "B"]would never find an earlier one.
Complexity
Time: checkIn, checkOut and getAverageTime are each O(1), apart from building the route string, which depends only on the length of the two station names (at most 10 characters each). They are map reads and writes and a little arithmetic.
Space: O(C + R), where C is the number of cards currently checked in and R the number of different routes. Finished trips are not stored one by one, only their total and count, so space does not grow with the number of trips.
Edge cases and common mistakes
- Direction. After the third example's trips, route "A,B" holds
[5, 2](trips of 3 and 2) and route "B,A" holds[7, 1].getAverageTime("A", "B")returns 2.5 andgetAverageTime("B", "A")returns 7, printed as2.50 7.00. - Unfinished trips. A card that checked in but not out is only in
checkIns, so it does not affect any average. - One card, many trips. In the second example card 10 travels twice, and the deletion in
checkOutfrees it to check in again. The four trips from Leyton to Paradise take 5, 6, 9 and 5, and the averages after one, two and four trips are5.00,5.50and6.25. - Forgetting the delete. Without it the map keeps stale check-ins forever. A later check-in overwrites it, so the answers stay right, but the map grows with the number of cards.
- Averaging averages. Do not store the current average and update it with the new time: keep the total and the count and divide only when asked.
- A route key without a separator. With plain
"A" + "BC"and"AB" + "C"both producing "ABC" (true in JavaScript), two different routes would share one entry. With the comma,"A,BC"and"AB,C"differ.
Testing
Outputs of the solution above, printed with toFixed(2):
- Example 1 prints
14.00 11.00and then11.00. - Example 2 prints
5.00,5.50and6.25. - Example 3 prints
2.50 7.00.
