views:

1426

answers:

6

Hey, it's Friday afternoon, let's have a fun puzzle/algorithm problem to solve.

One of my favorite Nintendo DS games is Picross DS. The game is quite simple, it involves solving puzzles called Nonograms. You can try a simple online Picross clone here: TylerK's Picross.

Nonograms are a grid, with sequences of numbers defined for every row and column of the grid. The numbers define blocks of "filled in" squares for that row/column, but the unfilled areas on either sides of the blocks are not defined. For example, if you have a row that looks like this:

Possible solutions for that row would include:

etc.

The "4 5" simply tells you that, somewhere in the row, there are 4 sequential blocks filled in, followed by 5 sequential blocks filled in. Those will be the only blocks filled in, and the amount of space before/after them are not defined.

A puzzle is complete when all rows and columns meet their definitions, without any contradictions.

Extremely simple game in concept, but it can take quite a while to solve some of them manually. Picross DS's puzzles gradually increase in size up to 25x20 grids, which would generally take me about half an hour to solve.

However, it always occurs to me that it'd be a pretty easy game to write a program to solve. I've never gotten around to it, but perhaps some people here will enjoy the challenge. If a decent number of solutions are posted, I'll benchmark them on a large puzzle against each other, similar to what Paolo Bergantino did here with his Boggle question. There's quite a bit of information on the Nonogram Wikipedia page about methods of attacking a puzzle, if you want to reference that.

Here's an easy-to-parse definition of Puzzle #1 from TylerK's Picross, so you have something to feed a program. Line 1 is puzzle dimensions (probably unnecessary), line 2 is row definitions, line 3 is column definitions. This is just the first thing that came to mind, so if anyone can think of a better input format, feel free to comment or edit this post to include it.

15 15
15,4 5,2 4,1 3,2,2,2 4 3,2 6 2,2 1 6 2,2 1 1 4 2,1 1,1 3 2 1,2 2 1 2 1,3 3 2 1,9
4 4,3 1 2 3,2 1 2 2,2 1 1,1 4 2,1 3,1 8,1 3 1 1,1 4 2 1,1 4,2 4 3,3 3 3,4 1,10 3,10
A: 

I don't have enough time right now to flesh out a solution, but this is how I would tackle it.

"function1" determine the possible combinations for a row or column given the constraints of the numbers on the top or side, and the squares that are already filled out and those that have been emptied.

"function2" take the output from function1 and logically and all the results together - the spots with ones could be filled in.

"function3" take the output from function1 and logically or all the results together - the spots with zeros could be emptied.

keep applying function2 and function3 to all the rows and collumns until no more boxes are emptied or filled in. If the puzzle is solved then, you are done.

If the puzzle is not solved, then take a row or column that has the fewest possibilities and apply the first possibility. Then apply function2 and function3 to the new board. If it leads to a contradiction (0 possibilities for a row or column) then un-apply the possibility and try a different one. Keep recursing like this until a solution is found.

John
This solution would come unstuck if you had, say, a 50 x 50 puzzle and the numbers in one of the rows were 1 1 1 1 1 1 1 1 1 1 1 1. There are many millions of possible combinations here.
Pourquoi Litytestdata
+2  A: 

This seems like a pretty obvious case for depth first search with backtracking, as with the "n queens" problem. The sort of cute part is that as well as placing rows/columns, you can shift the blocks.

Okay, here's an outline.

  1. Start with an empty board, place the first row

  2. Now, with that board, place a second row, check it against the column constraints. If it passes, recursively try the next row against that state; if it doesn't pass, then try the next possible placement of that row.

  3. If at any time you run out of possible placements of a row that satisfy the constraints, then the puzzle has no solution. Otherwise, when you run out of rowns, you've solved the problem.

It's convenient that these rows can be treated as binary numbers, so there is a natural ordering to the possibilities.

Charlie Martin
This is exactly the solution I had in mind for implementing it myself, I may try actually coding it up this weekend, if I get a chance.
Chad Birch
+3  A: 

Determining whether a Nonogram solution exists/is unique is NP-hard. See http://en.wikipedia.org/wiki/Nonogram#cite_note-0

Dave
+1. Translation: it's almost certain that no algorithm exists that is faster than brute-force trying all possibilities **on every problem instance**.
j_random_hacker
Further translation: if you do come up with an algorithm that is always better than brute force, regardless of what problem instance I throw at you, then you're smarter than **everyone in computer science**.
j_random_hacker
These translations are not quite accurate. There won't be a polynomial-time algorithm on all instances unless P=NP, but you might still do better than brute force. For example, brute force travelling salesman is O(n!), but it's theoretically possible you might find a O(2^n) or even O(n^(ln n)) algorithm, even if P!=NP. For comparison, 10!=3628800, 2^10=1024, and 10^(ln 10)=200.7. That kind of improvement can make quite a difference for moderate values of n, even though they are still non-polynomial.
dewtell
Also, as the Wikipedia page writes, we're not trying to solve every possible Nonogram. Assuming that we are looking at commercial puzzles, we're limited to Nonograms that (1) have a unique solution which (2) is supposed to be discoverable by a human being in reasonable time. That means there should be a path to a solution that involves very few branches, assuming that we can make the right deductions in the right order.
dewtell
+2  A: 

