如何使用回溯算法使数独求解器回退?

3
这个周末我写了一个数独求解器(基于回溯算法)(Ruby quiz)。数独被加载到一个包含81个整数的数组sudoku_arr中(9x9网格),其中0表示空位。有一个valid?方法用于检查sudoku_arr是否是有效的数独。 官方回溯算法如下:尝试在下一个空位上放置数字,检查是否是有效的数独,如果不是,则将该位置的值增加1(最大到9),如果有效,则继续并尝试在下一个空位上放置第一个数字,如果无效,则将前一个0的值增加1。
因此,我们必须跟踪先前的数组,这就是我出错的地方,我不确定是否能够解决。下面是我代码中未能正常工作的部分:SudokuSolver类中的solve_by_increasing_value_previous_index。以下是代码:
require 'pp'

sudoku_str = "
+-------+-------+-------+
| _ 6 _ | 1 _ 4 | _ 5 _ |
| _ _ 8 | 3 _ 5 | 6 _ _ |
| 2 _ _ | _ _ _ | _ _ 1 |
+-------+-------+-------+
| 8 _ _ | 4 _ 7 | _ _ 6 |
| _ _ 6 | _ _ _ | 3 _ _ |
| 7 _ _ | 9 _ 1 | _ _ 4 |
+-------+-------+-------+
| 5 _ _ | _ _ _ | _ _ 2 |
| _ _ 7 | 2 _ 6 | 9 _ _ |
| _ 4 _ | 5 _ 8 | _ 7 _ |
+-------+-------+-------+"

class SudokuException < StandardError
  attr_reader :sudoku_arr
  def initialize(message, sudoku_arr)
    super(message)
    @sudoku_arr = sudoku_arr
  end
end

class Sudoku

  attr_accessor :sudoku_arr, 
                :index_of_tried_value,
                :tried_value

  def initialize(sudoku_arr, index_of_tried_value, tried_value)
    @sudoku_arr = sudoku_arr
    @index_of_tried_value = index_of_tried_value
    @tried_value = tried_value
  end

  def rows_valid?
    rows_have_unique_values?(@sudoku_arr)
  end

  def columns_valid?
    rows_have_unique_values?(@sudoku_arr.each_slice(9).to_a.transpose.flatten!)
  end

  def squares_valid?
    tmp_a = @sudoku_arr.each_slice(3).to_a

    rows_have_unique_values?(
    (tmp_a[0] << tmp_a[3]  << tmp_a[6]  << tmp_a[1]  << tmp_a[4]  << tmp_a[7]  <<
    tmp_a[2]  << tmp_a[5]  << tmp_a[8]  << tmp_a[9]  << tmp_a[12] << tmp_a[15] <<
    tmp_a[10] << tmp_a[13] << tmp_a[16] << tmp_a[11] << tmp_a[14] << tmp_a[17] <<
    tmp_a[18] << tmp_a[21] << tmp_a[24] << tmp_a[19] << tmp_a[22] << tmp_a[25] <<
    tmp_a[20] << tmp_a[23] << tmp_a[26]).flatten!)
  end

  def valid?
    rows_valid? && columns_valid? && squares_valid?
  end

  def rows_have_unique_values?(arr)
    (arr[0,9]- [0]).uniq.size == (arr[0,9]- [0]).size &&
    (arr[9,9]- [0]).uniq.size == (arr[9,9]- [0]).size && 
    (arr[18,9]-[0]).uniq.size == (arr[18,9]-[0]).size && 
    (arr[27,9]-[0]).uniq.size == (arr[27,9]-[0]).size && 
    (arr[36,9]-[0]).uniq.size == (arr[36,9]-[0]).size && 
    (arr[45,9]-[0]).uniq.size == (arr[45,9]-[0]).size && 
    (arr[54,9]-[0]).uniq.size == (arr[54,9]-[0]).size && 
    (arr[63,9]-[0]).uniq.size == (arr[63,9]-[0]).size && 
    (arr[72,9]-[0]).uniq.size == (arr[72,9]-[0]).size 
  end

end


