Backtracking: How to Approach Search Programming Interview Questions | by Andre Ye | Towards Data Science

Backtracking is an effective method for solving commonly asked programming interview algorithmic problems. Backtracking depth-searches for solutions and then backtracks to the most recent valid path as soon as an end node is reached (i.e., we can proceed no further). This method reduces the search space since any paths we know are invalid are eliminated from the solution space.
However, using backtracking requires the ability to test partial solutions — the method can’t find global optimum if there’s no progress indicator that the solution path we’re currently on leads to a solution or not. However, something like Sudoku can be solved with backtracking — it is clear immediately if the current solution path is invalid if, say, there are two of the same number in a row, column, or square.
Backtracking consists of three parts: a recursive function, which takes in a solution set (which may be incomplete n), a mutation or addition to n that adds on to set n, and a completion and validity check, which checks a solution set mutation’s completion (is the solution complete?) and validity (does it meet the requirements for validity? With the currently filled-in values, is this an acceptable solution?).
Backtracking automatically creates a clean and computationally efficient search tree based on very simple elements.
In this article, I’ll demonstrate, along with visuals, how backtracking can be used to solve three classic programming interview-style search questions.
The N-queens problem is a classic backtracking problem: given a chessboard of N by N size, how many ways can you arrange N queen pieces such that they do not threaten each other (one is not in the line of one queen’s column, row, or diagonals)?
As an example, this grid is an invalid move, because at least two of the eight queens is in another’s row, column, or diagonal, marked by a red square.
The brute-force solution to the problem is to try every single combination of N queens in each of the N by N spots — N2 choose N possibilities.
Graphing it out, where x represents N and y represents the number of possibilities to search through, it skyrockets.
Just for a standard 8-by-8 grid, there are 4,426,165,368 combinations, which is painfully slow. This can be partially improved by not putting two queens ever in the same row or column, but we still need to iterate over each row and each cell in each row — a runtime of O(N2), not fast enough.
Now, let’s go through a checklist of questions to determine if backtracking can help.
Yes, partial solutions can be constructed. By placing a certain number of queens on the board, we can partially construct a solution.
Yes, we can verify whether a partial solution is valid or invalid by checking if any two queens threaten each other. This can be sped up by acknowledging that the original queens do not threaten each other, and only checking if a newly added queen threatens any of the other queens.
Yes, the solution is complete when all N queens have been placed on the board. What’s great about backtracking is that when the solution is complete, it is also correct, because all incorrect solution paths have been ruled out beforehand.
Let’s construct a backtracking approach to solve the N-queens problem in faster time.
Function n_queens:
Intake N and board, a list of numbers.Initialize board as an empty list if it is not specified, and count to 0.For each column X in N,>> Add column X’s index to board.>> If board is valid, add the output of n_queens with inputs N and board to count (this will initialize another loop of the function, and is the tunneling or tree-building process of backtracking. Since the partial solution is considered valid in this case, it initiates another instance of function n_queens).>> Otherwise, remove the column X.Return count.
Intake N and board, a list of numbers.
Initialize board as an empty list if it is not specified, and count to 0.
For each column X in N,
>> Add column X’s index to board.
>> If board is valid, add the output of n_queens with inputs N and board to count (this will initialize another loop of the function, and is the tunneling or tree-building process of backtracking. Since the partial solution is considered valid in this case, it initiates another instance of function n_queens).
>> Otherwise, remove the column X.
Return count.
The function to evaluate if a board is valid or not is as follows:
Intake board, a list of numbers where each number corresponds to one column.The most recently added queen’s row is the current length of the board, minus one (because indexes start from 0), and the current queen’s column is the last element of the board.For each row R and column C in the board (which consists only of placed queens),>> Find the difference between the newly added queen’s column and the current column C. If it equals 0 (the two columns are the same), return False and break from the function.>> Find the difference between the newly added queen’s row and the current row R. If it equals 0 (the two rows are the same), return False and break from the function.Otherwise, return True.
Intake board, a list of numbers where each number corresponds to one column.
The most recently added queen’s row is the current length of the board, minus one (because indexes start from 0), and the current queen’s column is the last element of the board.
For each row R and column C in the board (which consists only of placed queens),
>> Find the difference between the newly added queen’s column and the current column C. If it equals 0 (the two columns are the same), return False and break from the function.
>> Find the difference between the newly added queen’s row and the current row R. If it equals 0 (the two rows are the same), return False and break from the function.
Otherwise, return True.
By recursively iterating new instances of a building function within a building function, backtracking is able to build a smart tree that doesn’t continue where the situation is hopeless: it is process-based instead of outcome-based.
The results, where the index indicates the value of N:
1, 1, 0, 0, 2, 10, 4, 40, 92, 352
Given an unordered list of someone’s flights, each represented with an (origin, destination) pair and a starting airport, compute the person’s itinerary. If none exists, return na — all flights must be used in the itinerary.
Let us take, as an example, these flights:
HNL ➔ AKL
YUL ➔ ORD
SFO ➔ HNL
ORD ➔ SFO
The brute force solution to the problem is to try every permutation of flights and verify if it is a valid itinerary, making it O(n!) time.
Let’s ask our three-question checklist again for using backtracking.
Yes, partial solutions can be constructed by listing out an incomplete itinerary, such as SFO ➔ HNL ➔ ORD ➔ ?.
The solution can be verified invalid if there are no flights leaving from the last destination in the itinerary and there are still flights remaining that can be taken. Since all flights must be used, we’ve reached an end node.
Yes, the solution is complete when the itinerary uses all the flights.
Constructing the logic of the solution:
A function get_itinerary:
Intake an unordered list flights with elements in the form (origin, destination), and current_itinerary, a list of the itinerary (where the first element is the starting airport and the second is the destination of the first flight and the origin of the second flight, and so on).If the flights have been all used up, return current_itinerary.The last_stop will be equal to the last element of current_itinerary.For each (origin, destination) pair in flights,>> Add the destination to the current_itinerary.>> If the origin is equal to last_stop (this is the check, and makes sure that the flight is a valid connection),>> >> Return another run of get_itinerary with all the flights except the current (origin, destination) pair, and break from the function.>> Otherwise, remove the most recent destination.Return None if none of the previous tree searches were able to return any solution.
Intake an unordered list flights with elements in the form (origin, destination), and current_itinerary, a list of the itinerary (where the first element is the starting airport and the second is the destination of the first flight and the origin of the second flight, and so on).
If the flights have been all used up, return current_itinerary.
The last_stop will be equal to the last element of current_itinerary.
For each (origin, destination) pair in flights,
>> Add the destination to the current_itinerary.
>> If the origin is equal to last_stop (this is the check, and makes sure that the flight is a valid connection),
>> >> Return another run of get_itinerary with all the flights except the current (origin, destination) pair, and break from the function.
>> Otherwise, remove the most recent destination.
Return None if none of the previous tree searches were able to return any solution.
You can probably observe that a large part of implementing backtracking has to do with using a function many times and building an automated, recursive search tree.
Note that for this particular problem, there is a much more efficient way to solve it. We just used backtracking as an example. In reality, a faster way would be just to take the starting airport (e.g., SFO), find the linking airport in the flights (SFO to HNL), and add whatever the destination is to the itinerary, recursively carrying on.
Can we use backtracking to solve a standard Sudoku puzzle?
Yes, we can partially fill in positions on the Sudoku board.
Yes, if the partial solution has any numbers that have the same number in a row, column, or square, the solution is invalid. Otherwise, it is valid.
Yes, the solution is complete when the entire Sudoku board is filled.
Let’s try filling each empty cell one by one, and backtrack once we hit an invalid state. That’ll look something like this:
Function sudoku:
Intake the variable board, which is an array, each value representing a cell on the Sudoku board.If the board is complete, return the board and break from the function.Let r and c represent the row and column of the first empty value on the board.For each value i in [1, 2, 3, 4, 5, 6, 7, 8, 9],>> Set the rth row and cth column cell to i.>> If the board is valid so far,>> >> Run another instance of sudoku given board.
Intake the variable board, which is an array, each value representing a cell on the Sudoku board.
If the board is complete, return the board and break from the function.
Let r and c represent the row and column of the first empty value on the board.
For each value i in [1, 2, 3, 4, 5, 6, 7, 8, 9],
>> Set the rth row and cth column cell to i.
>> If the board is valid so far,
>> >> Run another instance of sudoku given board.
The simplicity and cleanness of using backtracking to solve something as complex as a difficult Sudoku board are beautiful. Just outline a recursive function, a partial solution check, and a function to evaluate if a solution is complete, and the program will automatically build a tree for you and find the best solution, if any.
I hope this article was able to give you an understanding of how outlining three simple components of backtracking can outline rules for the program to sort itself out and find the best solution.