Assignment 2: Search

Due Date: Feb 25th

Sliding Tile Puzzles

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.

Problem Definition

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

Required Functions

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:

  1. A tuple containing the number of states expanded during the search and the number of states added to the fringe. (The iterated functions, idastar and ids, should contain a list of these tuples, once for each iteration)
  2. The length of the solution
  3. The solution itself, as a sequence of moves. A move is denoted by the label of the tile that needs to be moved. So, for the puzzle:
    3 1 2
    4 7 5
    6 0 8
    A solution would be to first move the 7 tile down, then the 4 tile to the right, then the 3 tile down. This solution would be represented by the list [7, 4, 3]

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!

State Representation

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!

Heuristic Function

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.

Implementation Details

Closed List

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)

Priority Queue

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.

Code Reuse

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

Summary

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:

  1. A tuple contating 2 elements, the total number of states expanded and the total number of states generated (added to the open list)
  2. An integer, the number of moves in the final solution
  3. A list of integers, representing the solution (with each element in the list representing the label on the tile to be moved)

idastar and ids will return a similar 3-tuple, consisting of:

Sample outputs:
  1. BFS (10 points)
    >>> 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])
    
  2. IDS (10 points)
    >>> 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])
    
  3. A* (10 points)
    >>> 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])
    
  4. Greedy
    >>> 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])
    
  5. IDA* (10 points)
    >>> 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])
    

Testing script

As promised, here is a testing script: searchTest.py. It is not completely comprehensive (doesn't test greedy, for instance) but it is certainly enough to get started.