Reputation: 3224
So I recently saw this puzzle posted by the British GCHQ:
It involves solving a 25x25 nonogram:
A nonogram is picture logic puzzles in which cells in a grid must be colored or left blank according to numbers at the side of the grid to reveal a hidden picture. In this puzzle type, the numbers are a form of discrete tomography that measures how many unbroken lines of filled-in squares there are in any given row or column. For example, a clue of "4 8 3" would mean there are sets of four, eight, and three filled squares, in that order, with at least one blank square between successive groups."
Naturally I had the inclination to try and write a program that would solve it for me. I was thinking of a recursive backtracking algorithm that starts at row 0, and for each possible arrangement of that row given the information from the row clue, it places a possible combination of the next row and verifies whether it is a valid placement given the column clues. If it is, it continues, if not it backtracks, until all the rows are placed in a valid configuration, or all possible row combinations have been exhausted.
I tested it on a few 5x5 puzzles and it works perfectly. The issue is that it takes too long to compute the 25x25 GCHQ puzzle. I need ways to make this algorithm more efficient - enough so that it can solve the puzzle linked above. Any ideas?
Here is my code for generating a set of the row possibilities for each row as well as the code for the solver (Note* it uses some non standard libraries but this shouldn't detract from the point):
// The Vector<int> input is a list of the row clues eg. for row 1, input = {7,3,1,1,7}. The
// int currentElemIndex keeps track of what block of the input clue we are dealing with i.e
// it starts at input[0] which is the 7 sized block and for all possible places it can be
// placed, places the next block from the clue recursively.
// The Vector<bool> rowState is the state of the row at the current time. True indicates a
// colored in square, false indicates empty.
// The Set< Vector<bool> >& result is just the set that stores all the possible valid row
// configurations.
// The int startIndex and endIndex are the bounds on the start point and end point between
// which the function will try to place the current block. The endIndex is calculated by
// subtracting the width of the board from the sum of the remaining block sizes + number
// of blocks remaining. Ie. if for row 1 with the input {7,3,1,1,7} we were placing the
// first block, the endIndex would be (3+1+1+7)+4=16 because if the first block was placed
// further than this, it would be impossible for the other blocks to fit.
// BOARD_WIDTH = 25;
// The containsPresets funtion makes sure that the row configuration is only added to the
// result set if it contains the preset values of the puzzle (the given squares
// at the start of the puzzle).
void Nonogram::rowPossibilitiesHelper(int currentElemIndex, Vector<bool>& rowState,
Vector<int>& input, Set< Vector<bool> >& result,
int startIndex, int rowIndex) {
if(currentElemIndex == input.size()) {
if(containsPresets(rowState, rowIndex)) {
result += rowState;
}
} else {
int endIndex = BOARD_WIDTH - rowSum(currentElemIndex+1, input);
int blockSize = input[currentElemIndex];
for(int i=startIndex; i<=endIndex-blockSize; i++) {
for(int j=0; j<blockSize; j++) {
rowState[i+j] = true; // set block
}
rowPossibilitiesHelper(currentElemIndex+1, rowState, input, result, i+blockSize+1, rowIndex); // explore
for(int j=0; j<blockSize; j++) {
rowState[i+j] = false; // unchoose
}
}
}
}
// The function is initally passed in 0 for the rowIndex. It gets a set of all possible
// valid arrangements of the board and for each one of them, sets the board row at rowIndex
// to the current rowConfig. Is then checks if the current configuration so far is valid in
// regards to the column clues. If it is, it solves the next row, if not, it unmarks the
// current configuration from the board row at rowIndex.
void Nonogram::solveHelper(int rowIndex) {
if(rowIndex == BOARD_HEIGHT) {
printBoard();
} else {
for(Vector<bool> rowConfig : rowPossisbilities(rowIndex)) {
setBoardRow(rowConfig, rowIndex);
if(isValidConfig(rowIndex)) { // set row
solveHelper(rowIndex+1); // explore
}
unsetBoardRow(rowIndex); // unset row
}
}
}
Upvotes: 4
Views: 8906
Reputation: 1951
I've made a solution in Java, that for your example puzzle (25x25) solves it in about 50ms
.
Full code and input examples: Github
R, C // number of rows, columns
int[][] rows; // for each row the block size from left to right (ex: rows[2][0] = first blocksize of 3 row)
int[][] cols; // for each column the block size from top to bottom
long[] grid; // bitwise representation of the board with the initially given painted blocks
A permutation is also stored in a bitwise representation. Where the first bit is set to true if it fill the first column etc.. This is both time and space efficient. For the calculation we first count the number of extra spaces that can be added.
This is number_of_columns - sum_of_blocksize - (number_of_blocks-1)
Dfs over all possible permutations of placing extra spaces. See calcPerms
and add them to the list of possible permutations if it's a match with the initially given painted blocks.
rowPerms = new long[R][];
for(int r=0;r<R;r++){
LinkedList<Long> res = new LinkedList<Long>();
int spaces = C - (rows[r].length-1);
for(int i=0;i<rows[r].length;i++){
spaces -= rows[r][i];
}
calcPerms(r, 0, spaces, 0, 0,res);
rowPerms[r] = new long[res.size()];
while(!res.isEmpty()){
rowPerms[r][res.size()-1]=res.pollLast();
}
}
...
// row, current block in row, extra spaces left to add, current permutation, current number of bits to shift
static void calcPerms(int r, int cur, int spaces, long perm, int shift, LinkedList<Long> res){
if(cur == rows[r].length){
if((grid[r]&perm)==grid[r]){
res.add(perm);
}
return;
}
while(spaces>=0){
calcPerms(r, cur+1, spaces, perm|(bits(rows[r][cur])<<shift), shift+rows[r][cur]+1,res);
shift++;
spaces--;
}
}
static long bits(int b){
return (1L<<b)-1; // 1 => 1, 2 => 11, 3 => 111, ...
}
[Trivial:] We are going to use precalculated permutations so we don't need any extra validation per row.
Herefor I'm keeping for each row and column the index of the current blocksize colIx
, and the position in that size colVal
.
This is calculated by the value and index of the previous row:
Sample:
static void updateCols(int row){
long ixc = 1L;
for(int c=0;c<C;c++,ixc<<=1){
// copy from previous
colVal[row][c]=row==0 ? 0 : colVal[row-1][c];
colIx[row][c]=row==0 ? 0 : colIx[row-1][c];
if((grid[row]&ixc)==0){
if(row > 0 && colVal[row-1][c] > 0){
// bit not set and col is not empty at previous row => close blocksize
colVal[row][c]=0;
colIx[row][c]++;
}
}else{
colVal[row][c]++; // increase value for set bit
}
}
}
Now we can use these index/values to determine which bits are expected to be false/true in the next row.
Used datastructure for validation:
static long[] mask; // per row bitmask, bit is set to true if the bit has to be validated by the val bitmask
static long[] val; // per row bitmask with bit set to false/true for as expected for the current row
When bit in previous row is set, we expect the bit in current row to be set to true if and only if the current size is still smaller than the expected size for the current index. Else it has to be 0 because you want to cut it off at the current row.
Or when the last blocksize is already used for the column we can not start a new block. Hence bit has to be 0.
static void rowMask(int row){
mask[row]=val[row]=0;
if(row==0){
return;
}
long ixc=1L;
for(int c=0;c<C;c++,ixc<<=1){
if(colVal[row-1][c] > 0){
// when column at previous row is set, we know for sure what has to be the next bit according to the current size and the expected size
mask[row] |= ixc;
if(cols[c][colIx[row-1][c]] > colVal[row-1][c]){
val[row] |= ixc; // must set
}
}else if(colVal[row-1][c] == 0 && colIx[row-1][c]==cols[c].length){
// can not add anymore since out of indices
mask[row] |= ixc;
}
}
}
This makes the actual dfs part as easy as your own. If the rowmask fits with the current configuration we can update the column indices/values and traverse to the next row and eventually end up at row R.
static boolean dfs(int row){
if(row==R){
return true;
}
rowMask(row); // calculate mask to stay valid in the next row
for(int i=0;i<rowPerms[row].length;i++){
if((rowPerms[row][i]&mask[row])!=val[row]){
continue;
}
grid[row] = rowPerms[row][i];
updateCols(row);
if(dfs(row+1)){
return true;
}
}
return false;
}
Upvotes: 3