Рекурсивный поиск с возвратом в решающей программе судоку не завершается - PullRequest
1 голос
/ 27 мая 2020

Я написал программу MATLAB, которая решает головоломку судоку размером 9 x 9, используя рекурсивное решение для обратного отслеживания, но рекурсия, похоже, не завершается. Когда я останавливаю отладчик и смотрю на плату, я обнаруживаю, что моя плата уже содержит правильные решения. В моем подходе я прорабатываю элементы платы столбец за столбцом, начиная с элемента 1 в (1, 1) и заканчивая элементом 81 в (9, 9). checkSudoku проверяет, является ли номер допустимым местом размещения, просматривая строку, столбец и подсетку 3x3. h - это место, где происходит рекурсия. Может ли кто-нибудь дать совет относительно того, где мой код пошел не так?

function result = h(board, num)
if num >= 82
    result = board;
else
    if isnan(board(num))
        flag = false;
        c = ceil(num / 9);
        r = num - ((c - 1) * 9);
        n = 1;
        while (n <= 9) & (~flag)
            if checkSudoku(board, r, c, n)
                board(num) = n;
                product = h(board, num + 1);
                if ~isnan(product)
                    flag = true;
                    board(num) = n;
                else
                    board(num) = NaN;
                    n = n + 1;
                end
            else
                n = n + 1;
            end
        end
        if ~flag
            result = NaN;
        else
            result = h(board, num + 1);
        end
    else
        result = h(board, num + 1);
    end
end

end
function safe = checkSudoku(board, row, col, num)

r = row;
c = col;
subrow = board(r, :);
subcol = board(:, col);
subBoard = zeros(3, 3);

if any([1 2 3] == r)
    if any([1 2 3] == c)
        subBoard = board(1:3, 1:3);
    elseif any([4 5 6] == c)
        subBoard = board(1:3, 4:6);
    else
        subBoard = board(1:3, 7:9);
    end
elseif any([4 5 6] == r)
    if any([1 2 3] == c)
        subBoard = board(4:6, 1:3);
    elseif any([4 5 6] == c)
        subBoard = board(4:6, 4:6);
    else
        subBoard = board(4:6, 7:9);
    end
else
    if any([1 2 3] == c)
        subBoard = board(7:9, 1:3);
    elseif any([4 5 6] == c)
        subBoard = board(7:9, 4:6);
    else
        subBoard = board(7:9, 7:9);
    end
end

if any(subrow == num)
    safe = false;
elseif any(subcol == num)
    safe = false;
elseif any(any(subBoard == num))
    safe = false;
else
    safe = true;
end

end
function solvedBoard = solveSudoku(board)
solvedBoard = h(board, 1);
end

image

Я решил проблему и файл MATLAB из MITOpenCourseWare, домашнее задание 3 необязательный вопрос 3. Файл и фото можно найти здесь .

1 Ответ

1 голос
/ 28 мая 2020

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

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

Я настоятельно рекомендую вам прочитать документацию по return, так как это полезный инструмент для этих типов функций.

Теперь к ответу:


Стартовая доска:

Во-первых, для всеобщего блага представляю начальную неразрешенную доска. Это матрица 9x9, содержащая начальные числа и NaN везде.

unsolvedBoard = [
     5     3   NaN   NaN     7   NaN   NaN   NaN   NaN
     6   NaN   NaN     1     9     5   NaN   NaN   NaN
   NaN     9     8   NaN   NaN   NaN   NaN     6   NaN
     8   NaN   NaN   NaN     6   NaN   NaN   NaN     3
     4   NaN   NaN     8   NaN     3   NaN   NaN     1
     7   NaN   NaN   NaN     2   NaN   NaN   NaN     6
   NaN     6   NaN   NaN   NaN   NaN     2     8   NaN
   NaN   NaN   NaN     4     1     9   NaN   NaN     5
   NaN   NaN   NaN   NaN     8   NaN   NaN     7     9 ] ;

