Puzzle 6: A Profusion of Queens

Flash and JavaScript are required for this feature.

Download the video from Internet Archive.

SRINI DEVADAS: Good morning, everyone. Thanks for coming in on a Friday morning, especially with the long weekend coming up. We're going to pick up where we left off yesterday. And so if you recall, we were looking at an iterative approach to solving the eight queens problem. And in case you've forgotten in the, I guess, intervening 20 hours, the eight queens problem, which is a specific instance of n queens is you have a chess board-- and I won't draw all the squares-- which is, obviously, an 8 by 8 board. And you want to place eight queens on this board such that no two queens attack each other.

And we looked at different data structures to solve this. The naive data structure would be to represent the board as a two-dimensional list. And so you end up having an 8 by 8, if you have a chessboard for the eight queens problem. We looked at a 4 by 4 for starters.

And then after that, we looked at compressing that representation down to a one-dimensional list where let's say you had a 5 by 5 board because we've got five entries here. You exploit the fact that there can be at most one queen on any given column. And you say I'm going to represent the position of that queen using a single number where is from, in this case 0 through 4, if I have a 5 by 5 board, and you put that in here.

So a minus 1 would represent this being empty. A 2 would represent a queen in the second column in this position over here, et cetera, et cetera. So with this more compact representation, it turns out that the conflict check becomes pretty straightforward.

And if you remember, our conflict check is incremental in the sense that what you're going to do is assume that queens have already been placed in some number of columns. I mean, it could be zero. In this case it's 2. And you're going to try and check to see when a third queen is placed on this third column, number two, whether it conflicts with the existing queens.

But you don't have to worry about the checks associated with the existing queens because you've already taken care of that. So that's the incremental nature of this check.

And I won't go over this code again, but the check associated with ensuring that when you place a queen on a given column, it checks the diagonals. That's the most interesting part. And you put a queen here, you need to check this, and you need to check that. And in this case, obviously there's a conflict. And because there are two diagonals, that's why you have the ABS, the absolute value over there, and you're exploiting the fact that the diagonal is a diagonal of a square, and how much you move up should be how much you move to the right or down, left, or what have you.

Another check is simply ensuring that when you look at the particular column and you see the number current is a particular column, the number associated with that particular column shouldn't be repeated in some other column. Because that would imply that if you saw a 2 and a 2 here, obviously, that would imply that you have two queens on the same row, and you can't have that.

So that's the story. And we decided to use iteration because, at this point in time, you know nothing else in terms of control flow. I know that's not the case, but we assume that. And so you end up having this ugly code associated with eight nested loops that does this incremental enumeration. So board 0 being i implies that you're taking the first column and you're iterating the positions of the queen in the rows of the first column, and so on and so forth.

And if you go ahead and run this, which we did last time you end up getting 92 different solutions to the eight queens problem, and as I mentioned, there's only 12 distinct ones. So if you take rotation and reflection into account and each of these is a legitimate solution, that needs to be translated into the picture that you see here by essentially taking this data structure that you see. 7 becomes the queen in the left corner, et cetera.

So we absolutely wouldn't want to publish this code. I guess, in effect, I'm doing it. But you would not want to claim authorship of this code especially if you know about the programming technique that's called recursion.

So that's really what we're going to do today. We're going to look at a couple of different puzzles. We'll finish up with the n queen's puzzle fairly quickly, and we'll look at how we could use recursion, which is an algorithm paradigm as well as a programming paradigm to solve challenging problems.

And there's really two classes, at least, of recursive algorithms, and you're going to see both of those today. The first one is nQueens, and I'll get to the second one when we get to the second one.

So our goal now is to take that ugly code and make it pretty. And before I get into that, I want to show you a simpler example of iteration versus recursion. And this is about as simple as it gets while still being interesting from an algorithmic standpoint.

