![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
the first k columns contain Queens no two of which attack each otherwhich we can trivially initialise with k = 0 and then try to extend by increasing k. The first example shows that this could work, the second shows that it need not. So our algorithm has to be more tentative. We must be prepared for a given configuration not to be extensible. In that case, we must backtrack to a previous value of k and try to extend that in a different way; there may be no different way, in which case we must backtrack again, and perhaps yet again.
Writing this algorithm down is, however, surprisingly hard unless you are happy with recursion. The problem is that you need a sophisticated structure in the program to control whether we are looking to extend the current situation forwards or to backtrack, and after backtracking whether we are now going forwards afresh or re-backing, etc. This is exactly what recursion does best. Let's see:
procedure place (k) # k-1 Queens have already been placed, with the jth Queen in # column j and row p[j], for 1 <= j < k. We try to # place the kth Queen. if k = 9 then for i = 1 to 8 do print p[i] endfor, print newline else for i = 1 to 8 do p[k] = i if "this configuration is OK" then place (k+1) endif endfor endifand all we have to do to turn this into a complete program for this problem is to expand the test this configuration is OK and then to initiate the whole process by calling place (1). [As given, this code will print all 92 different solutions; if you want only the first, then you must exit after printing it.] The test is a "small matter of programming"; you must test to see whether the new Queen in column k and row i is in the same row or a same diagonal as any of the previous Queens. This is not particularly trivial, but nor is it difficult, and it's not really relevant to our present purpose.
Note that the backtracking has virtually disappeared from the program; it is implemented automatically by the recursion. As each instantiation of place is completed, we automatically backtrack to the place from which it was called, part-way through the internal loop.
This problem is really just a puzzle. It obviously generalises to N Queens on an N×N board with almost no changes. As N gets large, the time taken to complete the search increases dramatically; we will be looking at a significant fraction of the N^N different ways of assigning values between 1 and N to the elements of p. But for N no more than about 12, it works very well.
There are zillions of other problems which are also, it seems, best solved by a backtracking algorithm. We look at a few representative cases.
Suppose, for example, we have containers of size 100, and our first few objects have sizes 50, 40, 30 and 20. We put the 50 into the first container, and now want to place the 40. Do we [`greedily'] place it in the first space that is big enough? If so, we have a space of 10 left over, that may be completely wasted if the objects we haven't yet seen are all bigger than 10; for example, if the remaining objects are 44 and 11, we will need a third container where two should have been sufficient. On the other hand, if we place the 40 in the second container, this enables us to fill the first container exactly with the 30 and 20, but leaves the second container perhaps over-full. Suppose, for example that the next two objects are of size 65; then 50+30+20;40;65;65 needs four containers where 50+40;30+65;20+65 needs only three.
A more sophisticated version of this example can be used to show that none of a wide range of plausible ideas always gives the best solution. It seems that the best solution can be found only by trial and error; or, in other words, by trying a placement and being prepared to backtrack it to try a different placement.
Computer operating systems have to solve a problem very like this one when they try to assign memory to a number of concurrent processes, while other processes are `swapped out' to disc to await their chance to run. Another practical application is the cloth-cutting problem -- you have a design for an item [say] of clothing, and you want to cut up the minimum length of cloth to make up your design [or to stamp out components from a minimum area of sheet metal, or whatever]. Yet another similar problem is the fair division problem; you have a number of children at a party and a pile of presents [enough for several per child], and you have to divide the presents so that the gap between `richest' and `poorest' is as small as possible. [Practical parents resolve this by having enough presents to give every child exactly the same.] A similar problem is dividing a boat-race crew into port and starboard sides with roughly equal pull.
Two other important problems are much harder. The Hamiltonian circuit problem asks whether there is a route which visits every village exactly once, again finishing back where you started. The Travelling Salesman problem asks for the shortest route, again finishing back where you started, which visits every village at least once. We shall see later that these two problems are very closely related. Current theory suggests that we will again have to resort to trial and error. In other words, for Hamilton's problem, we will sometimes have to guess which village to visit next, and our guess will sometimes be wrong, so that we find ourselves in a dead-end, and have to retrace our steps and try a different guess.
to analyse the current position: if position is terminal then return result of game else construct list of legal moves for each move on this list do play this move analyse the resulting position unplay this move endfor endifIf your program is exactly this, then you will wait an awfully long time for it to find you a good move in any game more complicated than noughts-and-crosses; so a great deal of optimisation is needed for a practical program! [This optimisation is discussed in the Game Theory module.] As before, the backtracking is implicit in the recursion [in this case, the instruction to analyse inside the for-loop].
Another point to note, which we shall return to later, is that many of these problems can be solved by lucky guesses. If you place Queens at random on the chessboard, then you may strike lucky, and hit a placement that satisfies the conditions of the puzzle. If so, then you can easily test that your solution is good. Similarly, if you simply wander at random around some villages, then you may accidentally complete a Hamiltonian tour. If so, then you can easily show that you have done so. In these and other cases, finding a solution seems to be hard, but verifying the correctness of a proposed solution is easy.