Problem overview
An underground railway wants to know how long trips take between stations. A passenger checks in with a card at the station where the trip starts and checks out at the station where it ends. The class UndergroundSystem records the trips and reports the average time between any two stations:
checkIn(id: Int, stationName: String, t: Int): Unitstarts a trip for the cardid.checkOut(id: Int, stationName: String, t: Int): Unitends that trip. It tooktminus the check-in time.getAverageTime(startStation: String, endStation: String): Doublereturns the average time of the finished trips fromstartStationtoendStation. Direction matters: a trip from B to A belongs to a different route than a trip from A to B. Trips still in progress do not count.
The statement guarantees that a passenger checks in only when not checked in already, checks out only after checking in, and that getAverageTime is only asked about routes with at least one finished trip.
Example: card 45 goes from Leyton to Waterloo between times 3 and 15 (12), card 27 on the same route between 10 and 20 (10), and card 32 from Paradise to Cambridge between 8 and 22 (14). The average for Paradise to Cambridge is 14.0, and for Leyton to Waterloo it is (12 + 10) / 2 = 11.0. A card that has checked in but not out yet changes nothing.
Approach
The simple way and its cost
Store every finished trip as a record (start, end, duration) in a list. To answer getAverageTime, go through the list, pick the trips on the route and average them. That costs O(n) per question, where n is the number of trips so far, and the list keeps growing. The lesson asks for O(1) per operation.
Keep only what the answer needs
An average is a total divided by a count. So for each route the class only needs two numbers: the sum of all the trip times and the number of trips. At each check-out both change by a small amount, and getAverageTime becomes one lookup and one division.
The class keeps two hash maps:
checkInsholds the trips in progress: card id to a pair(station, time).routesholds the finished trips, per route: the route written as one string,"start,end", to the pair(total time, number of trips).
The comma in the route's name keeps stations apart. Without it, the stations "A" and "BC" would give the same text, "ABC", as "AB" and "C". Because the route is built in the order start, then end, the route from A to B and the route from B to A are different keys.
When a passenger checks out, the class removes their entry from checkIns. That ends the trip and also lets the same card check in again later, as in the case of one card making two trips.
Step by step
- Declare
checkIns, amutable.HashMap[Int, (String, Int)].(String, Int)is a tuple type: a station and a time. - Declare
routes, amutable.HashMap[String, (Int, Int)]: a route name to its total time and its trip count. - In
checkIn, store the pair(stationName, t)under the card:checkIns(id) = (stationName, t). The trip's duration is not known yet. - In
checkOut, take the check-in out of the map withcheckIns.remove(id).getand unpack it:val (startStation, startTime) = ....removedeletes the entry and returns its old value inside anOption, and.gettakes the value out of it. - Build the route's name as
startStation + "," + stationName. - Read the route's totals with
routes.getOrElse(route, (0, 0))and unpack them intototalandcount. A route with no trips yet starts at zero. - Store the new totals:
routes(route) = (total + t - startTime, count + 1). Only two numbers change, however many trips came before. - In
getAverageTime, read the pair forstartStation + "," + endStationfromroutes, built with the comma exactly as incheckOut. - Return
total.toDouble / count. Converting the total to aDoublefirst keeps the fraction.
The solution in Scala
class UndergroundSystem {
val checkIns = mutable.HashMap[Int, (String, Int)]()
val routes = mutable.HashMap[String, (Int, Int)]()
def checkIn(id: Int, stationName: String, t: Int): Unit = {
checkIns(id) = (stationName, t)
}
def checkOut(id: Int, stationName: String, t: Int): Unit = {
val (startStation, startTime) = checkIns.remove(id).get
val route = startStation + "," + stationName
val (total, count) = routes.getOrElse(route, (0, 0))
routes(route) = (total + t - startTime, count + 1)
}
def getAverageTime(startStation: String, endStation: String): Double = {
val (total, count) = routes(startStation + "," + endStation)
return total.toDouble / count
}
}
A short run, with one card making two trips on the same route:
val u = new UndergroundSystem
u.checkIn(10, "Leyton", 3)
u.checkOut(10, "Paradise", 8)
println(u.getAverageTime("Leyton", "Paradise"))
u.checkIn(5, "Leyton", 10)
u.checkOut(5, "Paradise", 16)
println(u.getAverageTime("Leyton", "Paradise"))
u.checkIn(2, "Leyton", 21)
u.checkOut(2, "Paradise", 30)
u.checkIn(10, "Leyton", 32)
u.checkOut(10, "Paradise", 37)
println(u.getAverageTime("Leyton", "Paradise"))
println(u.routes)
5.0
5.5
6.25
HashMap(Leyton,Paradise -> (25,4))
The four trips take 5, 6, 9 and 5, so the route holds a total of 25 and a count of 4. The lesson's tests print averages with two decimals, so the values above show as 5.00, 5.50 and 6.25.
Scala details:
- Tuple unpacking.
val (total, count) = pairdeclares two values at once from the two elements of a tuple. remove(id).get.removeon a mutable map returns anOption:Some(value)if the key was there,Noneotherwise. Calling.getonNonethrowsNoSuchElementException. The statement guarantees that a check-out always has a check-in.getOrElseagainstroutes(route).getOrElse(route, (0, 0))returns the default when the key is missing. Plainroutes(route)throws for a missing key, which is acceptable ingetAverageTimeonly because the statement promises that the route has a trip.checkIns(id) = ...is shorthand forcheckIns.update(id, ...). It inserts the card, or replaces the value if the key was present.- Integer versus floating-point division. Two
Ints divide to anInt:11 / 2is5. With one side aDouble,11.toDouble / 2is5.5. That is whytotal.toDoublecomes first. - The lesson's code uses
mutablewithout a prefix. In a file of your own, addimport scala.collection.mutable.
Complexity
Time. Every operation is a few hash-map operations on average, so it is O(1), apart from building the route's name, which takes time proportional to the lengths of the two station names (at most 10 characters each here).
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 route's two numbers.
Edge cases and common mistakes
- Integer division.
total / countwith bothInts drops the fraction: a total of 11 over 2 trips would give 5 instead of 5.5. The result type isDouble, so writetotal.toDouble / count. - The direction of the route. A to B and B to A are different keys. After trips A to B of 3 and 2 minutes, and a trip B to A of 7,
getAverageTime("A", "B")is 2.5 andgetAverageTime("B", "A")is 7.0. - Leaving the check-in in the map. If
checkOutusedcheckIns(id)without removing it, the old entry would stay behind forever. A card that checks in again simply overwrites it, but the map would hold finished trips. - Counting unfinished trips. Only
checkOuttouchesroutes, so a passenger who has checked in but not out does not change any average. - A card travelling twice. Each check-out removes the card's entry, so the card can check in again, and both trips count.
- Route names without a separator. Joining the stations with no comma can make two different routes collide, such as
"A"to"BC"and"AB"to"C". - Asking about an unknown route.
routes(...)throwsNoSuchElementExceptionfor a route with no finished trip. The statement rules this case out.
Testing
- Cards 45 and 27 travel Leyton to Waterloo (3 to 15, and 10 to 20), and card 32 travels Paradise to Cambridge (8 to 22).
getAverageTime("Paradise", "Cambridge")is 14.00, andgetAverageTime("Leyton", "Waterloo")is 11.00. AftercheckIn(10, "Leyton", 24)with no check-out, it is still 11.00. - Four trips from Leyton to Paradise of 5, 6, 9 and 5 minutes give 5.00, 5.50 and then 6.25 after the fourth.
- Trips A to B of 3 and 2 minutes and one trip B to A of 7 minutes give 2.50 and 7.00.