Начальные условия: Ваш алгоритм вслепую перебирал все 99 возможных блоков сетки. В формулировке задачи рекомендовалось идентифицировать пустые индексы в сетке (которые должны быть помещены в переменную emptyInd и выполнять итерацию только по этим пустым индексам благодаря переменной ind. Чтобы включить это, я изменил начало основного решателя:

function solvedBoard = solveSudoku(board)

    emptyInd = find(isnan(board)) ; % find the empty indices in the grid

    % this will solve the board recursively
    solvedBoard = solverec( board, emptyInd, 1 );

end

Теперь emptyInd содержит только 51 индекс, который нужно найти. Мы будем выполнять итерацию только по ним, а не по 99 ячейкам сетки.


Возможные числа для данного поля:

Ваша функция checkSudoku(board, row, col, num) работала отлично, но ее можно упростить. Вы уже конвертировали индексы строк и столбцов в линейные индексы. в своей функции h вы можете повторно использовать тот же тип вычислений в этой функции, чтобы узнать индексы subrow/subcol/subBoard. Также обратите внимание, что вы можете объединить условия if с логическими or для проверки всех условий сразу. Функция может выглядеть так:

function safe = checkSudoku(board, row, col, num)
    subrow = board(row, :);
    subcol = board(:, col);

    subSquareRow = (1:3) + 3*(ceil(row/3)-1) ; 
    subSquareCol = (1:3) + 3*(ceil(col/3)-1) ;

    subBoard = board( subSquareRow , subSquareCol );
    subBoard = subBoard(:) ; % Reshape into column vector (easier comparison)

    % This whole block can be replaced with the line described below
    if any(subrow == num) || any(subcol == num) || any(any(subBoard == num))
        safe = false;
    else
        safe = true;
    end

    % Note that since we are dealing with boolean, the "IF" check above could
    % be avoided and simply written as :

    % safe = ~( any(subrow == num) || any(subcol == num) || any(any(subBoard == num)) ) ;
end

Теперь эта функция позже используется в рекурсивном l oop, чтобы проверять, соответствует ли число от 1 до 9 действительно в данной позиции. Вы использовали while l oop для бега от 1 до 9. Я считаю бесполезным проверять девять чисел, когда мы могли с самого начала знать несколько возможных кандидатов для данного ящика. Итак, я написал функцию, которая возвращает список единственно возможных действительных номеров для коробки. Если он вернет только 3 возможных числа, мне нужно будет перебрать только эти 3 числа, вместо того, чтобы делать это вслепую более 9 из них.

function candidates = getCandidates(board, row, col)
    subrow = board(row, :);
    subcol = board(:, col);

    subSquareRow = (1:3) + 3*(ceil(row/3)-1) ; 
    subSquareCol = (1:3) + 3*(ceil(col/3)-1) ;

    subBoard = board( subSquareRow , subSquareCol );
    subBoard = subBoard(:) ; % Reshape into column vector (easier comparison)

    % Get the difference of each array compared to a reference line
    refval = 1:9 ;
    cdrow = setdiff(refval,subrow) ;
    cdcol = setdiff(refval,subcol) ;
    cdsqr = setdiff(refval,subBoard) ;

    % intersection of the three arrays
    candidates = intersect( intersect(cdrow,cdcol) , cdsqr ) ;
end

Вы можете прочитать setdiff и intersect, чтобы понять, как это работает.


Теперь рекурсивный решатель:

Эта функция выполняет работа вашей h() функции. В вашей реализации были две основные проблемы:

  • Слишком много условных ветвей: в потоке программы было слишком много if ветвей, и некоторые пути фактически никогда не использовались. Даже когда это работает, это сбивает с толку, но часто путаница также приводит к ошибкам.
  • Нет надежного условия для проверки, когда доска была полностью решена: у вас была проверка, но она не фиксировала завершение доски (частично из-за проблемы, описанной выше).

То, что произошло, - это когда ваша плата была полностью решена, алгоритм не имеет возможности обнаружить это и вернуться к вызову итеративной функции с конечным результатом. Ваш алгоритм находил решение, но из-за отсутствия выходной двери для этого случая (полностью решенного) он по умолчанию работал с другими ветвями и в конечном итоге последовательно возвращал последние несколько итераций, даже если они были правильными.

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

Код для solverec.m:

function [res, solved, noSolutionFound] = solverec(board,emptyInd,ind,solved)

%% initialise the return flag for first function call
if nargin < 4 ; solved  = false ; end

noSolutionFound = false ; % initialise second flag

% check if we are done with all the EmptyInd
if ind>numel(emptyInd) ;
    solved = true ;
end

%% Return quickly if the board is already solved
if solved
    res = board ;
    return ;
end

%% If we are here, we still have to find new emptyInd

% prepare useful indices (row, column & linear index)
num     = emptyInd(ind) ;
col     = ceil(num / 9);
row     = num - ((col - 1) * 9);

% get possible candidates for this box
cd  = getCandidates(board, row, col) ;
ncd = numel(cd) ;   % number of candidates

if ncd == 0
    % no candidate for this box => back track
    noSolutionFound = true ;
else
    % Try the possible candidates one by one
    for k=1:ncd ;
        board(num) = cd(k) ; % try one candidate
        % move on to next emptyInd
        [res, solved, noSolutionFound] = solverec(board,emptyInd,ind+1,solved) ;

        % bail out if solved
        if solved ; return ; end

        % otherwise, reset this emptyInd before trying next candidate
        if noSolutionFound
            board(num) = NaN ;
        end
    end
end

if noSolutionFound
    % We have exhausted all possible candidates for this emptyInd
    % We have to back track further
    board(num) = NaN ;
    res = board ;
    return  % this one is actually optional, the function will "return"
            % anyway at the end of the "if" block.
end
end

Тестирование:

>> solvedBoard = solveSudoku(unsolvedBoard)
solvedBoard =
     5     3     4     6     7     8     9     1     2
     6     7     2     1     9     5     3     4     8
     1     9     8     3     4     2     5     6     7
     8     5     9     7     6     1     4     2     3
     4     2     6     8     5     3     7     9     1
     7     1     3     9     2     4     8     5     6
     9     6     1     5     3     7     2     8     4
     2     8     7     4     1     9     6     3     5
     3     4     5     2     8     6     1     7     9

Я позволю вам написать необязательную функцию displaySudoku(board) в качестве упражнения;)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...