A sliding tile puzzle is a x × y grid, which contains (xy - 1) tiles numbered 1 to (xy - 1), and one empty space. Tiles adjacent to the empty space (that is, tiles immediately above, below, to the left, or to the right of the empty space) can be slid into the space. The object is to get the tiles into a particular orientation. For example, consider the followiong goal position for the 4 × 4 puzzle:
1 | 2 | 3 | |
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
If the original state of the puzzle was:
1 | 5 | 2 | 3 |
4 | 6 | 7 | |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
We could solve the puzzle by moving the 6 tile to the right, then the 5 tile down, then the 1 tile to the right. For this assignment, you will solve a (n × m)-sliding tile puzzle, using Uniform Cost Search (which will just be Bredth-First for this problem, since the operations all have unit costs), Iterative Deepening Depth First Search, A*, IDA*, and greedy.
The problem will be defined by the height and width of the puzzle, and the initial state of the problem. The goal will always be the blank, follwed by tiles numbered from 1 to (n * m), left-to-right, and top-to-bottom. For example, for a 4 × 4 puzzle, the goal would be:
1 | 2 | 3 | |
4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
Whereas in a 2 × 3 puzzle, the goal would be:
1 | 2 | |
3 | 4 | 5 |
The initial state will be given as a list of tiles, as they would be filled in from left to right and top to bottom. So, the intial position:
4 | 5 | |
2 | 1 | 7 |
8 | 6 | 3 |
would be reperesented by the list [4, 0, 5, 2, 1, 7, 8, 6, 3]. Note that you need both the list and the width of the puzzle to completely represent the puzzle. Technically, you only need the width and the list to completely represent a puzzle's state (since the height is just the length of the list divided by the width), but your functions will take both the width and height as parameters
You will need to write 5 functions (ucs, ids, astar, idastar, greedy), each of which take the same three parameters: Width of the puzzle, the height of the puzzle, and a list reperesenting the initial state of the puzzle. Your functions should return a tuple conisiting of 3 values:
3 | 1 | 2 |
4 | 7 | 5 |
6 | 0 | 8 |
Thus, a valid run of astar woul look like:
>>> astar(3,3,[1,4,2,6,3,5,0,7,8]) ((5, 12), 4, [6, 3, 4, 1])
while a valid run of idastar would be:
>>> idastar(3,3,[8,7,6,5,4,3,2,1,0]) ([(1, 2), (5, 12), (29, 72), (81, 202), (115, 268)], 28, [3, 6, 7, 8, 5, 2, 1, 3, 6, 7, 8, 5, 2, 1, 3, 6, 7, 8, 5, 2, 1, 3, 6, 7, 8, 5, 2, 1])
Note the the number of states expaned and added for your solution do not need to match exactly the numbers above (since they depend upon several factors, such as the order in which states are created by your "next" operator). The solutions should be 4 and 28 moves long, respectively, however!
You have some freedom in how you represent a state. I would recommend creating a State class, that stores at least the following data:
You could also add some more information to each state, to make processing easier. Don't go crazy, though -- BFS and A* take up lots of memory with even a compact board representation! In addition, your State class should contain at least the following methods
You are of course welcome to add more methods -- in particular I would suggest a __repr__ method that returns a "pretty-print" string of your state -- it is very helpful for debugging!
For A* and IDA*, you will use the "Manhattan Distance" heuristic, defined on page 103 of the textbook. The best way to use this heurisitic is to create a table, such that T[i][j] stores the cost if the tile numbered i is at position j (were the positions are enumerated in row major order). This is best shown with an example. For a 2 × 3 puzzle, assuming that the goal is:
1 | 2 | |
3 | 4 | 5 |
The heuristic table should be:
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | 0 | 1 | 2 | 1 | 2 |
2 | 2 | 1 | 0 | 3 | 2 | 1 |
3 | 1 | 2 | 3 | 0 | 1 | 2 |
4 | 2 | 1 | 2 | 1 | 0 | 1 |
5 | 3 | 2 | 1 | 2 | 1 | 0 |
Note that the 0 row is all 0's because the ``blank'' is not a tile, and is not counted -- otherwise Mahnattan Distance would not be admissible
You should make this table once (with clever use of mods, slices, and list comprehensions), and then use it each time you need to determine the h-value of a particular board position.
You will want to use a closed list for these algorithms. Whenever a state is expanded (that is, its children are generated), that expanded state should be added to a closed list. Whenever a state is generated, it should first be tested to see if it is in the closed list, before being added to the open list (that is, the priority queue of states on the fringe). You should use either a set or a dictionary to represent your closed list, since that gives O(1) time for membership testing. (That's why you want your states to have __hash__ and __eq__ methods -- then creating a closed list is very easy)
You'll want to use a priority queue for your best-first search. You are perfectly welcome to use a built-in python function for this. I used heapq, which was easy to used and seemed efficient enough for the purpose.
A* and UCS and greedy are all very similar. Likewise, IDA* and IDS are very similar. You do not want to be rewriting (nearly) the same code -- and you certainly don't want to be using cut and paste! So, you are required to write 2 functions that will do the bulk of the work (one for the iterated functions, ida* and ids, and one for the queue-based functions, A*, UCS, and Greedy). So your code for astar should look something like:
def astar(w, h, pieces): heuristicTable = buildTable(w,h) return bestFirst(w, h, pieces, lambda state: state.g + state.h(heuristicTable))Your astar function does not need to be exacly like this, but it does need to follow the same idea -- call a bestFirst function passing in an evaluation function
You need to create (at least!) 5 python functions, named ucs, ids, astar, idastar and greedy. Each method will take 3 parameters: width of the board, height of the board, and a list of board contents. astar, ucs, and greedy should return a 3-tuple consisting of:
idastar and ids will return a similar 3-tuple, consisting of:
>>> bfs(3,3,[3,5,1,6,8,4,7,0,2]) ((2420, 6698), 13, [8, 4, 2, 8, 4, 5, 1, 2, 5, 4, 7, 6, 3])
>>> ids(3,3,[1,4,2,6,3,0,7,8,5]) ([(1, 3), (4, 11), (12, 35), (36, 99), (100, 291), (292, 803), (804, 2339), (1048, 2876)], 7, [5, 8, 7, 6, 3, 4, 1])
>>> astar(4,4,[7,6,5,4,3,2,1,0,8,9,10,11,12,13,14,15]) ((1296, 3979), 28, [1, 2, 3, 7, 6, 5, 4, 1, 2, 3, 7, 6, 5, 4, 1, 2, 3, 7, 6, 5, 4, 1, 2, 3, 7, 6, 5, 4])
>>> greedy(4,4,[7,6,5,4,3,2,1,0,8,9,10,11,12,13,14,15]) ((394, 1246), 76, [4, 5, 6, 2, 1, 6, 2, 1, 3, 7, 1, 3, 6, 4, 5, 2, 3, 6, 4, 5, 2, 3, 6, 1, 7, 4, 5, 6, 1, 7, 4, 5, 7, 1, 6, 7, 5, 4, 1, 6, 3, 2, 7, 3, 2, 7, 3, 2, 6, 1, 4, 5, 2, 6, 7, 3, 6, 2, 5, 4, 1, 7, 2, 6, 3, 2, 7, 5, 6, 7, 2, 3, 7, 6, 5, 1])
>>> idastar(4,4,[7,6,5,4,3,2,1,0,8,9,10,11,12,13,14,15]) ([(5, 2), (230, 82), (4376, 1545), (65718, 23093), (250366, 88130)], 28, [1, 2, 3, 7, 6, 5, 4, 1, 2, 3, 7, 6, 5, 4, 1, 2, 3, 7, 6, 5, 4, 1, 2, 3, 7, 6, 5, 4])