Problem overview
Passengers check in with a card at the station where they start and check out at the station where they leave. The class UndergroundSystem records these trips and answers one question: what is the average time of the finished trips from one station to another?
It has three methods. checkIn(id, stationName, t) starts a trip for the card id. checkOut(id, stationName, t) ends that card's trip, which took t minus the check-in time. getAverageTime(startStation, endStation) returns the average time of all finished trips from the first station to the second. Direction matters: a trip from B to A belongs to a different route than one from A to B. A trip that has not been checked out does not count. The tests print each average with two decimals.
In the first example, cards 45 and 27 both go from Leyton to Waterloo, taking 12 and 10 minutes, and card 32 goes from Paradise to Cambridge in 14. The averages are 14.00 for Paradise to Cambridge and 11.00 for Leyton to Waterloo. Then card 10 checks in at Leyton but does not check out, so the Leyton to Waterloo average stays 11.00.
The statement asks for O(1) time per operation, apart from building the route's name from the two station names.
Approach: keep a total and a count per route
The simple solution stores every finished trip in a list and, in getAverageTime, walks the whole list, adds up the matching trips and divides. That is O(n) per call for n trips so far.
An average is a total divided by a count, and both can be updated one trip at a time. So the class keeps, for each route, only two numbers: the total time of its finished trips and how many there are. A check-out adds the trip's time to the total and adds one to the count. getAverageTime reads the two numbers and divides.
The class needs two maps:
checkIns: for each card id, where and when its owner checked in. It holds the trips in progress, and an entry is removed when its trip ends.routes: for each route, written as one string"start,end", the pair (total time, number of trips).
After the first example's check-outs and the extra check-in by card 10, the state is:
routes:
Leyton,Waterloo -> total 22, count 2
Paradise,Cambridge -> total 14, count 1
trips in progress: 1
The average for Leyton to Waterloo is 22 / 2 = 11.00. The one trip in progress belongs to card 10 and is not in routes.
The comma in the route's name keeps stations apart. Without it, the stations A and BC and the stations AB and C would both give the route ABC. With the comma they give A,BC and AB,C, which differ. The station names contain only letters and digits, so a comma never occurs inside a name.
Step by step
- Declare
checkInsas anunordered_mapfrominttopair<string, int>: the station and the time of each check-in in progress. - Declare
routesas anunordered_mapfromstringtopair<int, int>: the total time and the number of trips of each route. - In
checkIn, setcheckIns[id]to{stationName, t}. The trip's time is only known at check-out. - In
checkOut, unpackcheckIns[id]intostartStationandstartTimewithauto [startStation, startTime]. - Erase
idfromcheckIns. The trip is over, and the card may check in again later. - Build the route's name:
startStation, a comma, thenstationName. The order gives the direction. - Unpack
routes[route]intototalandcount. A route not in the map yet gets a pair of two zeros. - Store the new pair:
total + t - startTimeandcount + 1. - In
getAverageTime, unpack the pair of the routestartStation + "," + endStation, built exactly as incheckOut. - Return
(double) total / count. The cast turns the division into a division of doubles and keeps the fraction.
The solution in C++
class UndergroundSystem {
public:
unordered_map<int, pair<string, int>> checkIns;
unordered_map<string, pair<int, int>> routes;
void checkIn(int id, string stationName, int t) {
checkIns[id] = {stationName, t};
}
void checkOut(int id, string stationName, int t) {
auto [startStation, startTime] = checkIns[id];
checkIns.erase(id);
string route = startStation + "," + stationName;
auto [total, count] = routes[route];
routes[route] = {total + t - startTime, count + 1};
}
double getAverageTime(string startStation, string endStation) {
auto [total, count] = routes[startStation + "," + endStation];
return (double) total / count;
}
};
The C++ details worth a closer look:
- Structured bindings.
auto [a, b] = pair;copies the two parts of a pair into two new variables. It needs C++17. Here the copy is taken beforecheckIns.erase(id), so erasing the entry is safe. The namesstartStationandstartTimeare copies, not references into the map. - Braces assign a pair.
checkIns[id] = {stationName, t};builds apair<string, int>from the two values. Assigning tocheckIns[id]overwrites an existing entry or creates a new one. operator[]creates missing entries. IncheckOut,routes[route]for a new route inserts a pair of two zeros, which is exactly the starting state. IngetAverageTimethe same call would insert an empty route if it were asked about one with no trips; the statement promises there is always at least one.- Integer division.
total / countwith twoints drops the fraction:11 / 2is 5. With(double) totalit is 5.5. The cast applies tototalbefore the division. - Overflow. The constraints keep the sum of all trips within one billion, so the totals fit in an
int. Signed overflow in C++ is undefined behaviour, so it matters that they do.
Running the second example (four trips from Leyton to Paradise taking 5, 6, 9 and 5 minutes, with card 10 travelling twice) prints 5.00 5.50 6.25 for the three calls to getAverageTime. Running the third example prints 2.50 7.00: the trips from A to B take 3 and 2, and the one trip from B to A takes 7.
Complexity
Time: O(1) per operation on average. Each method does a few hash-map lookups, one addition and at most one division. The route's name is built by joining two strings of at most 10 characters each, so the cost is proportional to the length of the names, a small constant here.
Space: O(p + r), where p is the number of passengers currently in a trip and r is the number of distinct routes. The map of finished trips does not grow with the number of trips, only with the number of routes.
Edge cases and common mistakes
- Direction. The route
"A,B"and the route"B,A"are different entries. With trips A to B of 3 and 2 minutes and one B to A trip of 7, the averages are 2.50 and 7.00. - Unfinished trips. A card that checked in but did not check out is only in
checkIns, so it does not change any average. - The same card travelling twice. The check-out erases the entry, and the next check-in creates a new one, so
checkInsholds only the trips in progress. - Integer division. Without the
(double)cast, the average of 5 and 6 would be 5.00 instead of 5.50. - Forgetting the comma. It makes different routes collide, as
ABCshows. Building the name differently incheckOutand ingetAverageTimemakes every lookup miss. - Averaging averages. Storing a running average and averaging it with each new trip gives wrong results. Keep the total and the count, and divide at the end.
- Using the check-out time as the trip time. The trip took
t - startTime.
Testing
- Cards 45, 27 and 32 as in the first example, then
getAverageTime("Paradise", "Cambridge")andgetAverageTime("Leyton", "Waterloo")give14.00 11.00; aftercheckIn(10, "Leyton", 24)the second average is still11.00. - Four Leyton to Paradise trips taking 5, 6, 9 and 5, with an average read after the first, second and fourth, give
5.00 5.50 6.25. - Trips A to B (3 and 2 minutes) and B to A (7 minutes), then
getAverageTime("A", "B")andgetAverageTime("B", "A")give2.50 7.00.