Rather than trying to place the "first" row, it will cut down on your search substantially if you try to get information from the "most constrained" row or column, which may have several forced values. A quick indication is to add up all the values in a row/column and add #_of_values - 1, then look for the row/column with the biggest such value (or smallest difference between this value and the # of rows or columns, if the rows and columns differ). Thus if you have a 25x25 puzzle, and one of the rows is "5 1 1 6 1 1 3", that row has a value of 24, which means it is very constrained - the relative position of all but one of the blank squares in that row is known, and the last blank square can be in any of 8 different relative positions. So for each group of filled squares, there are only two possibilities: extra blank square is to the left of this group, or extra blank square is to the right of this group. So, for example, five of the filled squares in that group of 6 are already known.

Once you have some forced values from one direction, that further constrains any of the groups from the other direction that intersect with the known information. So the best approach is likely to be working back and forth between columns and rows as more information becomes known, which is pretty much the way you need to solve it by hand.

dewtell
A: 

The real problem is whether anyone can come up with an algorithm that solves the said puzzle faster than a human? This is easy for relatively easy puzzles such as the reference one but if the puzzle grows most of the algorithms here will quickly slow down. Here is the puzzle I tried to solve. The problem is that the 4th row for example has 2220075 possible combinations I believe. If Charlie's algorithm temporarily accepts a wrong row for row 3, it will go through all these combinations for row 4. And this is not to mention the case where the algorithm contradicts itself on row 35 over a mistake it made on row 2.

My algorithm was similar to John's. It failed to run in x86 mode on my 64bit desktop. When I switched it to 64bit mode and let it run overnight, my computer was completely unusable in the morning. The process was reserving 8 Gigs of memory (8 Gigs physical on the desktop) and the computer wouldn't respond due to the frantic swapping. And it hadn't even solved all the possible rows. Not to mention it hadn't even touched the possible column combinations.

