使用回溯法递归地解决数独难题的理论方法

4
回答:我的伪代码在递归方面有些模糊,但是这个视频和下面的资源对我很有帮助。

http://www.youtube.com/watch?v=p-gpaIGRCQI

http://norvig.com/sudoku.html

无法理解这个回溯递归算法在数独谜题中的实现。 我正在尝试使用递归回溯来解决数独谜题。然而,我仍然无法理解如何将通用算法应用于我的问题领域。
我尝试使用标准的回溯算法,但是我无法理解其中的逻辑和底层操作。
以下是回溯算法及其定义。
编辑:已删除类定义,只保留声明并提供伪代码。
以下是我使用此算法的伪代码。
伪代码(C++实现) backtrack game(81,9) // 表示游戏的所有可能输入和值的组合
    //All info is loading into a vector of size 81 with the initial state 
    //puzzle = the initial state 9x9 grid from left to right of integers  
        vector <int> puzzle
while(!not solved && not the end of the vector){
     for(int i =puzzle.begin::iterator i , puzzle.end()) //from 0-80 of the vector until end
        if puzzle[i] != 0
             //leave alone, original state of board
        else
              if (valid move) //a guess is allowed in this column/row/square of the board  
                   solution[i] = puzzle_guess[i]  //guess a move and insert 
              else  // one of previous guesses were wrong
                   game.prune(i); //backtracks, or reverses guesses until valid move
}

//游戏的初始状态

    4 0 0  6 0 5  2 0 3
    0 0 0  0 4 9  0 7 5
    0 0 0  1 0 7  6 0 0

    6 0 1  0 0 0  4 8 7
    0 8 0  0 0 0  0 3 0
    2 7 4  0 0 0  5 0 6

    0 0 8  7 0 3  0 0 0
    3 1 0  9 6 0  0 0 0
    7 0 9  2 0 8  0 0 1

谢谢你。
唯一的线索是使用回溯游戏声明(81,9) //表示81个可能数字和9个不同选项。
#ifndef BACKTRACK_H
#define BACKTRACK_H

#include <vector>
#include <algorithm>

class BackTrack {
public:
  typedef std::vector<unsigned>::const_iterator const_iterator;
  typedef std::vector<unsigned>::const_iterator iterator;

  BackTrack (unsigned nVariables, unsigned arity=2);
  // Create a backtracking state for a problem with
  // nVariables variables, each of which has the same
  // number of possible values (arity).

  template <class Iterator>
  BackTrack (Iterator arityBegin,
         Iterator arityEnd);
  // Create a backtracking state in which each variable may have
  // a different number of possible values. The values are obtained
  // as integers stored in positions arityBegin .. arityEnd as per
  // the usual conventions for C++ iterators. The number of
  // variables in the system are inferred from the number of
  // positions in the given range.

  unsigned operator[] (unsigned variableNumber) const;
  // Returns the current value associated with the indicated
  // variable.

  unsigned numberOfVariables() const;
  // Returns the number of variables in the backtracking system.

  unsigned arity (unsigned variableNumber) const;
  // Returns the number of potential values that can be assigned
  // to the indicated variable.

  bool more() const;
  // Indicates whether additional candidate solutions exist that
  // can be reached by subsequent ++ or prune operaations.

  void prune (unsigned level);
  // Indicates that the combination of values associated with
  // variables 0 .. level-1 (inclusive) has been judged unacceptable
  // (regardless of the values that could be given to variables
  // level..numberOfVariables()-1.  The backtracking state will advance
  // to the next solution in which at least one of the values in the
  // variables 0..level-1 will have changed.

  BackTrack& operator++();
  // Indicates that the combination of values associated with
  // variables 0 .. nVariables-1 (inclusive) has been judged unacceptable.
  // The backtracking state will advance
  // to the next solution in which at least one of the values in the
  // variables 0..level-1 will have changed.

  BackTrack operator++(int);
  // Same as other operator++, but returns a copy of the old backtrack state


  // Iterator operations for easy access to the currently assigned values
  const_iterator begin() const {return values.begin();}
  iterator begin()             {return values.begin();}

  const_iterator end() const {return values.end();}
  iterator       end()       {return values.end();}

private:
  bool done;
  std::vector<unsigned> arities;
  std::vector<unsigned> values;

};
inline
unsigned BackTrack::operator[] (unsigned variableNumber) const
  // Returns the current value associated with the indicated
  // variable.
{
  return values[variableNumber];
}

inline
unsigned BackTrack::numberOfVariables() const
  // Returns the number of variables in the backtracking system.
{
  return values.size();
}

inline
unsigned BackTrack::arity (unsigned variableNumber) const
  // Returns the number of potential values that can be assigned
  // to the indicated variable.
{
  return arities[variableNumber];
}


inline
bool BackTrack::more() const
  // Indicates whether additional candidate solutions exist that
  // can be reached by subsequent ++ or prune operaations.
{
  return !done;
}