So the greatest common divisor, Euclid's age-old algorithm to compute the greatest common divisor. And you see the two line, three line, what have you iterative algorithm up there that corresponds to taking the pair of numbers, obviously, m and n that you need to compute the greatest common divisor for. And all you do is assign m to n and n becomes n mod m.

And I'm not going to explain why this algorithm is correct or anything like that. I'm not particularly interested in that. What I'm much more interested in is ensuring that you understand that the iterative algorithm is the same as the recursive one. The recursive one gets you the correct answer if you assume the iterative algorithm is correct and vice versa, and that's really what this is about. So we won't argue if Euclid is correct or not.

But the point here is you could have translated-- well, you did translate this iterative algorithm into a recursive one. And there's a couple of things I want you to keep in mind when you look at recursive code and certainly when you write recursive code. And it's much easier pointing this out in a simple example like this.

So first off, when we say recursion-- and I should have probably said this a little bit earlier-- recursion is something calling itself. So you can say a function, f, calling itself, that's the simple case. We also consider if you have f calling g, which calls f, then that is recursive as well. And see you can have arbitrary structures that correspond to nested recursion. We're really only going to be looking at functions at least in this lecture, where you have a single function, f, calling itself, which is exactly the case for our RGCD, the Recursive GCD that you see here.

You see a call RGCD inside of RGCD, and you know this function is recursive. Sometimes you may look at a function and you don't see that immediate syntactic evidence that the function is recursive, but it may be recursive because of nested recursion, f calling g, more complicated recursion. So this is clearly a recursive function, RGCD. And two things that you need to look for if you want to write correct recursion, I mean this is not sufficient to write correct recursion, but necessary.

And in particular, when I say correct recursion, I want it to be non-terminating, so the problem with something calling itself is, you think of it as there's some sort of cycle and you absolutely need to break out of the cycle, otherwise, you will be in the cycle forever. You could call something which calls something, which calls something that goes on, your program isn't going to terminate. So you always want to be careful when you write recursive code that there's a base case. There's a termination condition.

So there should be some part of the code where if certain conditions are satisfied, you're not making a recursive call. That is what I would consider the base case. That really comes from induction and from the mathematics of recursion. You want to have a base case and then the recursion is really the inductive step.

If that doesn't make too much sense, don't worry about it. You can think of it syntactically. Look for a path in the code where you don't see RGCD inside of RGCD. And you can clearly see that in the if m mod n equals equals 0, return n.

So you want to have what we call the base case where a function returns. That's not enough because you don't quite know if you're going to get to that base case or not. You're going to call this function with some arguments, and you want to get some sense that the arguments are shrinking as you go. And it could even be the other way around. The arguments may increase, and once you grow beyond a certain threshold, you fire off on the return without making the recursive call.

So this notion of the arguments becoming smaller is very common in the case of the algorithmic paradigm that we'll be following in this class where we're going to take large problems like a eight queens or n queens and go to n minus 1 queens and n minus 2 queens. And divide and conquer, which is other paradigm that we look at. As you can see from the name is taking a big problem and making it smaller.

So really for the most part, you're going to essentially have a situation where you're going to say that the arguments to the function should be smaller in some sense. And smaller could mean bigger. I mean, it's just a definitional thing. I mean, the point is when you get to the base case, usually you are saying, as you can see in this base case, you're saying when it comes down to the fact that a particular condition is satisfied, in this case, it's the relationship between m and n. And if that's 0, then we return n.

But in other cases, it just might be when you get to a one queen problem, you're done. I mean, you have no choice. You've got this 1 by 1 board and you've got to put your queen on it, and obviously, that might create a conflict, but you're done. I mean, there's no two things about it if you have one queen problem. There's only one step. You put your queen down.

So that's where we were at with respect to the puzzles that we're going to be doing in this class where you're going to shrink things down. So in that sense, smaller would mean smaller. It's not necessarily in quotes. So that makes sense? Any questions at all about syntax or anything that you see on the board here? All right good.

