Optimizing the search

Chapter: Magic Square
...Section: Optimizing the search

Were you tempted to change the line .. SIZE = 3; to .. SIZE = 4; and search for a 4*4 magic square? Go ahead and try it. You should notice that it looks like it's going to take an awful long time to find a magic square. Be sure to read the rest of this section before you wait for a result!

A notable feature of backtracking search algorithms becomes apparent when you consider the tree of possibilities that an algorithm is exploring. A useful notion is branching factor. The branching factor for my algorithm for the three by three magic square problem was a fixed 9. At each level of the search tree each and every node begat 9 children. Your algorithm for exercise 1 was a bit smarter: The node at level 0 had nine children; each of those had 8 children, each of which had 7 of their own; etc. Your branching factor started at 9, then was 8 on the level 1, 7 on level 2, etc.

Even in your code, you did not discover that nodes were hopeless until you got to the bottom level. On that bottom level you had 9! nodes, over a quarter of a million. And you will have to search through a lot of them until you get a magic square.

Each node on the bottom level is without descendants. So discovering that a node on the bottom level is hopeless only eliminates that one magic square candidate. Imagine that your code is a little more clever and can detect if a node on the last level but one is hopeless. Each of those nodes has two descendants. So discovering the hopelessness of one node on that level saves checking two candidates. Go up a level and you will find that eliminating a node at that level eliminates 6 candidates. One more level up and you eliminate 24 candidates. If you can detect that a node at the third level is hopeless and cannot lead to a solution, then you are effectively eliminating 6! or 720 candidates in one fell swoop.

The earlier you can discover the futility of continuing to fill in cells of a partial square, the more candidates you can eliminate.

This suggests a modification you can make to the code in PartialSquare.java:

        public Vector successors() {
                Vector retValue = new Vector();
                if (myNums.size() != numCells)  { // generate all the successors
                        PartialSquare aCandidate;
                        for (int i=1; i
 If you can discover that the current partial square is hopeless, then you
 can avoid a whole lot of branching by having successors() return the
 empty vector.

If you analyze the magic square problem you will find that you can predict what the magic constant (the number to which all rows, columns and diagonals) has to be. For example, for the 3*3 magic square, the entries will be the numbers 1 2 3 4 5 6 7 8 9. These sum to 45. For all three rows to sum to the same magic constant, it must be the case that each one sums to 15. In particular, the first row must sum to 15. As soon as you have added three numbers to the vector, you have filled the first row. If this does not sum to 15 then no matter how you continue to fill the partial square it will never be magic. So we can eliminate this partial square and all its descendents with a check carried out when the size of the vector is 3.

In general, for the n*n magic square problem, you can make a check at the level where the size of the vector becomes n. Let's do the math:

  1. The n*n magic square uses the numbers 1 2 ... n2.
  2. These numbers sum to n2(n2 + 1)/2
  3. So the first of n rows row must sum to one nth of this total, or n2(n2+1)/2n. This can be simplified to n(n2 + 1)/2

From the math above we can institute a check at the point when there are SIZE integers in our Vector that can be used to eliminate an awful lot of candidates. If we discover that the first SIZE entries into our vector sum to anything other than SIZE(SIZE2+1)/2 then we can return the empty Vector for the successors of this particular partial square.

I added some code to my Magic Square Solver that prevented the generation of children of any node that violated one of these rules. Actually I noted a few extra similar rules and modified my successor() method so it started with the lines:

                Vector retValue = new Vector();
		if (anyRowWrong()) return retValue;
       		if (anyColWrong()) return retValue;
This pruned the search tree sufficiently that my code can find 4*4 magic squares as in the applet below:


            Your browser is ignoring the <APPLET> tag!      

In about the time it takes you to read this page, this applet will find a solution, perhaps
16 13 3 2
1 4 14 15
6 7 9 12
11 10 8 5
If the applet finishes and you want to see it again, just reload the page. Your code for exercise 1 probably would not work quickly enough for you to find a 4*4 square. Try it: Just set SIZE to 4 instead of 3 and everything should work. Just not very quickly!

It costs some time to check the legality of partial solutions so that you can prune the search tree. For some problems, the time invested in early checks pays off handsomely. From now on, our code should work that way. All of your PartialXxx classes will need to check the legality of a successor. As explained earlier, for my Magic Squares program, I added a three-line legality check:

        if (anyRowWrong()) return retValue;
        if (anyColWrong()) return retValue;
        if (mainDiagWrong()) return retValue;
These lines are executed early in the successors method an ensure that no successors are generated if the current partial solution violates any of the magic properties. Of course, you have to write the code for public boolean anyRowWrong() etc. The time penalty for running those methods will, we hope, be repaid many times over because they may enable us to avoid generating successors, which might themselves generate successors, ...


Exercise 2

You can earn 20 bonus points if you can add early checks to your code from exercise 1 so that it will produce a 4*4 magic square in 200 seconds or faster running on your lab instructor's machine. If you can find a 5*5 magic square, just by adding this kind of early-checking to a basic depth-first search (that's what we've been doing so far) you should bring it and show it to me. I'll award even more bonus points.


rhyspj@gwu.edu