【问题标题】:Sudoku solver recursive backtracking not terminating数独求解器递归回溯未终止
【发布时间】:2020-05-27 09:53:52
【问题描述】:

我编写了一个 MATLAB 程序,该程序使用递归回溯解决方案解决了 9 x 9 数独难题,但递归似乎没有终止。当我暂停调试器并查看电路板时,我发现我的电路板已经包含正确的解决方案。在我的方法中,我逐列处理板元素,从 (1, 1) 中的元素 1 开始,到 (9, 9) 中的元素 81 结束。 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

我从 MITOpenCourseWare,作业 3 可选问题 3 中拿了题目和 MATLAB 文件。文件和照片可以在here找到。

【问题讨论】:

  • board 变量是什么样的?
  • 您好,该板是 9 x 9 矩阵。我从 MITOpenCourseWare,作业 3 可选问题 3 中获取了问题和 MATLAB 文件。文件和照片可以在此链接中找到 ocw.mit.edu/courses/electrical-engineering-and-computer-science/…
  • 无法附加图片,因为没有足够的信誉点:p
  • 这是因为即使板子完成了,你也不会检查它或有一个标志来表示它,所以递归永远不会停止。应始终编写递归例程以实现平滑返回。如果您需要返回一个 FoundIt 标志来告诉调用例程返回,那么就这样做。
  • Matlab 的新手,所以我不太确定 return 关键字的使用方式,但这是否意味着我应该在最顶部添加一条 if 语句来检查整个板子是否已填满,有效(终止条件),如果是,写'return'关键字?我要终止的基本情况实际上是(如果 num >= 82),但它似乎没有正确终止。

标签: matlab sudoku


【解决方案1】:

即使在简单的情况下,递归函数也可能难以抽象。您的案例具有额外的复杂性,因为除了必须根据先前的迭代计算事物之外,该算法还应该能够在继续前进之前回溯一定数量的迭代。 p>

我做了一个工作示例,但这不是实现结果的唯一方法。我建议使用两个 flags 的方式来帮助递归函数知道它的前进方向。您可以不使用标志,但它需要在函数期间进行更多检查以评估董事会的状态。由于可以使用标志,因此我使用它来简化。

我强烈建议您阅读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 个可能的框进行盲目迭代。问题陈述建议您识别网格中的 empty 索引(放置在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

现在此函数稍后在递归循环中用于检查从19 的数字在给定位置是否有效。您使用 while 循环从 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

您可以阅读 setdiffintersect 以了解其工作原理。


现在是递归求解器:

此函数正在完成您的 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) 函数作为练习;)

【讨论】:

    最近更新 更多