So now, I'm ready to show you what the n queens code looks like. One of the other problems, of course, with the eight queens code is that it only works for eight queens, and it doesn't work for nine. And that's annoying, so you'd like to have a procedure that takes n as an argument and works for n equals 4, n equals 8. And eventually, you could run this on n equals 20, and it will finish. I'll show you.

But there is an exponential relationship between run time and n because it's just a complicated problem. There's a lot of combinations. So you do not want to run this code for more than n equals 25.

It hasn't completed. I think at some point I did an analysis on how long it would take for n equals 30, and it wasn't something I wanted to wait for. I mean, my computer would have gotten old and died before the program came back. Which is a reasonable experiment to carry out, but I like my computer. I'm kind of annoyed with it because of the new operating system, but I generally like it.

So you can see that we're going to use exactly the same framework. We're going to use the same no conflicts routine. There's no change there, no change in data structures. There's no change in really even the incremental strategy that we're following. But we can take that intuitive code and turn it into this pretty code that has about five lines, or seven lines, what have you.

And so you can see, again, that if you look at that code, I mean, it's not that much more complicated. There's more going on in there. There's more going on because it's a more interesting problem at some level than GCD, but it has the same characteristics in terms of having a base case, and it's actually an easy one to look at. And it also has a property 2 here where the argument to the nQueens is going to be one less than what the caller function has as its argument because you're going column by column, and you're taking away.

In this case, you're not necessarily going to a 7 by 7, really. It's not that you're going from 8 by 8 to 7 by 7. You're going from 8 by 8 to 7 by 8. You have one less column. You still have to work with all the rows.

So that's really what's going on out here. If current equal size-- so what's happening is current is actually incrementing. So you're moving and what is remaining is becoming smaller. And current starts with-- so if you look at nQueens here, it says nQueens, a size, and rQueens, which is this routine here, it sets current to be 0. And size is, of course, whatever you give nQueens, which could be 8 or 20.

And finally, if you look at this part of the code, you get to the point where you run out of columns when current equals size. And at this point, you put all the queens down, and that things haven't fallen apart-- you haven't returned false-- and therefore, you're done. So when you get to that quote ninth column, you're off the board, and you're done. And all of the existing queens, the eight queens that were put on the board, didn't conflict with each other. That's how it works.

And then over here, you've taken your eight loops that were in the eight queens problem or the for loops in the four queens problem and you've gone off and turned it into one loop. And the reason that works is because rQueens is calling rQueens out here. So that's your recursive call.

And then the argument here is current plus 1, and it's getting closer and closer to size as you go around, which means that it's effectively getting smaller. What you're working with is getting smaller. So if of look at what happens here, you can trace the execution of this, and this is worthwhile doing. And I'll put this in the notes, and you can look at it for a 4 by 4.

But in general, if you're confused about recursion, it's absolutely worthwhile to trace the execution for small arguments and figure out exactly when things are terminating and when things are being called recursively. And this is simply a single call. It looks like it's a single call, because there's only one instance of rQueens. But if you look at the code, how many calls would I possibly make if I were sitting with an eight queens problem and I started with size equals 8?

Then in that very first, I invoke rQueens with current equal 0 and size equals 8. How many rQueens calls will the first rQueens make? How many times would potentially make that? So if I look at the branching here and I say this is rQueens and this has current equals 0 and size equals 8, the way you want to think about this from a tracing standpoint is you're making calls. rQueens is calling rQueens.

And obviously, there is rQueens in the middle of that code over there. And so what is the breadth here in terms of the number of calls? Someone? Trace the code. So i in range 8, which means 0 through 7, and then I'm going to go off, and this is the very first one. So noConflicts is going to constantly return-- it's the first queen. So noConflicts is going to constantly return true.

So I'm going to go inside of the if, and I'm going to make that rQueens call. So I'm going to have eight different calls associated with each of the positions that corresponds to the queen on that first column. And then each of these could potentially make eight different calls, potentially I said because as you get deeper in the recursion, the noConflicts is going to fire false, and it may not make the call. But obviously, the very first one, you're going to make eight calls.

