Competitive programmer hit with the hardest algorithmic problem
Billed as the hardest coding interview by Clément Mihailescu, an ex-Google employee now at AlgoExpert, a high school student, William Lin, the interviewee, was given an algorithmic problem you might have been given at Google. William is not just any high schooler though, he is a competitive programmer, who expectedly would do well. Quite enticing, you might say.
Interviewee schools interviewer
Turns out William, the interviewee, teaches Clément, the interviewer, about Kosaraju's algorithm, directed graph, strongly connected components, condensed graph and so on. The guy knows algorithms and makes lots of little drawings. Clément (the interviewer) follows along, doing his best to keep up and complement with his own knowledge where he can.
Viewers discouraged and disgusted
The bad part of this YouTube video and the reason I'm writing down my thoughts on this lies in the YouTube comments. Almost everyone is bummed out. Some say they got some of it, some say they got the gist of it, but the solution William came up with, basically flew over everyone's head. Many commenters tell you that flat out.
This video now has over 2 million views. A large part of the thousands of comments is people getting turned off to programming.
And that's so sad! Comment after comment was that they took a look, as I did, and got more and more discouraged as one concept after the next was introduced and explanations for those concepts were being given, for loops were being added and variable names with little meaning were used to feed an algorithm.
But don't give up — there is a way
I'm a senior IT professional, who loves software development and has done his fair share of it, but I like to solve issues and work on projects that are beyond software: understand the actual business problem, propose innovative solutions to solve it, hire the people, write the software, procure the hardware, configure it and ultimately deliver a solution that hopefully everyone is happy with, can run for years, scales and is easy to maintain.
Maybe that gives me a different perspective.
How did I take the onslaught of algorithms, graphs and explanations? I stopped watching. After William started drawing circles and arrows pointing at other circles and arrows, I gave up. Why? I wasn't getting anything out of it, plus thought I understood the problem (not true after my first round of thinking, but true after two more) and I wanted to give it a shot myself. I felt that unconsciously my gears had started turning trying to tackle the problem.
The problem
The following are the input parameters from the mock interview.
let airports = ["BGI", "CDG", "DEL", "DOH", "DSM", "EWR", "EYW", "HND", "ICN", "JFK", "LGA",
"LHR", "ORD", "SAN", "SFO", "SIN", "TLV", "BUD"] // 18 airports, including the starting airport
let routes = [
["DSM", "ORD"],
["ORD", "BGI"],
["BGI", "LGA"],
["SIN", "CDG"],
["CDG", "SIN"],
["CDG", "BUD"],
["DEL", "DOH"],
["DEL", "CDG"],
["TLV", "DEL"],
["EWR", "HND"],
["HND", "ICN"],
["HND", "JFK"],
["ICN", "JFK"],
["JFK", "LGA"],
["EYW", "LHR"],
["LHR", "SFO"],
["SFO", "SAN"],
["SFO", "DSM"],
["SAN", "EYW"]
] // 19 one-way routes
let startingAirport = "LGA"
Use logic, not algorithms
What if I can solve this with pure logic? No learned algorithms. Just logic. What would I do if I was given the task to add the least amount of routes to an existing set of routes having to serve a list of airports from one given airport? What would I do manually at first?
Break down the problem by asking questions
What's the worst case?
There are no routes and I have to add a route to each airport. In this case, as the starting airport is one of 18 airports, I'd have to add one route from the starting airport to each other airport, so I would have to add 17 routes.
What's the best case?
There already is a route from the starting airport to each of the other airports, so I don't have to do anything — add 0 routes.
Somewhere in-between is the real situation: there are some routes connecting some of the airports.
What would be the most beneficial route to add?
One to an airport that has routes to all other airports. Then I'd only have to add that single route and would be done. That airport is a hub — all I have to do is connect to that hub from the starting airport and I'm done.
The solution
Building on the answers to the above questions, this should work:
- Make a list of airports with all the airports they connect to. Generally airlines are based out of one or more "hubs", which connect to most of their destinations.
- Find the biggest hub — the airport that will give me the most connections to other airports. That's my first connection.
- Look for the next biggest hub — the airport that gives me the most connections to airports the first airport didn't connect me to.
- Add a connection to that airport.
- Move on to the next biggest hub and so on.
- Each time after adding a connection to a hub, check if I have connections to all airports — if I do, I'm done.
- Count the connections. Done.
Let's try this programmatically
I have to know, for each airport, what all the airports are that connect to it. Data structure-wise, a map with the airport name as the key and an array of airports that connect to it as the value seems perfect. I'm going to use Swift, because I like it.
This is how you declare and initialize the map (it's called a dictionary in Swift) that's going to hold the list of all airports and all airports that they're reachable from.
var airportsReachableFrom = [String: [String]]()
1. Make an airport list
Building the list takes the most code, so I broke it down into three parts:
- Make a list of airports and where they're reachable from.
- Invert that list to get a list of airports and the airports they can reach.
- Sort the list, so it's easy to find the biggest hub (the one that can reach the most airports), then the next one down, and so on.
1a. Make a list of airports and where they're reachable from
To populate this map I'm going to read the routes one by one and make map entries by taking the destination — the "to" — as the key and the starting airport — the "from" — as the value of each map entry. So the first key/value pair I'm adding is:
Key: "ORD", Value: "DSM"
Then I have to backtrack into all the airports that point at "ORD", because if there is a route that leads to "ORD", then the from airport of that route leads to "DSM" as well (via "ORD"). Those would now be chained:
Some other airport => ORD => DSM
And there could be many other airports that lead to "ORD", so I have to recurse through them. Let's write a function called buildAirportsReachableFrom. It has a nested function and code that calls that nested function.
// go backwards from the route's tos
func buildAirportsReachableFrom(_ airports: [String], _ routes: [[String]], _ startingAirport: String) -> [String: [String]] {
// nested recursion function
func airportReachableFrom(_ airportsReachableFromKey: String, _ airport: String, _ routes: [[String]], _ airportsReachableFrom: inout [String]) {
for route in routes {
if route[1] == airport {
if !airportsReachableFrom.contains(route[0]) && route[0] != airportsReachableFromKey {
airportsReachableFrom.append(route[0])
airportReachableFrom(airportsReachableFromKey, route[0], routes, &airportsReachableFrom)
}
}
}
return
}
var airportsReachableFrom = [String: [String]]()
for airport in airports {
if airport != startingAirport { // don't bother looking at routes to the starting airport
airportsReachableFrom[airport] = [String]()
// recurse
airportReachableFrom(airport, airport, routes, &airportsReachableFrom[airport]!)
}
}
return airportsReachableFrom
}
That gives me this map — a list of airports (minus the starting airport), each with all the airports that can possibly lead to them.
"SIN": ["CDG", "DEL", "TLV"]
"TLV": []
"DOH": ["DEL", "TLV"]
"CDG": ["SIN", "DEL", "TLV"]
"SAN": ["SFO", "LHR", "EYW"]
"HND": ["EWR"]
"DEL": ["TLV"]
"EWR": []
"LHR": ["EYW", "SAN", "SFO"]
"ICN": ["HND", "EWR"]
"BGI": ["ORD", "DSM", "SFO", "LHR", "EYW", "SAN"]
"DSM": ["SFO", "LHR", "EYW", "SAN"]
"BUD": ["CDG", "SIN", "DEL", "TLV"]
"SFO": ["LHR", "EYW", "SAN"]
"EYW": ["SAN", "SFO", "LHR"]
"ORD": ["DSM", "SFO", "LHR", "EYW", "SAN"]
"JFK": ["HND", "EWR", "ICN"]
Which is a pretty good expression of the connections graph of the problem.
1b. Invert the list to get a list of airports and all the airports they can reach
I want to read the map and "invert" it, so I get the same list of airports — but instead of each airport showing all the airports that connect to it, with all the airports that they connect to. So I have to go through the map above one key/value pair at a time and scan through each of the airports (in the values) that connect to them and create a new list with those airports as the keys.
So the first key/value pair
"SIN": ["CDG", "DEL", "TLV"]
turns into 3 pairs
"CDG": ["SIN"] is added,
"DEL": ["SIN"] is added and
"TLV": ["SIN"] is added
The next line
"TLV": []
has no connections, but the next one
"DOH": ["DEL", "TLV"]
will add "DOH" to the value of the already created "DEL" key and create a new key/value pair for "TLV":
"DEL": ["SIN"] gets "DOH" added to become "DEL": ["SIN", "DOH"]
"TLV": ["DOH"] is added
Here's the code that does that. Let's call this function buildAirportsServingRoutesTo.
// invert the list
func buildAirportsServingRoutesTo(_ airportsReachableFrom: [String: [String]]) -> [String: [String]] {
var airportsTo = [String: [String]]()
for airportFrom in airportsReachableFrom {
for airport in airportFrom.value {
if airportsTo[airport] == nil {
airportsTo[airport] = [String]()
}
var airports = airportsTo[airport]!
airports.append(airportFrom.key)
airportsTo[airport] = airports
}
}
return airportsTo
}
Now we have the "inverted" list — all the airports with all the airports they connect to.
"EWR": ["HND", "ICN", "JFK"]
"CDG": ["SIN", "BUD"]
"EYW": ["SAN", "LHR", "BGI", "DSM", "SFO", "ORD"]
"TLV": ["SIN", "DOH", "CDG", "DEL", "BUD"]
"SIN": ["CDG", "BUD"]
"SAN": ["LHR", "BGI", "DSM", "SFO", "EYW", "ORD"]
"SFO": ["SAN", "LHR", "BGI", "DSM", "EYW", "ORD"]
"DEL": ["SIN", "DOH", "CDG", "BUD"]
"LHR": ["SAN", "BGI", "DSM", "SFO", "EYW", "ORD"]
"ORD": ["BGI"]
"HND": ["ICN", "JFK"]
"ICN": ["JFK"]
"DSM": ["BGI", "ORD"]
1c. Sort the list
Look for the airport that connects to the most other airports. Add it to a new list and remove it from consideration. Also remove all airports it connects to in all other key/value pairs. Then look for the next airport with the most connections — that's a recursion — until we have a sorted list starting with the airport with the most routes to the other airports.
Being that it also is a recursing function, it has the same structure as buildAirportsReachableFrom above, with a nested function and the code that calls it. Let's call this function orderAirportsServingRoutesTo.
// order the list by biggest hub first
func orderAirportsServingRoutesTo(_ airportsServingRoutesTo: [String: [String]]) -> [(String, [String])] {
// nested recursion function
func orderAirports(_ airportsByNumberOfTos: inout [(String, [String])], _ airportsServingRoutesTo: inout [String: [String]]) {
// get airport that serves most routes
var airportWithMostRoutes = ("", [String]())
for airport in airportsServingRoutesTo {
if airport.value.count > airportWithMostRoutes.1.count {
airportWithMostRoutes = (airport.key, airport.value)
}
}
if airportWithMostRoutes.1.count == 0 {
return
}
airportsByNumberOfTos.append(airportWithMostRoutes)
// remove tos that are already covered in airport with more routes
for airportServingRoutesTo in airportsServingRoutesTo {
if airportServingRoutesTo.key != airportWithMostRoutes.0 {
var airportWithMostRoutesInclKey = airportWithMostRoutes.1
airportWithMostRoutesInclKey.append(airportWithMostRoutes.0)
airportsServingRoutesTo[airportServingRoutesTo.key] = Array(Set(airportServingRoutesTo.value).subtracting(airportWithMostRoutesInclKey))
}
}
airportsServingRoutesTo.removeValue(forKey: airportWithMostRoutes.0)
if airportsServingRoutesTo.count > 0 {
orderAirports(&airportsByNumberOfTos, &airportsServingRoutesTo)
}
}
var airportsByNumberOfTos = [(String, [String])]() // array of tuples
var airportsServingRoutesToCopy = airportsServingRoutesTo
orderAirports(&airportsByNumberOfTos, &airportsServingRoutesToCopy)
return airportsByNumberOfTos
}
2.–6. Read the list and create the routes to add
Now we can read this list and create a new list of routes by adding routes to it for each of the airports until we've covered all the destinations or we've gone through the list. If we've run through all the airports but haven't covered all destinations yet, we have to create individual routes to all the missing airports. Let's write a function called determineRoutesToAdd.
func determineRoutesToAdd(_ startingAirport: String, _ airports: [String], _ orderedAirportsServingRoutesTo: [(String, [String])]) -> [[String]] {
var routesToAdd = [[String]]()
var airportsCopy = airports
// remove starting airport
airportsCopy = airportsCopy.filter { $0 != startingAirport }
// add routes to all airports that have tos
for orderedAirportsServingRouteTo in orderedAirportsServingRoutesTo {
routesToAdd.append([startingAirport, orderedAirportsServingRouteTo.0])
// remove the airport that a route was added to from the copy
airportsCopy = airportsCopy.filter { $0 != orderedAirportsServingRouteTo.0 }
// if all airports are covered
if airportsCopy.isEmpty {
break
}
// remove airports that have routes to them from the copy
for airportServed in orderedAirportsServingRouteTo.1 {
airportsCopy = airportsCopy.filter { $0 != airportServed }
}
}
// add direct routes to the tos that have no routes to them
for airport in airportsCopy {
routesToAdd.append([startingAirport, airport])
}
return routesToAdd
}
The result of this function is the routes we have to add, so that the starting airport is connected to all other airports. It is the answer to our problem.
7. Present the results
All we have left to do is call each function we've created above and print the result.
let airportsReachableFrom = buildAirportsReachableFrom(airports, routes, startingAirport)
var airportsServingRoutesTo = buildAirportsServingRoutesTo(airportsReachableFrom)
let orderedAirportsServingRoutesTo = orderAirportsServingRoutesTo(airportsServingRoutesTo)
let routesToAdd = determineRoutesToAdd(startingAirport, airports, orderedAirportsServingRoutesTo)
print("minimum routes to add: \(routesToAdd.count)")
print("routes:\n\(routesToAdd)")
Done — try it yourself
That's all the code. Below is a playground for it. The code was written in Swift 5 and works fine in Swift 6. Download the playground (it runs in Xcode or in the Swift Playgrounds app on Mac or iPad), or follow the swiftfiddle.com link to run the code online.
Final thoughts
Even this write-up is long and possibly difficult to understand. Don't be discouraged. Use the download and play with it in Xcode or Swift Playgrounds, or try the Swift Fiddle link. You can contact me here if you like.
Download .playground →