Problem overview
Passengers check in at a station with a card id and check out at another. The class UndergroundSystem records these trips and answers one question: what is the average time of all finished trips from station A to station B? Its methods are checkIn(id, stationName, t), checkOut(id, stationName, t) and getAverageTime(startStation, endStation).
Only finished trips count, and direction matters: a trip from B to A belongs to a different route. A card can travel many times, but is checked in at one place at a time. The statement asks for O(1) per operation, apart from building the route's name.
Take card 45 checking in at "Alder" at time 3 and out at "Cedar" at time 15, and card 27 doing the same route from time 10 to time 20. The trips take 12 and 10 minutes, so getAverageTime("Alder", "Cedar") is 11. A card 10 that checks in at "Alder" at time 24 has not finished, so the average stays 11. The method returns a Double, and the lesson's tests show it with two decimals, such as 11.00.
Approach: store a total and a count per route
The simple design keeps a list of every finished trip: start, end and duration. getAverageTime then goes through the list, picks the trips of the route and averages them. That works, but each call costs O(n) for n trips so far, and it needs to store every trip.
An average only needs two numbers: the total time and the number of trips. Updating them when a trip finishes is cheap, and reading them is cheap too. So the class keeps two dictionaries:
checkIns: for each card id, the station and time of the trip in progress;routes: for each route, the pair(total time, number of trips).
checkIn saves the pair. checkOut takes it out, which ends the trip and frees the card for its next one, builds the route's name from the two stations, and adds the duration t - startTime to the route's total and 1 to its count. getAverageTime reads the route and divides.
The route is a string of the form "start,end". The comma matters. Without it, a trip from "A" to "BC" and one from "AB" to "C" would both be the route "ABC", and their times would be mixed.
Step by step
- Declare
checkInsas avarof type[Int: (String, Int)], set to[:]. The tuple holds a station and a time. - Declare
routesas avarof type[String: (Int, Int)], set to[:]. The tuple holds the total time and the number of trips. - In
checkIn, setcheckIns[id]to(stationName, t). The duration is not known yet. - In
checkOut, remove the check-in withremoveValue(forKey: id)!and unpack the tuple withlet (startStation, startTime) = .... - Build
routeasstartStation + "," + stationName. - Read the route's pair with
routes[route, default: (0, 0)], unpacked intototalandcount. A new route starts at 0 and 0. - Store the updated pair:
total + t - startTimeandcount + 1. - In
getAverageTime, read the pair forstartStation + "," + endStation, with!after the lookup. The lesson promises the route has at least one finished trip. - Return
Double(total) / Double(count).
The solution in Swift
class UndergroundSystem {
var checkIns: [Int: (String, Int)] = [:]
var routes: [String: (Int, Int)] = [:]
func checkIn(_ id: Int, _ stationName: String, _ t: Int) {
checkIns[id] = (stationName, t)
}
func checkOut(_ id: Int, _ stationName: String, _ t: Int) {
let (startStation, startTime) = checkIns.removeValue(forKey: id)!
let route = startStation + "," + stationName
let (total, count) = routes[route, default: (0, 0)]
routes[route] = (total + t - startTime, count + 1)
}
func getAverageTime(_ startStation: String, _ endStation: String) -> Double {
let (total, count) = routes[startStation + "," + endStation]!
return Double(total) / Double(count)
}
}
The Swift details worth a closer look:
- Tuple destructuring.
let (startStation, startTime) = ...splits a tuple into two constants in one line. It works the same way for the pair read fromroutes. removeValue(forKey:). It deletes the entry and returns its old value as an optional,nilif the key was missing. The!unwraps it, which is safe because the lesson says a card checks out only after checking in. After it,checkInsno longer holds the card, so it can check in again.default:with a tuple.routes[route, default: (0, 0)]gives(0, 0)for a route that isn't there yet. Reading withdefaultdoesn't add anything to the dictionary. The line that follows stores the updated pair.- Integer division.
totalandcountareInt. Dividing them directly drops the fraction:11 / 2is 5. Converting each toDoublefirst givesDouble(11) / Double(2), which is 5.5. Converting after the division,Double(11 / 2), gives 5.0, the wrong answer. - The type of the result. Swift prints a
Doublewithout trailing zeros: 14.0 and 5.5 appear as14.0and5.5. The lesson's tests format the value to two decimals. - Arithmetic order.
total + t - startTimeadds first, then subtracts. The lesson guarantees totals fit in 32 bits, so there is no overflow on a 64-bitInt. - Class properties. The dictionaries are
varproperties of a class, so methods change them without being markedmutating.
Complexity
Time. Every method is a few dictionary operations, O(1) on average, plus building the route's name, which takes time proportional to the two names (at most 10 characters each). getAverageTime does one lookup and one division, whatever the number of trips.
Space. O(P + R), where P is the number of passengers currently checked in and R the number of different routes. Finished trips are not stored one by one: each adds to a total and a count and is forgotten.
Edge cases and common mistakes
- Unfinished trips. A card that has checked in but not out is in
checkInsonly, so it doesn't affect any average. After the extra check-in in the first example,checkInsholds just card 10 and the average of "Alder" to "Cedar" is still 11. - The direction. With two trips from A to B taking 3 and 2, and one from B to A taking 7,
getAverageTime("A", "B")is 2.5 andgetAverageTime("B", "A")is 7. Using the same key for both directions would mix them. - The same card travelling twice. In the second example, card 10 makes two trips from "Leyton" to "Paradise". The averages after the trips are 5.0, 5.5 and 6.25, since each finished trip counts. Because
checkOutremoves the check-in, the second check-in starts clean. - No separator in the route. Joining the names without the comma makes
"A" + "BC"equal"AB" + "C". Station names are letters and digits only, so a comma can never be part of a name. - Integer division.
total / counton twoIntvalues returns 5 for 11 and 2. Convert both toDoublebefore dividing. - Keeping a list of all trips. It is correct but makes
getAverageTimeO(n)and the memory grow with every trip. - Checking out without a check-in. The
!would crash the program. The lesson never does this.
Testing
- Cards 45, 32 and 27 check in at "Alder" (time 3), "Birch" (8) and "Alder" (10), and check out at "Cedar" (15), "Dune" (22) and "Cedar" (20). The average from "Birch" to "Dune" is 14.0 and from "Alder" to "Cedar" is 11.0. After card 10 checks in at "Alder" at 24, the average from "Alder" to "Cedar" is still 11.0.
- Four trips from "Leyton" to "Paradise" taking 5, 6, 9 and 5 give 5.0, 5.5 and 6.25 after the first, the second and the fourth.
- Trips A to B taking 3 and 2, and B to A taking 7: the averages are 2.5 and 7.0.
