CMPSCI 383: Artificial Intelligence

Fall 2014 (archived)

Assignment 01

This assignment is due at 1700 on Friday, 12 September.

The goal of this assignment is to write a validator and a solver for a toy problem: Rubik’s Toroid, a simplified version of the better-known Rubik’s Cube puzzle.

I will be updating the assignment with questions (and their answers) as they are asked.

Rubik’s Toroid

Rubik’s Toroid is a sliding tile puzzle game played on a rectangular board. The board consists of n rows and m columns of colored tiles. Each tile is a single color; there are n colors (one for each row), and m tiles of each color.

The game is considered solved when each row consists only of tiles of the same color, and the colors of each row are in ascending order, top-to-bottom.

Consider the following example game with a 3 x 4 board. That is, n=3 and m=4. Let’s assign each color a number from the set [0, 1, 2].

1
2
3
0 0 0 0
1 1 1 1
2 2 2 2

This game is in the solved state.

Here is another game, on a 3 x 3 board, where we’ve assigned each color a letter from the set [a, b, c].

1
2
3
a a c
a b b
c c b

This game is not in a solved state.

The player manipulates the board by rotating a row or column. Rows are rotated left or right; columns are rotated up or down. When a row is rotated right, each tile shifts right one position, and the rightmost tile becomes the leftmost tile. Other rotations are analogous. If we took the previous example:

1
2
3
a a c
a b b
c c b

and rotated the middle column up, the board would then appear as follows:

1
2
3
a b c
a c b
c a b

Data format

I will test your programs using data in the following text-based formats.

Let’s start with the game board.

  • A game board is represented as a sequence of one or more lines.
  • Each line consists of a whitespace-delimited sequence of one or more tiles.
  • Each tile will be represented by its color, which is given as an integer in the interval [0, n-1].

For example:

1
2
3
0 0 0
1 1 1
2 2 2

is an instance of the game in the solved state.

Moves are represented in a fashion similar to boards.

  • A move list is represented as a sequence of lines.
  • Each line consists of a single move.
  • A move has two parts, separated by whitespace.
  • The first part of the move is the direction, and is a single character, one of u, d, l, or r, representing an upward, downward, leftward, or rightward rotation.
  • The second part of the move is either the row or column number, starting from zero. Columns are numbered left-to-right; rows are numbered top-to-bottom.

For example:

1
2
3
u 1
l 2
d 0

is a valid move list.

Validating a solution

A good first step in writing a program that searches for solutions is to write a solution validator. You will write a program which, when given an input that consists of a game board and a sequence of zero or more moves as described above, checks the validity of the solution.

For example, a validator would check:

1
2
0 0
1 1

and find it to be valid, as is:

1
2
3
4
5
0 0 1
1 2 1
2 2 0
r 1
d 2

But none of the following examples are valid:

1
2
3
0 1 0
1 0 1
2 2 2
1
2
3
2 2 2
1 1 1
0 0 0
1
2
3
4
0 0 0
1 1 1
2 2 2
u 0 
1
2
3
4
5
1 2 0
2 2 0
1 0 1
r 0
l 2

Your validator should output a single line, consisting only of the string valid or invalid, corresponding to the (in)validity of the input.

Finding a solution

You will also write a program to search for a solution to a given game board.

Your solver should find and output a valid solution. Specifically, your program’s output should start with its input, and follow with a (possibly empty) list of moves. For example, given the input:

1
2
0 1 0 1 1
0 1 0 1 0

one correct output is:

1
2
3
4
5
6
0 1 0 1 1
0 1 0 1 0
l 1
u 1
u 3
u 4

You are free to use your language’s standard library for data structures, such as queues and stacks. But you must implement the search method you use yourself. Do not use a library for search. I will consider it plagiarism if you do.

I will not provide unsolvable n x 1 boards, so your program needn’t check for this case. Any board provided as input to your program will be solvable, in principle.

What to submit

You should submit three things: a validator, a solver, and a readme.txt.

  • Both your validator and your solver should treat the first command line argument as the path to an input file. If, for example, your validator’s main method is in a Java class named Validator, I should be able to use java Validator /Users/liberato/testcase and your program should read and validate the input in the file located at /Users/liberato/testcase.
  • Your validator should print either valid or invalid to standard output (in Java: System.out.println("valid");). Do not print anything else to standard output, or your program will not pass the autograder. If you want to leave debugging information in the output, use standard error (System.err.print()).
  • Your solver should print only the initial game board and the sequence of moves to standard output.