List<List<int>> rows =
                new List<List<int>>()
                {
                    new List<int> { 8,29,4 },
                    new List<int> { 6,4,25,4,3 },
                    new List<int> { 5,3,2,3,9,4,2,1,3 },
                    new List<int> { 4,2,2,2,2,1,2,2 },
                    new List<int> { 4,1,1,9,10,2,2,1 },
                    new List<int> { 3,2,6,5,5,1,1 },
                    new List<int> { 3,1,5,5,1,1 },
                    new List<int> { 3,1,4,4,1,1 },
                    new List<int> { 3,1,4,4,1,1 },
                    new List<int> { 3,1,3,3,1,1 },
                    new List<int> { 3,1,3,6,2 },
                    new List<int> { 3,1,2,3,2,4,2 },
                    new List<int> { 4,3,1,8,7,1,2,3 },
                    new List<int> { 4,2,1,12,11,1,2,4 },
                    new List<int> { 5,1,2,7,2,2,6,1,1,4 },
                    new List<int> { 4,1,1,1,6,2,2,6,1,2,1,3 },
                    new List<int> { 4,1,1,2,4,3,4,3,1,1,1,1,3 },
                    new List<int> { 4,1,1,2,1,4,1,2,3,2,1,2,2 },
                    new List<int> { 3,1,1,1,2,5,6,1,1,1,3,2 },
                    new List<int> { 3,2,1,1,2,1,5,4,4,2,1,2,1,2 },
                    new List<int> { 3,2,2,1,1,4,2,2,3,1,1,2,1,1,2 },
                    new List<int> { 3,1,3,2,1,1,4,1,5,3,2,1,3,1,2 },
                    new List<int> { 3,1,2,1,2,1,3,7,4,1,4,2,2 },
                    new List<int> { 2,1,4,1,1,1,2,6,2,2,2,3,2,1 },
                    new List<int> { 2,2,4,1,2,1,2,5,2,1,1,3,2,1 },
                    new List<int> { 2,2,1,4,1,1,3,3,2,1,4,4,1 },
                    new List<int> { 2,3,3,2,1,3,3,7,4,1 },
                    new List<int> { 2,3,2,4,5,8,1,2,1 },
                    new List<int> { 1,1,3,11,6,7,1,3,1 },
                    new List<int> { 1,1,2,2,13,10,2,3,2 },
                    new List<int> { 1,2,3,1,6,1,1,7,1,5,2 },
                    new List<int> { 1,1,3,2,6,1,1,1,1,4,1,4,2 },
                    new List<int> { 1,1,6,7,2,4,2,5,6,1 },
                    new List<int> { 1,1,2,3,1,4,2,2,11,2,1 },
                    new List<int> { 1,1,1,1,2,1,5,10,1,1,1 },
                    new List<int> { 1,1,1,1,4,7,4,10,1,1,1 },
                    new List<int> { 1,2,1,1,28,1,1,3 },
                    new List<int> { 1,2,1,2,27,2,1,3 },
                    new List<int> { 1,1,1,1,26,1,1,1,1 },
                    new List<int> { 2,3,1,28,2,1,2,1 }
                };
            List<List<int>> cols =
                new List<List<int>>()
                {
                    new List<int> { 40 },
                    new List<int> { 28,1 },
                    new List<int> { 23,8 },
                    new List<int> { 5,6,7,4 },
                    new List<int> { 3,6,1,9,3,1 },
                    new List<int> { 2,3,2,5,4,2,2 },
                    new List<int> { 1,2,4,1,2,5,2 },
                    new List<int> { 1,1,4,9,2,3,2 },
                    new List<int> { 2,4,2,6,1,4,3 },
                    new List<int> { 1,4,1,3,4,1,6 },
                    new List<int> { 1,4,3,2,3,5,5 },
                    new List<int> { 2,4,1,2,3,4,1,3 },
                    new List<int> { 1,2,3,4,2,2,4,4,1 },
                    new List<int> { 1,1,2,3,2,1,4,2,4 },
                    new List<int> { 2,3,5,3,3,5,4 },
                    new List<int> { 3,1,6,1,2,5,5 },
                    new List<int> { 3,2,6,2,15 },
                    new List<int> { 3,1,8,2,13 },
                    new List<int> { 2,2,4,5,15 },
                    new List<int> { 2,2,2,2,22 },
                    new List<int> { 2,1,1,1,12,6 },
                    new List<int> { 2,1,10,4,5 },
                    new List<int> { 3,1,3,1,2,4 },
                    new List<int> { 3,1,1,4,3,1,4 },
                    new List<int> { 3,2,2,3,2,2,5 },
                    new List<int> { 3,1,1,5,1,1,5 },
                    new List<int> { 3,1,1,5,1,1,5 },
                    new List<int> { 3,1,1,5,1,1,5 },
                    new List<int> { 3,2,5,2,1,1,4 },
                    new List<int> { 3,1,1,3,2,2,4 },
                    new List<int> { 3,1,6,4,5 },
                    new List<int> { 2,2,12,2,6 },
                    new List<int> { 2,2,1,1,22 },
                    new List<int> { 2,1,2,2,5,15 },
                    new List<int> { 3,1,4,3,2,14 },
                    new List<int> { 3,1,7,2,1,13 },
                    new List<int> { 3,2,6,1,1,6,8 },
                    new List<int> { 3,2,5,2,2,4,7 },
                    new List<int> { 2,1,2,4,1,1,1,4,1,4,2 },
                    new List<int> { 1,1,4,4,3,1,4,5,1 },
                    new List<int> { 1,1,5,1,1,2,1,2,2,3,2 },
                    new List<int> { 1,5,2,2,1,5,5,3 },
                    new List<int> { 1,6,2,1,4,2,6,1 },
                    new List<int> { 1,6,2,6,5,2 },
                    new List<int> { 1,5,3,1,9,2 },
                    new List<int> { 2,2,4,2,6,3 },
                    new List<int> { 1,2,2,2,9,2,1 },
                    new List<int> { 3,5,5,8,4 },
                    new List<int> { 4,13,9 },
                    new List<int> { 27,2 }
                };

Copyright goes to Tampere Guild of Information Technology / Kaisapais / A finnish brewery.

Mikko Rantanen
Well, if the program followed my suggestion of starting with the most constrained row or column, it would start with column 1, which locks in the first group of every row, and gives you most of the first few columns right off the bat. It also looks helpful to be able to deduce part of a row or column, even if there are too many possibilities to try and place every group.
dewtell
A: 

Yes, the problem is NP-complete, but that only means that you are (pretty much) stuck with exponentially growing run-times as the size of the puzzle grows. But in real life puzzle sizes don't grow. Hardly anyone bothers to build puzzles that are bigger than 100x100 and the vast majority are smaller than 50x50. Building a solver that will solve 95% of the puzzles published in books and magazines in a fraction of second is actually not particularly challenging. A fairly straight-forward depth-first search system will do it.

What's less obvious is that there is a small fraction of puzzles that are quite nasty and will cause run-times to explode for most simple solvers. Most of these are badly designed puzzles that are also difficult or impossible for humans to solve, but some are particularly clever puzzles that experienced human solvers can readily solve, using deeper insights than most AI programs can manage.

I've written a solver of my own that will solve most puzzles very quickly, and I've done a survey of many other solvers with benchmark results comparing their performance. I also have a discussion of an interesting class of hard puzzles (the Domino puzzles) that demonstrates how some puzzles that are not hard for a clever human prove very hard for most programs. Links to my solver and the Domino Puzzle will be found in the survey.

I think there is still a lot of room for improvement and would encourage people with fresh ideas to take a shot at it. But it's worth noting that the obvious things have been done and there isn't much use in doing them again.

Jan Wolter
I fed both Chad's puzzle and Mikko's puzzle to my solver, and it solved both in under a hundredth of a second of CPU time. These are both examples of line solvable puzzles, which can be solved by examining one row at a time and marking all the cells that you can. You never have to think about more than one line at a time. Such puzzles are pretty easy for computers to solve.
Jan Wolter