I began by discussing a problem we discussed in section. Suppose that you are dealing with the Cartesian plane and you want to think about all possible paths from the origin (0, 0) to a particular point. Suppose, for example, that you want to get to the point (1, 2):
^ | | * (1, 2) | <---------------+---------------> | | | VSuppose that you are limited to just three kind of moves:
choice #1 | +----------------+-------+--------+----------------+ ... | | | | 1st possibility 2nd possibility 3rd possibility 4th possibilityEach choice we might make at the top of the tree leaves us in a different part of the solution space. From there, we consider all possible second choices we might make. For example, if for choice#1 we chose the 3rd possibility, then we'd have to consider the choices we could make from there:
choice #1 | +----------------+-------+--------+----------------+ ... | | | | 1st possibility 2nd possibility 3rd possibility 4th possibility choice #2 | +----------------+-------+--------+----------------+ ... | | | | 1st possibility 2nd possibility 3rd possibility 4th possibilityIt's difficult to provide a good picture of this in these notes (or even on the overhead for that matter), because you have to realize that there is a fanning out at the top of the tree into several possibilities and from each of those there is a fanning out of possibilities, and from each of those there is a fanning out of possibilities and so on. So this decision tree is potentially gigantic.
But for our problem with just three choices, it's a bit easier to draw the decision tree. We have to think of the problem in terms of a series of choices. Here the choices are simply the first move, the second move, the third move, and so on. So we draw the first level of this tree indicating the three choices available as a first move and where they would leave us:
start (0, 0) | +--------------------+--------------------+ | | | N NE E (0, 1) (1, 1) (1, 0)The leftmost branch corresponds to a first move of going North, which leaves us as (0, 1). We added another level to the tree underneath this indicating where various second moves would lead us from there:
start (0, 0) | +--------------------+--------------------+ | | | N NE E (0, 1) (1, 1) (1, 0) | +------+------+ | | | N NE E (0, 2) (1, 2) (1, 1)The normal recursive backtracking approach leads you to constantly "drill down" on the first choice available, so we added a third level for the first of these three new choices:
start (0, 0) | +--------------------+--------------------+ | | | N NE E (0, 1) (1, 1) (1, 0) | +------+------+ | | | N NE E (0, 2) (1, 2) (1, 1) | +------+------+ | | | N NE E (0, 3) (1, 3) (1, 2) dead dead yes!Notice that the first of these new choices leads to a dead-end. It's normal to reach a dead-end while backtracking. When that happens, you simply back up to where you last had a choice. So that means we think about choosing NE instead of N for our third move. That also leads to a dead-end. But then we come to choosing E as our third move, and that's a solution. So we've found the solution that involves the sequence of moves N, N, E. Once you've found a solution, you simply go back to where you last had a choice. We've exhausted all of the possible third moves after going N, N, so we back up to the second move and consider a second move of NE. This turns out to be another solution. Then we consider the other possibility for the second move, which is moving E. That leads us to build a new subtree with all of the possible third choices after moving N, E.
We continued in this way, ending up with the following complete tree:
start (0, 0) | +---------------------------+--------------------+ | | | N NE E (0, 1) (1, 1) (1, 0) | | | +------+-------------+ +------+------+ +------+------+ | | | | | | | | | N NE E N NE E N NE E (0, 2) (1, 2) (1, 1) (1, 2) (2, 2) (2, 1) (1, 1) (2, 1) (2, 0) | yes! | yes! dead dead | dead dead | | | +------+------+ +------+------+ +------+------+ | | | | | | | | | N NE E N NE E N NE E (0, 3) (1, 3) (1, 2) (1, 2) (2, 2) (2, 1) (1, 2) (2, 2) (2, 1) dead dead yes! yes! dead dead yes! dead deadWe found all five solutions. This technique could be used to find all possible paths to any location.
Then I said that I wanted to solve a classic problem known as "8 queens". The challenge is to place eight queens on a chessboard so that no two queens threaten each other. Queens can move horizontally, vertically or diagonally, so it is a real challenge to find a way to put 8 different queens on the board so that no two of them are in the same row, column or diagonal.
The simplest way to think of this in terms of a decision tree is to imagine all the places you might put a first queen. There are 64 of them because the chess board is an 8 by 8 board. So at the top of the tree, there are 64 different choices you could make for placing the first queen. Then once you've placed one queen, there are 63 squares left to choose from for the second queen, then 62 squares for the third queen and so on.
The backtracking technique we are going to use involves an exhaustive search of all possibilities. Obviously this can take a long time to execute, because there are lots of possibilities to explore. So we need to be as smart as we can about the choices we explore. In the case of 8 queens, we can do better than to consider 64 choices followed by 63 choices followed by 62 choices and so on. We know that a whole bunch of these aren't worth exploring.
One approach is to observe that if there is any solution at all to this problem, then the solution will have exactly one queen in each row and exactly one queen in each column. That's because you can't have two in the same row or two in the same column and there are 8 of them on an 8 by 8 board. So we can search more efficiently if we go row by row or column by column. It doesn't matter which choice we make, so let's explore column by column.
In this new way of looking at the search space, the first choice is for column 1. We have 8 different rows where we could put a queen in column 1. At the next level we consider all of the places to put a queen in column 2. And so on. So at the top of our decision tree we have:
choice for column 1? | | +------+------+------+---+--+------+------+------+ | | | | | | | | row 1 row 2 row 3 row 4 row 5 row 6 row 7 row 8So there are 8 different branches. Under each of these branches, we have 8 branches for each of the possible rows where we might place a queen in column 2. For example, if we think just about the possibility of row 5 for the queen in column 1, we'll end up exploring:
choice for column 1? | | +------+------+------+---+--+------+------+------+ | | | | | | | | row 1 row 2 row 3 row 4 row 5 row 6 row 7 row 8 | choice for column 2? | +------+------+------+---+--+------+------+------+ | | | | | | | | row 1 row 2 row 3 row 4 row 5 row 6 row 7 row 8Again, the pictures I can draw in these notes aren't very good because the tree is so big. There are 8 branches at the top. From each of these 8 branches there are 8 branches. And from each of those branches there are 8 branches. And so on, eight levels deep (one level for each column of the board).
What happens in backtrack is that we explore specific choices until we reach a dead end. Once we find that some path is not going to work out, we back up to where we last made a choice and make a different choice. That's where the idea of "backtracking" comes from (backing up to the last place we made a choice and moving on to the next choice).
For example, we might choose to explore placing a queen in row 1 for column 1. So we go down that branch. We will explore all of the possibilities in that branch. If they all lead to dead ends, then we'll come back to the top level and move on to the possibility of having a queen in row 2 of column 1. We explore all of those possibilities. And if they all fail, we try row three for column 1. And so on. As long as we have more choices to explore, we keep trying the next possibility. If they all fail, we conclude that this is a dead end.
It's clear that the 8 choices could be coded fairly nicely in a for loop, something along the lines of:
for (int row = 1; row <= 8; row++)But what we need for backtracking is something more like a deeply nested for loop:
for (int row = 1; row <= 8; row++) // to explore column 1 for (int row = 1; row <= 8; row++) // to explore column 2 for (int row = 1; row <= 8; row++) // to explore column 3 for (int row = 1; row <= 8; row++) // to explore column 4 ....That's not a bad way to think of what backtracking does, but we're going to use recursion to write this in a more elegant way.
Before we looked at the recursive solution to the problem, I mentioned something I learned from an old colleague of mine named Steve Fisher who used to teach the cse143 equivalent at Stanford in Pascal back in the 1980's. Steve pointed out to me that these recursive backtracking problems are much easier to write and to understand if you separate off the low-level details of the problem. For the 8 queens problem he created supporting code for what we would today recognize as a board object to keep track of a lot of the details of the chess board.
So I began by discussing what kind of methods we'd want to have for a board object. Obviously it would need some kind of constructor. I mentioned that we want to pass it an integer n so we could solve the more general "n queens" problem with an n-by-n board. So we want:
public Board(int size)I asked for more suggestions. Someone said we need to be able to test whether it's safe to place a queen at a particular location:
public boolean safe(int row, int col)What else? Someone said we need a way to place a queen on the board:
public void place(int row, int col)Someone else mentioned that we'd need a way to remove a queen because the backtracking involves trying different possibilities. So we need:
public void remove(int row, int col)Anything more? Someone suggested that we need a way to find out where the queens are placed. I said that for our purposes, let's settle for a method that just prints out the entire board, queens and all:
public void print()Anything else? Perhaps I'm just a lazy programmer, but I'd like to be able to ask the board what size it is:
public int size()So I told people to assume that we have a Board class that implements all of those methods. In the handout I include a class that does all those things. But that's not the interesting code. The interesting code is the backtracking code which, given this class, we can now write in a very straightforward manner.
I mentioned that in the program I wrote, I prompt the user for a board size and I construct a board of that size. Then I call a method called "solve", passing it the Board object:
public static void solve(Board b) { ... }I mentioned that one of the issues that comes up with recursive programming is that we are often asked to write a method like this and we discover that we want to have the recursion work in a different way. For example, we might want more parameters or we might want to have a different return type. So it's common with recursive programming to introduce a "helper" method that does the actual recursion. That's what we'll do here.
So how is the recursion going to work? I pointed out that there are different ways to approach a backtracking problem. You can decide that you want to find all solutions or you might decide that one is enough. In this case, I decided that one is enough. In other words, if we ever find a solution to the problem, we'll stop exploring. That means that our recursive method will need to have a way to let us know whether a certain path worked out or whether it turned out to be a dead end. A good way to do this is to have a boolean return type and to have the method return true if it succeeds, false if it is a dead end.
I mentioned that a good name for our recursive method would be "explore". So what we know so far is that it looks like this:
public static boolean explore(...) { ... }What parameters will it need? Someone fairly quickly pointed out that it needs the Board object. Anything else? This seemed to stump people for a while. That's not surprising, because we're trying to understand how this recursion is supposed to implement the backtracking.
I said that each level of the decision tree will be handled by a different invocation of the method. So one method invocation will handle column 1. A second invocation will handle column 2 and so on. So what will the method need to know besides where the Board is? Someone said it needs to know what column to work on. So that leaves us with this header:
public static boolean explore(Board b, int col) { ... }I mentioned that we don't want to waste our time exploring dead ends. So we want to think in terms of what kind of precondition we'd want for this method. For example, suppose that I've placed queens in columns 1, 2 and 3 and I'm thinking of calling the method to explore column 4. Would that make sense if the first three queens have not been placed safely? The answer is no. If the first three queens already threaten each other, then why bother placing any more? Once something becomes a dead end, we should stop exploring it. So I mentioned that the method should have the following precondition:
// pre: queens have been safely placed in previous columnsI told people that this precondition will turn out to be very helpful to us as we think through the different cases. We're using recursion to solve this, so we have to think about base cases and recursive cases. So I asked people, "What would be a nice column to get to?" Some people said "8" and I said it would be nice to get to column 8 because it would mean that 7 of the 8 queens have been placed properly. But other people had an even better answer. It would be even better to get to column 9 because the precondition tells us that if we ever reach column 9, then queens have been safely placed in each of the first eight columns.
This turns out to be our base case. What do we do if we get to column 9? If we get there, we'd know that we've found an answer. In this problem we want to stop when we find an answer, which means we return true. We also want to write this in the more general way by testing against the size of the board rather than hard-coding a number like 8 or 9:
if (col > b.size()) return true; else { ... }What about the recursive case? For example, maybe 4 queens have already been placed and we've been called up to place a queen in column 5. We have 8 possibilities to explore (the 8 rows of this column where we might place a queen). A for loop works nicely to go through the different row numbers.
What do we do with each one? First we'd want to make sure it's safe to place a queen in that row of our column. If not, then it's a dead end that's not worth exploring. So our code will look like this:
for (int row = 1; row <= b.size(); row++) if (b.safe(row, col)) { ... }And how do we explore a particular row and column? First we place a queen there. And then we'd explore the other choices. In this case, the other choices involve other columns that come after our column. That's where the recursion comes in:
for (int row = 1; row <= b.size(); row++) if (b.safe(row, col)) { b.place(row, col); explore(b, col + 1); ... }I pointed out that people often use an expression like "col++" in their recursive calls, but it's not a good idea. In this case, because we're in a for loop, we'd be using different column numbers for each row.
Another problem with this code is that we don't want to call explore like this. Remember that explore returns a boolean value that indicates whether or not it worked. If it does work, what are we supposed to do? We're supposed to stop exploring, which means we'd want to return with the value true:
for (int row = 1; row <= b.size(); row++) if (b.safe(row, col)) { b.place(row, col); if (explore(b, col + 1)) return true; ... }But what if it doesn't work? What if it turns out to be a dead end? Then we want to move on to the next possibility and explore it. That means just coming around the for loop to try the next row. But it's not quite that simple, because we have placed a queen in the current row. We have to "undo" the current choice before we move on to the next choice:
for (int row = 1; row <= b.size(); row++) if (b.safe(row, col)) { b.place(row, col); if (explore(b, col + 1)) return true; b.remove(row, col); }This pattern of making a choice (placing a queen), then making a recursive call to explore later choices, and then undoing the choice (removing the queen) is common in recursive backtracking.
If we get all the way through this for loop without finding a solution, then we'd simply return false to indicate that this has turned out to be a dead end.
It turns out this is the entire recursive backtracking code. Putting it all together we get:
public static boolean explore(Board b, int col) { if (col > b.size()) return true; else { for (int row = 1; row <= b.size(); row++) if (b.safe(row, col)) { b.place(row, col); if (explore(b, col + 1)) return true; b.remove(row, col); } return false; } }We need some code in the "solve" method that starts the recursion in column 1 and that either prints the solution or a message about there not being solutions:
public static void solve(Board solution) { if (!explore(solution, 1)) System.out.println("No solution."); else { System.out.println("One solution is as follows:"); solution.print(); } }But otherwise we're done. I brought up a full version of the program and ran it to show people that it finds a solution to 8 queens, 4 queens, and so on. In the case of 2 queens, it finds there are no solutions.
I then brought up a variation of the program that includes an animation. The animation is generated by the backtracking itself. I can't in these notes capture what that dynamic animation shows, so I'll just say that if you weren't in class to see it, I highly recommend that you download the queens zip file from the calendar or handouts page and compile and run the Queens2 program.