Submit the source code of your validator and solver, written in the language of your choice. Name the files containing the main() methods Validator.java and Solver.java, or your language’s equivalent. If the files you submit depends upon other files (for example, if both classes rely upon a third Board class), be sure to submit these other files as well.

Your readme.txt should contain the following items:

  • your name
  • if the language of your choice is not Java, Python, Ruby, node.js-compatible JavaScript, ANSI C or C++ (or if you’re concerned it’s not completely obvious to me how to compile and execute it), a description of how to compile and execute the submitted files
  • a description of your search strategy
  • a description of what you got working, what is partially working and what is completely broken

If you’re using language features that require a specific version of your language or runtime, check for that version at program start and fail if it’s not present, emitting an understandable error message indicating this fact. Your program must compile and execute on the Edlab Linux machines.

If your program does not compile, you will receive no credit. Check with me in advance if you’re concerned.

Grading

We will run your program on a variety of test cases. Your grade will be proportional to the number of test cases you pass. The test cases will not be available to you before grading. You are welcome to write and distribute your own test cases. Posting them to the Moodle forum will likely earn you kudos from your peers.

About 40% of your grade will be based on your validator, and the remaining 60% on your solver. For both the validator and solver, about half of the tests will be on 3 x 3 boards. If you’re having trouble with arbitrarily sized boards, focus on 3 x 3 boards so as to maximize your partial credit.

We’re not going to feed your program incorrectly formatted input, so you need only concern yourself with handling input in the format described in the assignment.

Some of the test cases will push at the boundaries of what your program will be capable of, depending upon your choice of search strategy. If your program exceeds available heap memory (which we’ll set to 1 GB in Java, using the -Xmx1024M argument if necessary), or if it does not terminate in twenty seconds on reasonably-sized valid input, we will consider the test case failed.

Important Note

Update: CSCF appears to have speedily resolved this problem. Plain old javac should now work. The text below should no longer apply, at least on the edlab Linux machines.

For whatever reason, while the default JVM on the edlab is Java 7 compatible:

1
2
3
4
$ java -version
java version "1.7.0_65"
OpenJDK Runtime Environment (IcedTea 2.5.1) (7u65-2.5.1-4ubuntu1~0.12.04.2)
OpenJDK Server VM (build 24.65-b04, mixed mode)

the default Java compiler is only Java 6:

1
2
$ javac -version
javac 1.6.0_32

I’ve asked CSCF to update the javac default, but in the meantime, you can use, for example,

1
/usr/lib/jvm/java-7-openjdk-i386/bin/javac Validator.java

to compile your code.

Questions and answers

I don’t think this is a big deal but when my solver prints out the board, it prints a space at the end of each line. For example

0 0 0 0 
1 1 1 1 
2 2 2 2 

It’s hard to tell, but there is an extra space at the end of the line after the 0, after the 1, and after the 2. Will this be an issue with the autograder?

It’s not ideal, but it should be OK for this assignment.

Later assignments may be more sensitive to whitespace, so even if you don’t fix it now, think about how you might fix it later.

I think we don’t need to use search. I wrote a solution that just moves tiles to the right places using fixed sequences of moves. Is this OK?

Absolutely! Search is a general solution to problems. Specific solutions often have different characteristics that make them worthwhile (in this case: time bounds not dependent upon the branching factor, and constant space). And viewing the problem at different levels of abstraction can lead to either improved search algorithms, or just specific algorithms for specific problems, as you’ve hinted at.

In this case, I imagine you figured out a sequence of moves that, say, swaps two adjacent tiles without moving any other tiles on the board, and then your program repeatedly applies that sequence to get each tile into the correct row. That’s great, and totally valid. The key problem in developing such an algorithm is figuring out the move sequence for swapping tiles. If you’re clever, you may be able to work it out on paper. If not, you could always search for it (once) then encode it into your program.

If you go this route, remember that small boards should still be solvable by your program.

For assignment01, the instructions say that “reasonably sized” input needs to terminate in 20 seconds.

What is considered reasonably sized? 4x4? 50x50? 1000x1000?

It depends. Your programs (both solver and verifier) should be able to handle arbitrarily large input. I’m hedging here because I don’t want people to hardcode array boundaries. Show me you can use data structures that are dynamically allocated.

The verifier should be able to handle just about anything, as its runtime should be linear in the number of moves in its input. I will not provide input to your solver that is so large and/or so scrambled that a reasonable implementation of the solver can’t solve it quickly.

