Problem overview
An underground railway records how long passengers travel and wants the average time between any two stations. The class UndergroundSystem has three methods. checkIn(int id, String stationName, int t) records that the card id entered a station at time t. checkOut(int id, String stationName, int t) ends that passenger's trip at another station. getAverageTime(String startStation, String endStation) returns, as a double, the average time of all finished trips from the first station to the second.
Two rules shape the design. Only finished trips count, so a passenger who has checked in but not out yet does not change any average. And direction matters: a trip from B to A is a different route from A to B. The tests print each average with exactly two decimals.
Take three passengers. 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.
UndergroundSystem us = new UndergroundSystem();
us.checkIn(45, "Leyton", 3);
us.checkIn(32, "Paradise", 8);
us.checkIn(27, "Leyton", 10);
us.checkOut(45, "Waterloo", 15);
us.checkOut(27, "Waterloo", 20);
us.checkOut(32, "Cambridge", 22);
System.out.println(String.format("%.2f", us.getAverageTime("Paradise", "Cambridge")));
System.out.println(String.format("%.2f", us.getAverageTime("Leyton", "Waterloo")));
us.checkIn(10, "Leyton", 24);
System.out.println(String.format("%.2f", us.getAverageTime("Leyton", "Waterloo")));
14.00
11.00
11.00
The last average stays 11.00, because card 10 has checked in at Leyton but has not checked out.
Approach: keep a total and a count per route
The simple approach stores every finished trip in a list as (start, end, duration). Then getAverageTime walks the whole list, picks the trips on the route, adds their times and divides. That costs O(n) per question, where n is the number of trips so far, and the tests may ask many questions after many trips.
An average needs only two numbers: the total time and the number of trips. A new trip changes both by a known amount, so there is no need to remember the individual trips. For each route the system keeps a pair (total time, number of trips). A check-out adds the trip's time to the total and one to the count, which is O(1). A question reads the pair and divides, which is also O(1).
The second thing to store is the trips in progress. When a passenger checks out, the system needs to know where and when that card checked in, so a second map goes from the card id to the pair (station, time). Checking out removes that entry, which means the same card can check in again later. This matters: in the tests a card may travel more than once, and each trip counts.
The route needs a key for the map. The lesson joins the two station names with a comma, as in "Leyton,Waterloo". The order is part of the key, so the opposite direction gets its own entry. The comma keeps names apart: without it, the stations "A" and "BC" would give the same key as "AB" and "C".
Step by step
- Declare
checkIns, aMap<Integer, Map.Entry<String, Integer>>: the trips in progress, from card id to (station, time).Map.Entryis Java's ready-made pair, made withMap.entry(a, b)and read withgetKey()andgetValue(). - Declare
routes, aMap<String, int[]>: for each route, an array of two ints, the total at index 0 and the count at index 1. checkIn: putMap.entry(stationName, t)incheckInsunderid. The duration is unknown until check-out, so nothing more is needed.checkOut: callcheckIns.remove(id). It deletes the entry and returns what was stored, which goes instart.- Build the route name: the start station
start.getKey(), a comma, andstationName. - Read the route's pair with
getOrDefault(route, new int[] {0, 0}). A route with no finished trip starts from a total of 0 and a count of 0. - Put a new pair under the route: the old total plus this trip's time
t - start.getValue(), and the old count plus 1. getAverageTime: read the pair stored understartStation + "," + endStation, built exactly as incheckOut.- Return the total divided by the count, with
(double)beforestats[0].
The solution in Java
class UndergroundSystem {
Map<Integer, Map.Entry<String, Integer>> checkIns = new HashMap<>();
Map<String, int[]> routes = new HashMap<>();
public void checkIn(int id, String stationName, int t) {
checkIns.put(id, Map.entry(stationName, t));
}
public void checkOut(int id, String stationName, int t) {
Map.Entry<String, Integer> start = checkIns.remove(id);
String route = start.getKey() + "," + stationName;
int[] stats = routes.getOrDefault(route, new int[] {0, 0});
routes.put(route, new int[] {stats[0] + t - start.getValue(), stats[1] + 1});
}
public double getAverageTime(String startStation, String endStation) {
int[] stats = routes.get(startStation + "," + endStation);
return (double) stats[0] / stats[1];
}
}
A few details about the Java code:
- The cast. Dividing two
intvalues in Java drops the fraction:11 / 2is 5. Writing(double) stats[0] / stats[1]converts the total first, so the division is adoubledivision and gives 5.5. The cast applies tostats[0]only, which is enough. - The printed form. The method returns a
double, and printing one directly shows14.0, not14.00. The tests format the result with two decimals, which you can do yourself withString.format("%.2f", value). removereturns the value.Map.remove(key)returns the removed value, ornullif the key was absent. The constraints say a passenger checks out only after checking in, sostartis nevernull.- Replacing the array. The code creates a new
int[]for each check-out and puts it back. Changingstatsin place would also work for an existing route, but the default array fromgetOrDefaultis not in the map, so creating a new pair covers both cases with one line. - Unboxing.
start.getValue()is anInteger, andt - start.getValue()unboxes it to anint.
Seeing it work
The state after the first run above shows what is kept. The map routes holds two routes, Paradise,Cambridge and Leyton,Waterloo, and the pair stored for Leyton,Waterloo is [22, 2]: the total of 12 and 10, over two trips. The map checkIns holds one entry, card 10, still in progress. The average is 22 divided by 2.
The division itself:
int total = 11, count = 2;
System.out.println(total / count);
System.out.println((double) total / count);
5
5.5
The average also moves as trips arrive. Four trips on one route that take 2, 6, 9 and 5 give, after each trip, the averages below, each formatted with two decimals.
UndergroundSystem u2 = new UndergroundSystem();
int[][] trips = {{3, 5}, {10, 16}, {21, 30}, {32, 37}};
for (int i = 0; i < trips.length; i++) {
u2.checkIn(i, "Leyton", trips[i][0]);
u2.checkOut(i, "Paradise", trips[i][1]);
System.out.print(String.format("%.2f ", u2.getAverageTime("Leyton", "Paradise")));
}
2.00 4.00 5.67 5.50
The third value is 17 divided by 3, which is not a two-decimal number: %.2f rounds it to 5.67. The lesson's tests only ask for averages with at most two decimals.
Direction is its own route. With two trips from A to B that take 3 and 2, and one trip from B to A that takes 7:
UndergroundSystem u3 = new UndergroundSystem();
u3.checkIn(1, "A", 1); u3.checkOut(1, "B", 4);
u3.checkIn(2, "B", 5); u3.checkOut(2, "A", 12);
u3.checkIn(3, "A", 13); u3.checkOut(3, "B", 15);
System.out.println(String.format("%.2f %.2f", u3.getAverageTime("A", "B"), u3.getAverageTime("B", "A")));
2.50 7.00
The first value is the average on A to B, (3 + 2) / 2, and the second is the single trip from B to A. In the first loop above, the trips take 2, 6, 9 and 5 time units.
Complexity
Time: O(1) for each operation, apart from building the route string, which takes time proportional to the length of the two station names (at most 10 characters each). The hash-map lookups and updates take O(1) on average. Looking through all finished trips in getAverageTime would take O(n).
Space: O(P + R), where P is the number of passengers currently checked in and R is the number of distinct routes used. Finished trips are not stored, only their total and count per route.
Edge cases and common mistakes
- Integer division. Without the cast, 11 / 2 gives 5. Always convert one operand to
doublebefore dividing. - Mixing up the direction. A trip from B to A must not change the A to B average. The route key keeps the order of the two stations.
- Counting unfinished trips. A check-in alone adds nothing to
routes. OnlycheckOutupdates the totals. - Not removing the check-in. If
checkOutread the entry withgetinstead ofremove, the stale entry would linger. A later check-in by the same card would overwrite it, but memory would grow for cards that never return. - Building the key differently in two places. If
checkOutandgetAverageTimejoin the names differently, the lookup misses. Both use"first,second". - Overflow. The totals are ints, which wrap silently on overflow in Java. The constraints keep all the trips together below a billion, so they fit. A larger system would use
long.