class SudokuSolver

  attr_accessor :sudoku_arr, 
                :indeces_of_zeroes

  def initialize(str)
    @sudoku_arr = str.gsub(/[|\+\-\s]/,"").gsub(/_/,'0').split(//).map(&:to_i)
    @indeces_of_zeroes = []
    @sudoku_arr.each_with_index { |e,index| @indeces_of_zeroes << index if e.zero? }
  end

  def solve
    sudoku_arr = @sudoku_arr
    try_index = @indeces_of_zeroes[0]
    try_value = 1
    sudoku = Sudoku.new(sudoku_arr, try_index, try_value)
    solve_by_increasing_value(sudoku)
  end

  def solve_by_increasing_value(sudoku)

    if sudoku.tried_value < 10
      sudoku.sudoku_arr[sudoku.index_of_tried_value] = sudoku.tried_value
      if sudoku.valid?
        pp "increasing_index..."
        solve_by_increasing_index(sudoku)
      else
        pp "increasing_value..."
        sudoku.tried_value += 1
        solve_by_increasing_value(sudoku)
      end
    else
      pp "Increasing previous index..."
      solve_by_increasing_value_previous_index(sudoku)
    end
  end

  def solve_by_increasing_index(sudoku)
    if sudoku.sudoku_arr.index(0).nil? 
      raise SudokuException(sudoku.sudoku_arr.each_slice(9)), "Sudoku is solved."
    end

    sudoku.index_of_tried_value = sudoku.sudoku_arr.index(0)
    sudoku.tried_value = 1

    solve_by_increasing_value(sudoku)
  end

  def solve_by_increasing_value_previous_index(sudoku)
    # Find tried index and get the one before it
    tried_index = sudoku.index_of_tried_value
    previous_index = indeces_of_zeroes[indeces_of_zeroes.index(tried_index)-1]

    # Setup previous Sudoku so we can go back further if necessary:

    # Set tried index back to zero
    sudoku.sudoku_arr[tried_index] = 0
    # Set previous index
    sudoku.index_of_tried_value = previous_index
    # Set previous value at index
    sudoku.tried_value = sudoku.sudoku_arr[previous_index]
    pp previous_index
    pp sudoku.tried_value
    # TODO Throw exception if we go too far back (i.e., before first index) since Sudoku is unsolvable

    # Continue business as usual by increasing the value of the previous index
    solve_by_increasing_value(sudoku)
  end

end

sudoku_solver = SudokuSolver.new(sudoku_str)
sudoku_solver.solve

很不幸,代码没有回溯到开头。代码输出:
“increasing_index…” “increasing_value…” “increasing_value…” “increasing_value…” “increasing_value…” “increasing_value…” “increasing_value…” “increasing_value…” “increasing_value…” “Increasing previous index…” 16 2
在一个循环中,直到抛出SystemStackError,因为堆栈级别太深。
发生的情况是,“回溯”没有回溯到一个索引之前。当solve_by_increasing_value_previous_index转到上一个索引时,它会取那里的先前值。在这种情况下,它是2,但是2不起作用,因此我们应该将其减少到1并继续,如果不起作用,则丢弃2并将其重新设置为0,然后进一步返回。
不幸的是,我没有看到实现此算法的简单方法。(我想到的是一个@too_much_looping变量,当调用solve_by_increasing_value_previous_index时会递增,并在81次后重置。但这只有助于再次回退一次,我们无法回到开头。)
希望有人能提供帮助!对代码的一般评论也非常欢迎,我怀疑这不是100%习惯用的Ruby。
2个回答

1

我还没有深入研究你的代码,但回溯算法归结为以下内容:

int solve(char board[81]) {
    int i, c;
    if (!is_valid(board)) return 0;
    c = first_empty_cell(board);
    if (c<0) return 1; /* board is full */
    for (i=1; i<=9; i++) {
        board[c] = i;
        if (solve(board)) return 1;
    }
    board[c] = 0;
    return 0;
}

这是一个递归函数,它尝试在找到的第一个空单元格(由first_empty_cell()返回)中尝试从19的每个值。如果这些值中没有一个导致解决方案,则必须处于搜索树的死分支上,因此问题单元格可以重置为零(或您用于指示未填充单元格的任何值)。
当然,还有很多其他事情可以做,以使您的软件更快地到达解决方案,但就回溯而言,就是这样。

附录:

好的,我现在正在浏览你的代码。看起来你正在将index_of_tried_value存储为数独网格的属性。那样是不行的。你需要将这个值存储在求解器程序的局部变量中,这样它就可以被推入堆栈并在你回溯搜索树时恢复。


谢谢。是的。我的问题是,当将一个值重置为0后,如何进一步回退?我的意思是,你回到上一个值,然后增加它?或者等等...你继续第一个值,并将其增加1?!那似乎是对的...你如何记住你在分支中的位置?两个变量(索引和值)?/编辑,这不行,因为你不能丢弃整个分支...你应该返回并将前一个索引上的值增加1。如果那行不通,就再往前一个。你如何跟踪? - user2609980
@user2609980 所有操作都在堆栈上完成。行 board[c] = 0; 将当前单元格的值恢复为“未填充”,并将零(="未解决")的值返回到调用 solve()for 循环中,其中前一个未填充的单元格正在被考虑。如果前一个单元格的所有测试都失败,则前一个单元格也将重置为零,并尝试为其之前的单元格尝试新值。以此类推... - r3mainer
嗨,你的回答帮了我很多,让我找到了问题所在。我不认为问题是因为在数独上存储了index_of_tried_value,因为我只有一个实例。但问题是,在回溯时我没有将值增加一。通过添加+1,代码似乎可以工作:# 在索引处设置先前的值 sudoku.tried_value = sudoku.sudoku_arr[previous_index] + 1,不幸的是,它无法完成数独的最后部分,因为出现了“堆栈层级过深错误”,你能看到解决这个问题的方法吗?PS我接受你的答案,因为你基本上已经回答了它。:-)谢谢。 - user2609980
1
如果你正确地使用回溯法,你的调用栈最多只能达到81层深度,并且永远不会溢出。听起来你仍在使用全局变量来存储搜索树中的位置,这是错误的。如果你想让它正常工作,你必须在搜索例程内部使用一个局部变量(就像上面示例代码中的i一样)。(好吧,除非你创建一个由81个元素组成的数组来存储每个单元格的搜索进度,我想。但为什么要费那个劲呢?) - r3mainer
使用您的简单算法,它可以工作!我看到了SudokuException。 :-) - user2609980