Implicitly (and now explicitly): You need to figure out a boundary for the solver on your own. I will release the test cases after we grade, so if you feel you barely missed a cutoff, you can show me and/or Patrick and we’ll give you credit. The twenty-second cutoff is to keep the autograder’s runtime reasonable.

I thought of something while writing some code: do we need to worry about the 1xN or the Nx1 edge cases? 1xN is really trivial as it only has a single state (the initial is also the goal), but Nx1 has N reachable states, but it’s possible that none of them are the goal state. For example the board:

3
1
0
2

is impossible to solve (no amount of shifting up/down will solve it and left/right shifts have no effect). I can imagine this would break some people’s code (infinite searches or exceptions caused in shifting/handling of the board) as these might have to specially handled.

Just wondering to handle these edge cases, if they will even be a problem at all.

A good question. I will only supply solvable boards to your solver. You needn’t consider degenerate n x 1 (where n > 1) boards like this one. I’ve updated the assignment text to reflect this clarification.

You should be prepared for trivial (1 x m) boards. Any reasonable search algorithm should notice when the initial state is the goal state, so they shouldn’t be a problem.

One student’s thoughts and questions

I have a few questions as to how to do part 2. I’m pretty confident I have part 1 down (maybe not how long it will take, but I have it in my head how to do it).

I’m thinking of the search in a tree like structure where shifting rows/cols expands the tree. Thus, the breadth of each node is the # of possible actions (# rows * # cols. a 3x3 grid is 9).

Actually, it’s worse than that for n=m=3, though better for larger values m and n. In the general case, you can rotate each row or column in either direction. So branching factor = 2 * (m + n) for (n, m > 2).

My original thought was that 9 is a sort of large branching factor and it would be better to go with DFS. Hey, it would even save on memory. However, I ran into an issue where I just couldn’t figure out how to structure the search without some sort of infinite loop where the tree becomes infinitely deep (keep generating the left-most part of the tree infinitely, never creating the rest of the tree. Keep repeating the same shifting action over and over).

When implementing search strategies, you need consider whether the generated search tree might have loops. If it can (as this one does), you need to keep track of the nodes in the tree you’ve generated and explored so far. This is discussed in Section 3.3 of the text and is the real source of memory use. Or see below.

Then I looked at the 2x2 grids and noticed that the minimum solution length was no more than 2 for all the different variations (there’s like 5). So, the tree for 2x2 grids had a depth of at most 2. This made me have the though that the depth for the all the 3x3 trees isn’t too high a number. And BFS keeps the depth to a minimum.

DFS or BFS, you’re constrained mostly by the number of possible states, since you need to keep track of them. Reach back into your CMPSCI 240 knowledge. This is a combinatorics bins-and-balls problem in disguise. You have n * m balls (n of each of m different colors), and n * m bins. How many different ways can you put one ball into each bin, where balls of the same color are indistinguishable?

It turns out it’s not too bad for smaller values of n and m. A 3x3 puzzle has < 2000 states, so either approach should work.

At roughly 100 Bytes a node (a very rough estimate) the tree would need a depth of more than 7 to go over the memory limit. The issue with this would be that there’s no way it would solve a 4x4 board (branching factor of 16) within the memory limit using BFS.

Here, the question is what is the maximum depth of a solution in the worst case? I’ll leave it to you to puzzle this out (or not, you don’t need to to solve the problem), but I guarantee it’s not ((maximum # states) - 1).

So I guess my question is to whether I’m on the right track in using BFS or should I go back to DFS or possibly some other search Algorithm? I’ve only really considered the memory space and not the running time, but I think memory space is the bigger constraint.

Optimally, of course, you’d use A*, which we haven’t covered, but many people claimed to know about when I asked for a show of hands. We’ll get to it on Tuesday and it’s straightforward to implement.

Ignoring that, iterative deepening depth-first search is generally the best approach for uninformed search, as it trades space/time in the most reasonable way. You get the optimality of BFS along with the better space bound of DFS, with only a small time overhead (see 3.4.5 in the text). You’ll likely still run into the time constraint for larger boards, but it should work fine on smaller (3x3) boards.

I realized a bit after I e-mailed you that iterative deepening DFS would be better. I originally just couldn’t think of a way to stop the looping without some sort of pattern recognition of the previous states (seemed a little too complex, looked for a different option). That is, I overlooked iterative deepening (and I guess depth limited DFS).

You can track previous states; your only concern there is memory. For example, if you use A* with an inconsistent heuristic, you’ll need to do so.