Эффективный решатель нонограмм

Недавно я увидел опубликованную головоломку от британского GCHQ:

Он включает решение нонограммы 25x25:

Нонограмма - это логическая головоломка с картинками, в которой ячейки в сетке должны быть окрашены или оставлены пустыми в соответствии с числами сбоку сетки, чтобы открыть скрытое изображение. В этом типе головоломок числа представляют собой форму дискретной томографии, которая измеряет, сколько непрерывных линий заполненных квадратов есть в любой данной строке или столбце. Например, подсказка «4 8 3» будет означать, что есть наборы из четырех, восьми и трех заполненных квадратов в указанном порядке, по крайней мере, с одним пустым квадратом между последовательными группами ».

Естественно, у меня была склонность попробовать написать программу, которая решила бы эту проблему за меня. Я думал о рекурсивном алгоритме поиска с возвратом, который начинается в строке 0, и для каждого возможного расположения этой строки с учетом информации из подсказки строки он помещает возможную комбинацию следующей строки и проверяет, является ли это допустимым размещением с учетом столбца подсказки. Если это так, он продолжается, если нет, он возвращается, пока все строки не будут помещены в допустимую конфигурацию или все возможные комбинации строк не будут исчерпаны.

Я тестировал его на нескольких пазлах 5x5, и он отлично работает. Проблема в том, что вычисление головоломки 25x25 GCHQ занимает слишком много времени. Мне нужны способы сделать этот алгоритм более эффективным - достаточно, чтобы он мог решить головоломку, указанную выше. Любые идеи?

Вот мой код для создания набора возможных строк для каждой строки, а также кода для решателя (примечание * он использует некоторые нестандартные библиотеки, но это не должно отвлекать от сути):

// 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
        }
    }
}

person gowrath    schedule 26.12.2015    source источник
comment
Использование программирования с ограничениями - один из хороших подходов к решению подобных проблем. См., Например, мою модель MiniZinc: hakank.org/minizinc/nonogram_gchq.mzn и модель Picat: hakank.org/picat/nonogram_gchq.pi Существует довольно много других вариантов, например neilmadden .wordpress.com / 2015/12/15 / и eclipseclp.org/ examples / nono_gfd.ecl.txt   -  person hakank    schedule 26.12.2015
comment
Очень интересно. Я не знаком с программированием в ограничениях, поэтому обязательно прочитаю об этом. Спасибо! Но что, если вы хотите решить эту проблему, используя, скажем, C ++; в основном императивный подход к программированию?   -  person gowrath    schedule 26.12.2015
comment
@ גלעדברקן Я добавил код. В нем многого не хватает, но это главная проблема.   -  person gowrath    schedule 26.12.2015
comment
Вы проверили ответы на этот вопрос? stackoverflow.com/questions/813366/   -  person m69 ''snarky and unwelcoming''    schedule 27.12.2015
comment
@ m69 Эти решатели работают так же, как и мои. Проблема не в том, чтобы заставить его работать, проблема в том, чтобы заставить его работать для большой головоломки (25x25 и т. Д.). Я подумал о том, чтобы заставить столбцы и строки с помощью ограничений, но я не знаю, как мне проверить, является ли это правильной конфигурацией или нет.   -  person gowrath    schedule 27.12.2015


Ответы (1)


Я сделал решение на Java, которое для вашего примера головоломки (25x25) решает его примерно за 50ms.

Полный код и примеры ввода: Github


Предпосылки

  • Понятия Java (понимание примеров)
  • Побитовые операции: я сильно от них зависим, поэтому прочтите об этом, если вы не очень знакомы с Это.
  • Алгоритмы обхода графа: DFS

Данный:

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

Предварительно рассчитайте все перестановки в строке.

Перестановка также сохраняется в побитовом представлении. Если первый бит установлен в значение true, если он заполняет первый столбец и т. Д. Это эффективно как по времени, так и по пространству. Для расчета мы сначала подсчитываем количество дополнительных пробелов, которые можно добавить.

Это number_of_columns - sum_of_blocksize - (number_of_blocks-1)

Dfs по всем возможным перестановкам размещения дополнительных пробелов. См. calcPerms и добавьте их в список возможных перестановок, если он совпадает с изначально заданными окрашенными блоками.

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, ...
}

Реализуйте проверки для каждой строки

  • Проверка строк:

[Тривиально:] Мы собираемся использовать предварительно вычисленные перестановки, поэтому нам не нужна дополнительная проверка для каждой строки.

  • Проверка столбцов:

Для этого я сохраняю для каждой строки и столбца индекс текущего размера блока colIx и позицию в этом размере colVal.

Он рассчитывается по значению и индексу предыдущей строки:

  • Значение увеличивается на 1, если столбец закрашен в текущей строке.
  • значение сбрасывается до 0, а индекс увеличивается на 1, если столбец был закрашен в предыдущей строке и не находится в текущей строке.

Пример:

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
        }
    }
}

Теперь мы можем использовать этот индекс / значения, чтобы определить, какие биты, как ожидается, будут ложными / истинными в следующей строке.

Используемая структура данных для проверки:

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

Когда бит в предыдущей строке установлен, мы ожидаем, что бит в текущей строке будет установлен в истинное значение, если и только если текущий размер все еще меньше, чем ожидаемый размер для текущего индекса. В противном случае он должен быть 0, потому что вы хотите обрезать его в текущей строке.

Или, когда последний размер блока уже используется для столбца, мы не можем начать новый блок. Следовательно, бит должен быть 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;
        }
    }
}

Dfs все строки и проверьте, действительны ли они

Это делает фактическую часть dfs такой же простой, как и вашу собственную. Если маска строки соответствует текущей конфигурации, мы можем обновить индексы / значения столбцов и перейти к следующей строке и в конечном итоге оказаться в строке 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;
}
person Sam Segers    schedule 27.12.2015