SRINI DEVADAS: We're going to do something quite different in terms of a puzzle. And while it's a recursive puzzle, it's going to play out to be a recursive solution. It looks quite different from the N-queens puzzle. And so this is going to be a tiling puzzle.
And so I set up the puzzle, and then we're going to take a little segue off into a canonical recursive algorithm called merge sort because I think it's good for you to see that algorithm before you dive into solving this particular puzzle because it kind of shows you the way. All right, and but I'll set up the puzzle. I guess I didn't have to erase this square.
It's a courtyard that has a bunch-- of tiles. You're supposed to tile it, and it's all square tiles. And I'll just make it eight by eight like a chessboard. And in general, we are going to say that this is a 2 raised to n by 2 raised to n courtyard. And I want to tile this courtyard.
So this is a three tile-- some people call it a tromino. You can think of it's really an L-shaped tile, and it can be oriented in any different way. It could be an inverted L, et cetera.
And you're supposed to tile this courtyard using L-shaped tiles or trominos. And they're called trominos because there's three tiles in there-- three squares in there. And so that's your job. I guess kind of a weird job, but this is a puzzle.
And so you're out there with 2 raised to n by 2 raised to n. And all you have are these L-shaped tiles, and you're not allowed to break them. If you break them, then that's not considered a valid tiling. That's bad, and that's your job.
Oh, and obviously, it needs to look like this, and these look like the floor. Maybe you'd like to see the markings because these are tiles, after all. But you can't have something that is uneven. You can't have a tile on top of another tile. That's a definite no-no.
And you can't not have a tile in any part of the courtyard. So I think that was kind of obvious, but it's worth explicitly stating just so you don't get confused here. So that's the question that we're going to try and answer.
But as I said, I want to talk about something a little bit different for about 5 or 10 minutes and actually show you some code because I want to introduce this notion that's a little bit different from the N-queens recursive paradigm. It is recursive, but it's called recursive divide and conquer. And usually, when we use the term recursive divide and conquer, you might think that it's applicable, even to the N-queens recursive code. But from a technical term standpoint when we say recursive divide and conquer, we're really saying that we're taking the problem, and we are dividing it up into smaller problems.
So you take the problem, and you divide it into two problems that are half the size or three problems that are a third of the size et cetera. Which is not what happened in the case of the N-queens problem.
And in the case of the N-queens problem, you took a column, and you actually did iterations on the column. So it potentially exploded on you. And then you had a problem that is slightly smaller than the N-queens. It was effectively n minus 1 queens.
And so it's not n over 2 or n over 2 or 3-- n over 3 in the case of a three-way recursive divide and conquer. So there's a qualitative difference. And there's also a quantitative difference in the sense that these kinds of algorithms are going to be efficient in that they are going to run very quickly in polynomial time, perhaps quadratic time. We can do some analysis for both of the merge sort that we're going to do now and for the tiling puzzle, once we get to that. But that's something to keep in mind.
So merge sort is the canonical recursive divide and conquer approach. And I'll put up code for it, but I'm not going to show it to you. You can look at it in the website.
But I'm going to describe the algorithm to you using a trivial example. And so I want to sort these numbers in ascending order.
So I have these four numbers that I want to sort in ascending order. And obviously, I could do this many different ways. We looked at selection sort. But this is a large array, and I want to do something more efficient than that quadratic algorithm and n squared algorithm. And merge sort, which is recursive divide and conquer, fits the bill.
And what merge sort says is I'm going to split the array. I'm just going to take this array, and I'm going to split it up into two sub-arrays. And I could do this recursively, but let's just talk about the first step.
So I'm going to go ahead and split this array up, and I have 76 and 32. And I'm going to call merge sort on this smaller array. And the first thing that merge sort does is split the array up into something smaller, and you end up getting that.
So this is the first thing, and that's the first split. And then you end up splitting 48 and 32, and I'm sorry-- or 12, 76, 32, 48, 12. Yep, I got that right. And so far, you've done nothing. I mean, you've just sort of broken things up and turned this into four one arrays or one list or what have you.
But then the fun starts where after you do the split, after you return, and so 76 as singleton is trivially sorted so there's no work to do. So there's no more splits that you could do. There's nothing that says you can-- I mean, 76 is a monolithic element in their array, so you can't do anything with it. And so you just leave it be. And the same thing with 32, and so on and so forth.
But after your two calls return, you have to merge the two together. And the way you merge the two together-- the invariant here is that when you merge the two together, they have to be sorted in the order that you want the overall array to be sorted, which is ascending order. So in this case, you have down at the bottom, it's trivial code because you have two singleton lists. And you just decide which order they go into.
OK, so the merge, in this case, would simply be produced, you would produce 32 and 76. OK, and in this case, the merger produced 12 and 48.
So at this level, you're going to return 32 and 76 back. You're going to return 32 and 76 back. And here you're going to return 12 and 48 back. Now it gets a little more interesting. Now it gets a little more interesting because now you have 32 and 76 and 12 and 48, and those are both sorted in ascending order, and you need to merge them together.
And so if you had a 128 element list, you would go, go, go all the way down, and then you would do some merging. Come up all the way and then the most time-consuming step really would be taking two 64 element lists that the left one is in ascending order, and the right one is in descending order. And then you need to merge them together so the result is in ascending order. That makes sense?
And so in this case, the way we're going to do this is if you have-- and I can do this for this example or a different example. But I'll just stick with 32, 76, and this could be larger. This is 12 and 48. So I need to merge these two to produce in ascending order.
And we use a straightforward algorithm that's called the two-finger algorithm, which says I'm going to start with my fingers pointing to the beginning of the two arrays, and I'm going to choose the element that's smaller. And I'm going to write it into my output array. So I'm going to compare 32 and 12. 12 is smaller, so I'm going to write 12 down. And because I picked 12 from this array, and I didn't pick 32, I'm going to leave my top finger where it is, and I'm going to move my bottom finger-- my right-hand finger-- over to the next element.
And then I'm going to compare 32 with 48 because this finger didn't move. And I'm going to figure out which one is smaller, and it's 32. And I'm going to put that over here. And then I'm going to move this finger because I took away 32.
And then at this 48 is smaller, and then I'm off this way, assuming the array ended here. But if it didn't, I keep going.
And then the last thing I do is put 76 down. So that is a recursive divide and conquer algorithm. It's the simplest algorithm I can think of, and that is still interesting. I'll show you code for it. I'm not going to explain it.
It is worth looking at the overall code. And then the merge, which is this two-finger algorithm, is actually the most amount of code. But the split itself, as you can see, is you see as with our recursion, we say we want to have a base case. And so when the length of the list becomes 2, I'm just going to do that little flip. This is a sorting algorithm that only works for a list of length 2.
But the beauty of this is that I can keep going down to get smaller and smaller lists, such that the sorting algorithm actually works. And this is it. This is a fine sorting algorithm for a base case. And we do have a base case here.
We also have the other property that I'm actually calling. I'm making two recursive calls with significantly smaller problems. In this case, I'm using list splicing to get the first part of the list and the second part of the list, and I'm just calling merge sort on those two lists.
And then finally, the workhorse in merge sort is the merge step-- this two finger thing that I just described to you. That is this merge thing. And this merge thing, as you can see-- there's not a lot of code.
But it's doing this movement and the comparisons. And that's essentially what you have there. So given that context-- right given that context, let's go back to our tiling puzzle.
And let's talk about this tiling puzzle and how we could we could solve this tiling puzzle. So you're going to break this courtyard up clearly because that's really what I've been trying to tell you in terms of recursive divide and conquer.
First question-- look at this 2 raised to n, and 2 raised to n, and look at this three tile. I am going to ask you the question. Is there a solution that is a perfect tiling with no overlapping tiles and no squares with holes in them? Is someone?
Fadi had his hand up first. Go for it.
AUDIENCE: Well, if we look at the case of n is equal to 1, you have a two by two courtyard, then it's impossible, you can see that trivially. But in a general case, then we will have 2 to the 2n squares. And every time we're adding 3, 3, 3, the number of tiles is a multiple of 3.
SRINI DEVADAS: Correct. So 2 to the 2n-- and Josh probably had the same answer-- 2 to the 2n is not a multiple of 3. The only thing that divides it is 2.
So OK so we're going to change the problem. We're going to say that there's a statue of your favorite person that is going to be out here. And so you don't need to tile that.
So now you have 2 raised to 2n minus 1 let me write that out properly. 2 raised to 2n minus 1 squares. Now can you do this for all n? Clearly, it works for n equals 1.
And then for n equals 2, you have what? 2 raised to 4, which is 16 minus 115. That seems to work.
And then you get up to, I think 80-- what is it? 2 raised to 6 would be 64. And so that will be 63. That works too.
In general, does it work? It does. So it turns out if you think about it, you can write this out as 2 raised to n minus 1 times 2 raised to n plus 1. And in the middle there, pretend that there's 2 raised to n. So you have three consecutive numbers.
And when you multiply three consecutive numbers, they're consecutive. Doesn't matter what the numbers are. The product is always going to be a multiple of 3 because one of those things is going to be a multiple of 3. And you know that 2 raised to n is not a multiple of 3. This is not a product of three consecutive numbers.
But if it were, and you pretended that 2 raised to n was in here, either this one is a multiple of 3, or that one is a multiple of 3. So this is good. So we know that at least from a standpoint of volume-- our surface area, if you will-- that this is going to work. Whereas the original problem didn't work from a surface area standpoint.
So it comes down to how am I going to solve this problem using recursive divide and conquer? And so I can break this up. And you want to have a similar feel to the merge sort, where when you go down to the base case of merge sort, you had this trivial problem to solve. That's the beauty of divide and conquer. It becomes obvious when you have such a small problem like one queen.
That's not technically not divide and conquer, but you do get to the smaller part. But clearly, in this case, this is a classic divide and conquer algorithm merge sort. And you can see that the first two or three lines of code are the trivial case of two elements that needed to be sorted. So let's go back and forget that.
Let's look at this picture and think about how you'd break this up. How would I bring this up?
So think two-dimensionally. And merge sort was a single dimension. OK, think two dimensional. This is two dimensional.
This is a two-dimensional problem. It's in that sense, closer to N-queens. But merge sort was simply a single dimension. All right, I see Kevin had-- Fadi had his hand up.
Kevin has a hand up. Someone else? All right, go ahead, Kevin.
AUDIENCE: Do you keep cutting the square into quarters?
SRINI DEVADAS: Quarters-- good. OK, yeah. So all right, so let's do that. Let's do that. You're on the right track.
And by the way, this particular thing could be over there. And I'll just do this just to make sure. Your algorithm needs to work, regardless of where that statue is.
So maybe the statue is over here. Maybe the statue is over there. There's only one statue that is covering one square, but it's not necessarily in near the center. It could even be anywhere. You need a general-purpose scheme that would work, regardless of where the statue is as long as it only occupies one square.
That's the only constraint we have. So you're on the right track, Kevin. Someone else or, Fadi, you want to add to that?
AUDIENCE: So in something that's similar to the merge sort so you divide it into quarters and then you reach the base case and then we tile that obviously with an L-shaped tile.
SRINI DEVADAS: Right, that's exactly right. So yeah. So that there's one thing that's missing in what both you and Kevin have put up. The thing with merge sort was I had exactly the same problem to solve.
It was exactly the same problem. It was just smaller. I needed to sort the smaller array in ascending order. And I had two of them, and it was exactly the same problem. But when I split this up into four quarters like you guys are suggesting, do I have exactly the same four problems to solve?
In fact, can I even solve this the 16 problem with L-shaped tiles? I can't. So at some level, I have three problems that I can't solve. And then a smaller problem that I don't quite know how to solve-- other than maybe splitting it up even more, which is kind of interesting, which is good. But then the next time, if I go ahead and split that, then wonderful. I will find one problem that's two by two that I can solve, but that doesn't solve my puzzle.
So I have to do one more thing in order to make this look like merge sort or any recursive divide and conquer. One of the paradigms of recursive divide and conquer is you're not creating new types of problems. You're creating the same type of problem, except smaller. And therefore, eventually easier.
So I have to do one more thing. And this is an aha moment here that I'm hoping-- I don't want to take an answer yet because I want you to think about it and perhaps arrive at it. But it's one of those things where you're going to go "oh!" if you don't know the answer. But it is worth spending another minute-- another minute for each of you thinking about it.
How am I going to set it up so these quarters-- so all of this is good? I'm going to split it up into quarters, but I want all four problems to look exactly the same in the sense that they look-- this obviously looks different from these other ones. Well, there's a tile missing here, but there is no tile missing over in the other three. So how do I make all problems look the same? I know you know, but we need more. I'll give you 30 more seconds, and I'll give you one more hint.
All! right, the hint is place one tile-- place one L-shaped tile. All right. Yeah, go for it.
AUDIENCE: Yeah, you know that with those last four quarters-- you know that that L-shape has to go around?
SRINI DEVADAS: Yes. Yeah, that's right. So you set it up so you tile.
The reason I picked this L-shape this way is because this was the statue. So I wanted to remove a tile from each of the other 3/4. I'm sorry, remove a square. A tile is L-shaped.
And so this was a missing square, which was part of the specification of the problem. It has nothing to do with the tile. It's just a missing square because there's a statue on it. But what I could do is if I oriented the tile in this way, now I have this being a smaller problem, which is one-quarter of the size of the original problem with one square missing. This one is one square missing.
This one is one square missing. This one is one square missing. Now that makes all of the problems look the same. Now, at first, they're not exactly the same in the sense that the missing square is in a different position.
But there's some generality that we need in our algorithm that says that eventually, if I keep breaking this up in this fashion, I just need to orient my L-shaped tile in the appropriate way. So there's only four different ways that an L-shaped tile is going to be oriented. It could be oriented like this.
And I won't draw all of them. It could be oriented like that. And it could also be that's the same. So what else is there?
Oh, it could be flipped this way. Yeah, like that and the fourth one. So that's what I did here. This one is what I put up there.
And then as I break this up, let's just say that now I'm going to make four recursive calls. I'm going to place one tile-- one L-shaped tile-- and I'm going to mark off of these things. And I'm going to make four recursive calls to basically myself, except that the arguments are different. It's the same code, obviously.
So when I go up here, I go like that, and hopefully, this will become clearer if you're going to do a tiling here. I'm going to go focus on the top left, and I'm going to do this. What am I going to do with respect to this at this particular quarter of the courtyard?
How am I going to orient the L-shaped tile for this one? Point with your fingers. Exactly right. So this is the same as this. I'm going to go ahead and do that.
So now I have this problem. So I'm going to mark that with a different color. So I'm focused on this problem, and I've done that break. And now I'm good because I have four two by two courtyards, and each of these four has one square missing in each of these four little two by two courtyards. So clearly, I can solve that.
And then once I solve that, I move on to this one, and I move onto this one-- whatever order you want. That's it. And so the code for this is-- from a control flow standpoint, from a standpoint of the overall algorithm, it's extremely straightforward. It's almost exactly like merge sort, except that you have four-way recursion because it's two dimensional, and you end up getting quarters.
And the only thing that's annoying about this code to write and also to understand, which you guys will-- I'll put this up, and I'll show you what to look for-- is how you represent this as such that you get all the values right. So you get an arbitrary square, to begin with, that is missing. And then you have to go to the middle. That's clear.
You're going to go to the middle of this courtyard, and you're going to split it. And you're going to decide which way this L-shaped tile is oriented, and then you got to do the splitting again and so it's just bookkeeping. That's really what this is all about. It's not algorithmically interesting. It's just bookkeeping in terms of the orientation of these L-shaped tiles.
So if you look at the code, and if you ignore a bunch of these indices of these different variables-- let me show you. Well, first, I'll show you what happens when you run the code. I'm running it for two different kinds of courtyards. And I've used A through, I guess, something like S, in this case, to signify a tile.
So you see AAA here. AAA is an L-shaped tile. BBB is an L-shaped tile. FFF is an L-shaped tile, et cetera.
The original specification of the problem-- this is just simply an eight by eight problem-- had this tile that you see here. That's a blank that I've covered up that was missing. I'm sorry, this square missing.
And then I broke this up into two or four 4 by 4's. And then I went all the way up and then the first tile that I placed, I pretended to place this U tile over here. I could've placed it and then called it the A tile.
But this is essentially what you think of the one in the center exactly the way this is oriented. That's your U tile over here. And then you go all the way down. And explicitly, it's just the A tile that displays first, and then the B tile, and so on and so forth.
And if you take a look at the code, what's the best place to start? So this is the base case-- same thing as we've talked about before. You always need to have a base case. The base cases is a two by two. And this code places one L-shaped tile on a two by two yard.
This is going to make four recursive calls, as you can see from range four, and there's two different kinds of recursive calls in the sense that they're both calling recursive tile, but the arguments of the recursive style are different for this recursive call because it already had the missing square in it. And these three recursive calls that are going to have the missing square in one of their corners. So that's why you see the if and the else here.
And if the original for the recursive call that already had the missing square because it had the statue on it, that's the if statement. And the other ones are the ones that are in the corners. So the first things it would be a corner for this one. This is the corner of that, one and that's the corner of that one. So that's pretty much all I had.
This code-- you need to gaze at it for a few minutes to figure out that I got all the arguments correct. But from a standpoint of, as I said, the control flow and the algorithm, it is hopefully completely clear to you at this point. If not, ask me questions. Come to office hours. Have a good long weekend.