Note in particular the default definition (in search/domains/problems.lisp) of the successors method in terms of an actions method, which returns all possible actions in a given state, and a result method, which computes the new state resulting from a given action in a given state. (Examples of their use appear in search/domains/nqueens-problem.lisp and search/domains/sliding-block-puzzle.lisp.) For local search algorithms, the random-successor method, defined in terms of a random-action method, is also useful. To construct and solve a problem, once the methods for a particular problem type are defined, one first makes a problem instance. Here, we make an 8-puzzle (3x3) and look at its initial state:
> (problem-initial-state (setq p (make-sliding-block-puzzle :n 3))) 6 3 5 7 2 1 4 8 .Then, one invokes one of the search algorithms to solve it. Here, we apply A* graph search. This returns the solution sequence:
> (pprint (setq soln (a*-graph-search p))) (LEFT LEFT UP UP RIGHT DOWN RIGHT UP LEFT DOWN DOWN LEFT UP UP)Just to be sure, we can look at the outcome of this sequence:
> (sequence-result p soln (problem-initial-state p)) . 1 2 3 4 5 6 7 8There are many more examples of how to solve problems in search/test-search.lisp.
cross curve straight twocurve blank lsplit rsplit * | * | * | * | * * | * | * * | * \ * | * \ * * | * | * *----|----* `--* | *--. `--* *--. | * | ,--* * | * * | * \ * * \| * |/ * * | * * | * | * * | * | *In addition, there is a barrier-piece subtype that surrounds the track (see below) and an unknown-piece subtype for parts of the track that are not yet defined.
Any track piece p is an instance of one of these subtypes. As such, it has an orientation (track-piece-orientation p) which is an integer. Each orientation gives the piece a different appearance; the appearances shown above are for orientation 0. In general, the possible orientations are 0, 1, 2, 3, each rotated 90 degrees clockwise from the previous. Because of symmetries, some pieces have fewer distinct orientations. For example, the blank and cross pieces just have orientation 0 and the straight, curve, and two-curve pieces have orientations 0 and 1. For any piece, the number of distinct orientations is given by (track-piece-d p). The track subtype definitions include appearances for each distinct orientation, but you need not worry about this because print-track takes care of them automatically (see below).
The appearance of a track piece is purely cosmetic (i.e., it's just for the purposes of printing out tracks so you can see them). What really matters is the set of connections, i.e., the ways in which the tracks connect the edges of the piece. The edges of any piece are called *top*, *right*, *bottom*, and *left*, which are defined as global parameters with values 0, 1, 2, 3 respectively. (Hence, you can enumerate over edges by looping from *top* to *left*.) Which edges are connected to which depends on the orientation of the piece. Consider, for example, the lsplit-piece, which is shown above in orientation 0:
The connections for orientation 0 are recorded in the track-piece-connections field of each subtype. In general, you won't want to mess with this field directly. Instead, you will use the following access functions:
The print-track function prints out a track as plain text characters. The display-track function displays a track in a separate window in a much nicer format. (See the track-display.lisp file for further instructions.)
IMPORTANT: As with all the arrays in AIMA, we use the Cartesian convention: (1,1) is the bottom left corner, (M,1) is the bottom right corner, (1,N) is the top left corner, (M,N) is the top right corner. This convention also applies when calculating, for example, the square adjacent to the top edge of another square.
IMPORTANT: As a general rule it is best to have each square of a track contain a distinct track piece instance. Putting the same instance in more than one square causes obvious problems when the piece orientation is modified!
The basic function for making a track is (make-full-track width height pieces), which makes a track with the given dimensions, adding barriers around the outside, and fills the squares from the list of pieces, in the given order. It returns the track (and a list of left-over pieces if any).
make-empty-track calls make-full-track with a list of unknown pieces:
> (print-track (make-empty-track 4 4)) ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- ----- . . . . .. . . . .. . . . .. . . . . ----- . . . . .. . . . .. . . . .. . . . . ----- | | . . . . .. . . . .. . . . .. . . . . | | ----- . . . . .. . . . .. . . . .. . . . . ----- : : : : :: : : : :: : : : :: : : : : . . . . .. . . . .. . . . .. . . . . ----- . . . . .. . . . .. . . . .. . . . . ----- | | . . . . .. . . . .. . . . .. . . . . | | ----- . . . . .. . . . .. . . . .. . . . . ----- : : : : :: : : : :: : : : :: : : : : . . . . .. . . . .. . . . .. . . . . ----- . . . . .. . . . .. . . . .. . . . . ----- | | . . . . .. . . . .. . . . .. . . . . | | ----- . . . . .. . . . .. . . . .. . . . . ----- : : : : :: : : : :: : : : :: : : : : . . . . .. . . . .. . . . .. . . . . ----- . . . . .. . . . .. . . . .. . . . . ----- | | . . . . .. . . . .. . . . .. . . . . | | ----- . . . . .. . . . .. . . . .. . . . . ----- : : : : :: : : : :: : : : :: : : : : ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- -----
make-random-track-unlimited calls make-full-track with a list of pieces generated randomly according to the *piece-probs* distribution (as if there were an unlimited supply of each piece type):
> (print-track (setq random44 (make-random-track-unlimited 4 4))) ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- ----- | | | ----- | | / ----- | | | ,----.------ | --' ,-- | | ----- |/ \ | / ----- | | | | | | | ----- / | \ ----- | | --. --' ,------|------. `-- | | ----- \ / | \ ----- | | | | | | ----- /| \ ----- | | --. --.--------' | `-- | | ----- \ \ | ----- | | | | | | ----- \ / \ ----- | | ------`----'------------,--------`-- | | ----- / ----- | ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- -----
One useful function for accessing track squares is (adjacent-square x y edge), which returns the square adjacent to the given edge of square (x,y) in the track. For example,
> (adjacent-square 1 1 *right*) (2 1)The function (adjacent-piece track x y edge) extracts the corresponding piece in the given track. Thus, if random44 is bound to the track above:
> (adjacent-piece random44 1 1 *right*) #.(make 'LSPLIT-PIECE :orientation 1)Notice the somewhat odd way we print out pieces: this makes it possible to print out a track to a file and read it back in again:
> (save-track random44 "~cs188/a2/random44.track") NIL > (setq random44copy (read-track "~cs188/a2/random44.track")) #2A((#.(make 'BARRIER-PIECE :orientation 0) #.(make 'BARRIER-PIECE :orientation 0) etc., etc.
> (print-track (setq weak44 (read-track "~cs188/a2/weak44.track"))) ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- ----- ----- ----- | | ,----. ,----. | | ----- / \ / \ ----- | | | | | | | | ----- \ /| | | ----- | | `----' | | | | | ----- | | | ----- | | | | | | ----- |\ / /| ----- | | | `----' ,----' | | | ----- | / | ----- | | | | | | ----- \ / / ----- | | `----'--------' | | ----- ----- ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- -----Later we will see a more stringent definition of strongly connected component.
Now go through the following steps:
1. (5) First, to help you become familiar with tracks and pieces, write a function (count-loose-ends track), which should return the number of loose ends in the track. For random44 you should get 20; for weak44 you should get 0.
2. (2) If a track has no loose ends, what does that imply about the numbers of lsplit and rsplit pieces? Explain.
3. (20) Now, write a function (count-wccs track), which should return the number of WCCs in the track. For random44 you should get 6; for weak44 you should get 1. [Hint: your first instinct may be to view WCCs in terms of collections of squares; but this won't work because of the twocurve and cross pieces, each of which can be part of two distinct WCCs. Instead, consider a WCC as a set of edges. For an MxN track, there are MxNx4 edges. To find a WCC, start from any non-empty edge not already noted as belonging to a WCC, note it as belonging to a new WCC, then recursively explore and note all edges connected to it (including the adjacent edge of the neighboring square). Start with a small track for debugging purposes and print out useful debugging information as the exploration proceeds.]
4. (25) Now, formulate and solve the local search problem type, unlimited-track-local-problem, where the goal is to construct a track with as few loose ends and WCCs as possible. In such problems, we start with a randomly filled track and make changes to improve it, with no limitations on the pieces that can be used. You will need to define the actions or random-action method and the result, h-cost and goal-test methods. Then construct a suitable problem instance with a random initial state and apply a suitable algorithm from local-search.lisp. (Note: for most of the algorithms, you will need to play with the parameters, such as number of restarts, temperature schedule, etc., to get good results.) Measure the run time as a function of track size, up to the largest track you can construct in a reasonable amount of time (say, a minute or two). While experimenting with the algorithms, you may find it helpful to insert code that displays the current track at each iteration.
5. (10 extra credit, optional) Formulate and solve the fixed-track-local-problem, where the track must be constructed from a fixed set of pieces that exactly fills the track. (Hence, actions can rotate or swap pieces, but cannot introduce new ones.) The fixed set can be specified as an association list, e.g., for a 4x4 track one might specify
((blank-piece . 1) (cross-piece . 1) (straight-piece . 3) (curve-piece . 6) (twocurve-piece . 1) (lsplit-piece . 2) (rsplit-piece . 2))Experiment with various different track sizes and sets of pieces. Is this problem harder or easier than the unlimited problem? Why? Which kinds of pieces seem to cause difficulties?
6. (8) Explain (in words) what variables, domains, and constraints you would need to formulate the track design problem as a CSP. Do this for (a) the unlimited-track-local-problem, where there are no constraints on the pieces used, and (b) the fixed-track-local-problem, where the set of pieces is fixed.
7. (10 extra credit, optional) The notion of WCC doesn't quite capture the full requirements of running trains on tracks. In the weak44 track given earlier, a train running north at (2,2) runs into the loop in the upper left corner and never escapes unless it reverses. A simple circular track is even worse: a train running forwards in the clockwise direction can never run forwards in the anticlockwise direction unless it is picked up bodily and placed facing the other way. According to Gordon (age 7), a track is only good if the train can traverse every part of it in both directions without stopping or reversing. Here is an example of such a strongly connected track:
> (print-track (setq strong44 (read-track "~cs188/a2/strong44.track"))) ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- ----- ----- ----- | | ,----.------------,----. | | ----- / \ / \ ----- | | | | | | | | ----- |\ | | | ----- | | | `----. | | | | | ----- | \| | | ----- | | | | | | | | ----- | |\ / /| ----- | | | | `----' ,----' | | | ----- | | / | ----- | | | | | | | | ----- \ \ / / ----- | | `--------`----'--------' | | ----- ----- ----- ----- ----- ----- ----- ----- | | | | | | | | | | | | ----- ----- ----- ----- ----- -----Write a predicate (strongly-connected? track), which should return t iff the track is strongly connected, and use it with any of your track design methods to make some large, strongly connected tracks.
8. (Not part of the assignment, but 199 credit is available; due last day of classes. You might be able to make a startup company out of this.) Tracks made from square tiles are hardly the real thing. Look into the problems involved in designing real Brio and/or Scalextric tracks. Propose a solution approach, implement and test the approach, and do a short write-up.
To solve a CSP, first make one and then apply backtracking to it:
> (setq p (make-nqueens-csp :n 8)) #S(NQUEENS-CSP :VARIABLES (1 2 3 4 5 6 7 8) :DOMAINS ((1 1 2 3 4 5 6 7 8) (2 1 2 3 4 5 6 7 8) (3 1 2 3 4 5 6 7 8) (4 1 2 3 4 5 6 7 8) (5 1 2 3 4 5 6 7 8) (6 1 2 3 4 5 6 7 8) (7 1 2 3 4 5 6 7 8) (8 1 2 3 4 5 6 7 8)) :CONSTRAINTS (#S(NQUEENS-CONSTRAINT :VARIABLES #) #S(NQUEENS-CONSTRAINT :VARIABLES #) #S(NQUEENS-CONSTRAINT :VARIABLES #) #S(...) ...) :N 8) > (print-nqueens-assignment (backtracking-search p)) . . Q . . . . . . . . . . Q . . . . . Q . . . . . Q . . . . . . . . . . . . . Q . . . . Q . . . . . . . . . Q . Q . . . . . . . ((8 . 4) (7 . 2) (6 . 7) (5 . 3) (4 . 6) (3 . 8) (2 . 5) (1 . 1))You can get the backtracking algorithm to use variable and value ordering heuristics by supplying keyword arguments:
> (backtracking-search p :select-unassigned-variable #'minimum-remaining-values) ((8 . 4) (6 . 7) (5 . 3) (7 . 2) (4 . 6) (3 . 8) (2 . 5) (1 . 1))
Sudoku is a fairly old puzzle that is now a worldwide phenomenon. You can type "sudoku" into Google, or go to www.sudoku.com for the basics (see "How to Solve") and the Wikipedia article to get more information than you could possibly imagine. Key facts about standard Sudoku puzzles:
We supply some useful code and examples:
Now go through the following steps:
9. (10) Write code defining Sudoku as a CSP, such that the call (make-sudoku-csp) returns a Sudoku CSP for the case where no initial digits are supplied. Your CSP should have (9+9+9)*(9*8/2) = 972 binary constraints. Notice that the initial-state constructor initial-csp-state lets you supply an initial assignment. This is by far the easiest way to handle the initial digits that define any particular Sudoku puzzle. For example, you will be able to do the following:
> (setq p (make-sudoku-csp)) [no peeking!!] > (print-sudoku (backtracking-search p (initial-csp-state p *easy-sudoku-01*))) +=======================+ | 6 7 1 | 4 5 3 | 8 9 2 | | 4 5 8 | 7 9 2 | 1 6 3 | | 2 9 3 | 6 1 8 | 5 4 7 | | ===================== | | 9 3 5 | 2 8 4 | 7 1 6 | | 8 4 6 | 1 7 5 | 2 3 9 | | 1 2 7 | 3 6 9 | 4 5 8 | | ===================== | | 3 1 9 | 5 2 7 | 6 8 4 | | 7 6 4 | 8 3 1 | 9 2 5 | | 5 8 2 | 9 4 6 | 3 7 1 | +=======================+ (((9 9) . 2) ((9 8) . 3) ((9 6) . 6) ((9 5) . 9) ((9 2) . 5) ((9 1) . 1) ((8 9) . 9) ((8 7) . 4) ((8 4) . 5) ((8 3) . 8) ...)
You will need to decide what the variables should be, what their domains are, and what the constraints are. The code in nqueens-csp.lisp is a good guide for what's needed. Apply plain backtracking to your CSP and show that it works.
10. (5) Now instrument backtracking-search so that it counts the total number of guesses made. Whenever the backtracking algorithm prepares to loop through a list of k>0 possible values, we will say that k-1 guesses are made. (This way, the algorithm is still charged for guessing even if it is lucky and its first value choice succeeds.) Show the numbers of guesses made for each of the 40 instances in sudoku-puzzles.lisp (use *all-sudokus*), using plain backtracking and backtracking with MRV.
11. (25) We would like our solver never to resort to guessing. For this to happen, we will need better inference methods. In this context, an inference method is a function that examines the current state of the CSP and deduces additional facts about the remaining unassigned variables -- either ruling out one or more values for a variable or asserting a particular value. The method can then modify the current state accordingly. Notice that every time a method modifies the current state, other methods (or indeed the method itself) may become applicable again. A waterfall is a set of methods that is applied repeatedly, until all the methods fail to do anything useful.
Modify backtracking-search so that it applies a waterfall to modify the current state right after the assignment-complete? check. (Be sure to have the program keep track of what the waterfall does so that it can be undone!)
Initially, your waterfall list contains no inference methods, so nothing will change. Now, find some Sudoku inference methods on the web (ones written in English, of course, not programs!). For each, describe how it works with an example; explain whether it is already covered by the standard operation of keeping track of legal values as in delete-inconsistent-values and using MRV; and, if it is not covered, implement it and add it to your waterfall. After each addition, check the number of guesses for each of the 40 puzzles. Ideally, you will be able to get the numbers to zero for all puzzles. If not, it may be possible to define your own inference methods by examining the current state at each point where a guess is made, to see what a smart human would do at that point (if one happens to be available).
12. (10 extra credit, optional) Design, implement, and test a Sudoku puzzle generator. For extra fun, ho ho, pass the puzzles to your friends to see if their solvers can solve them without guessing.