1
有一种名为“Dancing links”的算法,由Knuth发明,可以应用于数独。 http://en.wikipedia.org/wiki/Dancing_Links 数独问题可以通过精确覆盖问题来解决。以下是文章链接。 http://www-cs-faculty.stanford.edu/~uno/papers/dancing-color.ps.gz 这是我解决精确覆盖问题的代码,也可以解决数独问题,回溯部分在dlx()函数中。
const int INF = 0x3f3f3f3f;
const int T = 9;
const int N = T*T*T+10;
const int M = T*T*T*4+T*T*4+10;
int id;
int L[M],R[M],U[M],D[M];
int ANS[N],SUM[N],COL[M],ROW[M],H[N];


struct Dancing_links
{
    Dancing_links() {}
    Dancing_links(int n,int m)
    {
        for(int i=0; i<=m; i++)
        {
            SUM[i] = 0;
            L[i+1] = D[i] = U[i] = i;
            R[i]=i+1;
        }
        L[m+1]=R[m]=0,L[0]=m,id=m+1;
        clr(H,-1);
    }

    void remove(int c)
    {
        L[R[c]] = L[c];
        R[L[c]] = R[c];
        for(int i=D[c]; i!=c; i=D[i])
            for(int j=R[i]; j!=i; j=R[j])
            {
                U[D[j]] = U[j];
                D[U[j]] = D[j];
                SUM[COL[j]]--;
            }
    }

    void resume(int c)
    {
        for(int i=U[c]; i!=c; i=U[i])
            for(int j=L[i]; j!=i; j=L[j])
            {
                U[D[j]] = D[U[j]] = j;
                SUM[COL[j]]++;
            }
        L[R[c]] = R[L[c]] = c;
    }

    void add(int r,int c)
    {
        ROW[id] = r,COL[id] = c;
        SUM[c]++;
        D[id] = D[c],U[D[c]] = id,U[id] = c,D[c] = id;

        if(H[r] < 0)
            H[r] = L[id] = R[id] = id;
        else
            R[id] = R[H[r]],L[R[H[r]]] = id,L[id] = H[r],R[H[r]] = id;
        id++;
    }

    int dlx(int k)
    {
        if(R[0] == 0)
        {
            /*output the answer*/return k;
        }

        int s=INF,c;
        for(int i=R[0]; i; i=R[i])
            if(SUM[i] < s)
                s=SUM[c=i];

        remove(c);
        for(int r=D[c]; r!=c; r=D[r])
        {
            ANS[k] = ROW[r];
            for(int j=R[r]; j!=r; j=R[j])   remove(COL[j]);

            int tmp = dlx(k+1);
            if(tmp != -1) return tmp; //delete if multipal answer is needed

            for(int j=L[r]; j!=r; j=L[j])   resume(COL[j]);
        }
        resume(c);
        return -1;
    }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接