So recursion is scary from a performance standpoint to some people and rightly so because things could explode on you, and this goes back to what I said about running this with n r size equals 25 and so on and so forth. And one of the things that is worth doing and you have to do this type of analysis in classes like 6.006, not a lot of this but because we don't do exponential algorithms.

But you can look at this tree that is being generated by these recursive calls and you do want to get some sense of how large this tree is going to become. And we're going to look at a different class of algorithms. Most of algorithms we're going to look at that are recursive for the rest of this course including the next one that we'll get to in a few minutes, aren't as bad as this one in terms of computational complexity. But n queens is a hard problem. It needs exhaustive search, and there's a lot of combinations.

And so it's not an efficient algorithm. Efficient means polynomial times something like n square quadratic, n cubed, and so on. And this is not efficient, and the reason for that is this explosion in the number of recursive calls.

So it's easy to write five lines of code that takes forever to run. It will terminate. It's not that it had an infinite loop in it, but the number of combinations explodes on you because of eight and then eight again for each of these and then-- and so on. So you can see that gets pretty large pretty quick.

So this code is something that, as I said, is essentially the same. It's sitting there computing nQueens 20. That's why this is not a problem with my laptop, at least, as of this moment, I don't think it's a problem with my laptop. And so there you go.

So nQueens 20 gave you the representation that we chose. You can try and convince yourself that this, in fact, is correct. I haven't. But that code is so straightforward at this point that if there is anything wrong with it, it would be catastrophically wrong. It would just crash or it wouldn't return anything. And so that is one solution.

There's many, many solutions. Unlike the eight queens case, where I ran it to completion and all the 92 solutions got printed out, there's millions of solutions here. So if I just took out a return statement and let it run, you'll get screen fulls of solutions to the 20 queens problem because if you did that, there would be a lot of solutions. But especially given that we are not exploiting rotational symmetry and reflections, there's a lot of solutions. But that's one of them. So there you go. Yeah. Question.

AUDIENCE: Is there an expression for the number of solutions you'll get, that depends on n?

SRINI DEVADAS: No. There's no closed form solution. No. The 12 is a concrete number, and 92 and 12 were enumerated. Now you could obviously write a computer program that counted-- I mean, you didn't have to print this out. You could count the number of solutions to the 20 queens problem, and you can find that out. But then if you want to do rotations and reflections, you'd have to start shaving things off. Yeah, back there.

AUDIENCE: Do you have the run time for the recursive one? Would that be n to the n?

SRINI DEVADAS: No. Because you're doing significant pruning. Great question. So some of these things get truncated because you end up doing this, and then this one you don't have to go any further. So it's not n to the n simply because you never have to put down another queen if the first two queens conflict with each other.

So you likely did, actually, been back on Thursday about this time yesterday, we were going over the 4 by 4. And we didn't even put a queen down. We went over and we said, oh, this clearly can't work because there's a queen on this row already. This clearly can't work because there's a queen one a way. So there's no reason to go deeper.

You would go n raised to n if you only did the conflict check after you put n queens down. And if you put all n queens down, closed your eyes put n queens down and then ran a conflict check, and then did that over and over, over and over, it would be n raised to n. But that's not what we're doing.

What we're doing is if you look at the code, we are saying if-- this is an important statement, actually. Thanks for the question. That is pruning your search, and that is making it significantly less than n raised to n. A nice exercise. Remember what I said about opportunity for homework, when I get questions.

A nice exercise is to go to 8 raised to 8 and then go to the eight queens problem and figure out what that number is in terms of the number of times you're actually checking for no conflicts. So count the number of times, not the number of solutions, but the number of times noConflicts is being called, and you'll find that it's significantly less than 8 raised to 8.

Because you're pruning the search. Technically, it's called pruning the search high up in the tree. That's the technical term for it. All right? Good. Any other questions? So we're done with eight queens and nQueens.

Free Downloads

Video


Caption

  • English-US (SRT)