template <class Iterator>
BackTrack::BackTrack (Iterator arityBegin,
              Iterator arityEnd):
  // Create a backtracking state in which each variable may have
  // a different number of possible values. The values are obtained
  // as integers stored in positions arityBegin .. arityEnd as per
  // the usual conventions for C++ iterators. The number of
  // variables in the system are inferred from the number of
  // positions in the given range.
  arities(arityBegin, arityEnd), done(false) 
{
  fill_n (back_inserter(values), arities.size(), 0);
}


#endif

2
@MitchWheat 你认为他写了这段代码吗?显然他只是从教程中复制粘贴了过来。 - user2638922
@MitchWheat 我并不声称自己写出了这个算法。没有任何相关知识的情况下写出一个有效的算法是相当不容易的 :).这里有一份简略的幻灯片,其中包含一个半相关、更详细解释的幻灯片。 http://see.stanford.edu/materials/icspacs106b/Lecture11.pdf我承认我的伪代码在关于这个通用算法方面可能存在错误。 - Bjarn127
1
从问题中删除C++代码。它与您无关,也不会帮助您。您的问题在于算法,而不是特定的实现。请集中精力解决前者。 - n. m.
1
@Bjarn127 别忘了斯坦福荣誉准则。 - Marichyasana
1
我觉得我没有表达清楚。让我再试一次。伪代码中存在问题,这表明您并不真正理解算法。基于一个简单但被误解的算法来讨论相当复杂的C++代码是毫无意义的。现在您已经删除了伪代码,而它本可以作为讨论的有用基础,因为它很小且易于理解,却留下了代码。你应该做完全相反的事情。 - n. m.
显示剩余10条评论
2个回答

8

这里有一个简单的伪代码,可以帮助您理解递归和回溯:

solve(game):
    if (game board is full)
        return SUCCESS
    else
        next_square = getNextEmptySquare()
        for each value that can legally be put in next_square
            put value in next_square (i.e. modify game state)
            if (solve(game)) return SUCCESS
            remove value from next_square (i.e. backtrack to a previous state)
    return FAILURE

一旦您理解了这一点,下一步就是要了解如何通过以不同方式修剪状态空间图来理解getNextEmptySquare()的各种实现对性能的影响。
我在您原始伪代码中没有看到任何递归或有条不紊的搜索,尽管并不完全清楚,但似乎只是不断尝试随机排列?

嗨@The111,该算法使用第二个参数arity来跟踪可能的值。我认为它使用逻辑从左到右的解决方法,从0-9解决,并将有效值放置在每个相应的网格插槽中。如果发现网格位置6无效,则会回滚或“回溯”。就算法而言,我猜它会阻止arity中的“不可接受的值”,并返回到先前使用有效arity的状态。这个状态由“回溯类”保持。感谢您的回复! - Bjarn127

2

Sudoku的关键在于它有大量的状态:9^81是一个78位数。因此,任何从左上角开始并向右下角进行处理的“愚蠢”回溯算法都可能会陷入看似“无尽”的循环。

因此,我的建议是像人类一样解决数独问题:找到一个只能填入一个数字的区域,并将该数字填入该区域。然后寻找下一个这样的区域。如果没有更多只能填入一个数字的空区域存在,则查找允许最多两个(或通常为可能的最小数量)数字的区域,并尝试其中一个值,并继续递归。如果出现任何矛盾,请回溯,然后尝试另一个具有多个替代方案的区域的值。

伪代码如下:

solve(game)
    if (game->is_solved())
        game->print()
        return SUCCESS
    else
        next_square = game->find_most_constrained_square()
        foreach value = next_square->possible_values
            mygame = copyof(game)
            mygame->move(next_square, value)
            if solve(mygame) return SUCCESS
        endforeach
        return FAILURE
    endif

函数find_most_constrained_square()会计算每个空白格子还可以填入多少不同的数字,并返回一个索引值,这个索引值对应的空白格子是可选数字最少的。这个索引值甚至可能对应一个没有可选数字的格子。

通过这种修改后的递归方法,即使在速度较慢的语言和计算机上,数独问题也能够快速解决。别忘了在foreach内部循环中丢弃各种游戏状态的副本!


只要在递归调用后“撤消”移动,就可以重复使用相同的游戏副本。这样可以节省制作如此多副本的时间和空间。 - The111
在数独中,“撤销”一步并不是一个简单的操作,特别是如果为每个单元格保存了额外的状态。例如,可以使用一个位域来表示每个单元格仍然可能的值。在这种情况下,将某个值设置到某个单元格意味着从同一行、列或组中的其他单元格的合法值位域中删除该值。但是,在回溯移动后重新设置这些位可能是错误的,因为特定的值可能已经被禁止用于某些其他先前设置的字段。因此,我建议使用“复制”而不是“撤销”。 - Kai Petzke

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