Problem overview
You write the class UndergroundSystem. Passengers have a card id. CheckIn(id, stationName, t) starts a trip and CheckOut(id, stationName, t) ends it. GetAverageTime(startStation, endStation) returns, as a double, the average time of all finished trips from the first station to the second. Direction matters: a trip from B to A is a different route from A to B. A trip that has checked in but not out yet does not count.
In the first example, card 45 goes from Leyton to Waterloo in 15 - 3 = 12, card 27 on the same route in 20 - 10 = 10, and card 32 from Paradise to Cambridge in 22 - 8 = 14. The averages come out as:
Paradise to Cambridge: 14.00
Leyton to Waterloo: 11.00
Leyton to Waterloo, after card 10 checks in but not out: 11.00
The second example has one card, 10, travelling twice; each trip counts. The trips from Leyton to Paradise take 5, 6, 9 and 5, and the average after one, two and four trips is:
5.00
5.50
6.25
Approach: keep a total and a count per route
The simple design stores every finished trip and, in GetAverageTime, walks through all of them to add up the matching ones. That costs 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 never stores finished trips at all. It keeps two maps:
checkIns: the trips in progress. For each card id, the station and the time it checked in.routes: for each route, written as the string"start,end", the pair (total time of its finished trips, number of trips).
CheckIn only records the start. CheckOut looks up the start, computes the trip time t - startTime, and adds it to the route's total and 1 to its count. GetAverageTime is one lookup and one division. Each operation is O(1), apart from building the route string from the two station names.
The comma in the route name matters. Without it, the stations "A" and "BC" would give the same route string as "AB" and "C". With it, the two routes are A,BC and AB,C.
Step by step
- Declare
checkIns, aDictionary<int, (string, int)>, set tonew(). The value is a tuple holding a station and a time. - Declare
routes, aDictionary<string, (int, int)>, set tonew(): the pair is (total time, number of trips). CheckInsetscheckIns[id]to(stationName, t). Nothing else is known until the passenger leaves.CheckOutunpacks the stored pair withvar (startStation, startTime) = checkIns[id];, which copies the tuple's two values into two new variables.- Remove
idfromcheckIns. The trip is over, and the same card can check in again later, as card 10 does in the second example. - Build
routeasstartStation + "," + stationName. - Unpack the route's current pair with
GetValueOrDefault(route, (0, 0)), so a route with no finished trip yet starts from (0, 0). - Store the new pair: the total plus
t - startTime, and the count plus 1. Only these two numbers change, so the cost does not depend on how many trips came before. GetAverageTimeunpacks the pair stored forstartStation + "," + endStation, built exactly as inCheckOut, and returns(double) total / count.
The solution in C#
class UndergroundSystem {
Dictionary<int, (string, int)> checkIns = new();
Dictionary<string, (int, int)> routes = new();
public void CheckIn(int id, string stationName, int t) {
checkIns[id] = (stationName, t);
}
public void CheckOut(int id, string stationName, int t) {
var (startStation, startTime) = checkIns[id];
checkIns.Remove(id);
string route = startStation + "," + stationName;
var (total, count) = routes.GetValueOrDefault(route, (0, 0));
routes[route] = (total + t - startTime, count + 1);
}
public double GetAverageTime(string startStation, string endStation) {
var (total, count) = routes[startStation + "," + endStation];
return (double) total / count;
}
}
Details in C#. Dividing two int values drops the fraction: 11 / 2 is 5, while (double) total / count with total 11 and count 2 gives 5.5. The cast applies to total before the division, so the division is done in double. Reading checkIns[id] or routes[...] for a missing key throws a KeyNotFoundException; that is safe here because the problem promises that a card checks out only after checking in, and that averages are asked only for routes with at least one finished trip. GetValueOrDefault is used in CheckOut exactly because a route's first trip has no pair yet.
Complexity
- CheckIn, CheckOut, GetAverageTime: O(1) each, plus the cost of building the route string from two short station names.
- Space: O(P + R) for P passengers currently travelling and R distinct routes, instead of one entry per finished trip.
Testing
The third example checks that direction matters. Trips: card 1 goes A to B in 3, card 2 goes B to A in 7, card 3 goes A to B in 2. The averages:
A to B: 2.50
B to A: 7.00
Edge cases and common mistakes
- Integer division. Without the cast, 5 + 6 = 11 over 2 trips gives 5 instead of 5.5.
- Mixing up directions. Build the route as start then end in both methods, so A to B and B to A stay apart.
- No separator in the route name. Two different pairs of stations can then collide.
- Forgetting to remove the check-in. It would leave stale data behind, and a card that checks in again would rely on overwriting it.
- Counting unfinished trips. Only
CheckOutchangesroutes, so a passenger who has only checked in never affects an average.
