用STL实现先深搜索及先宽搜索——数独(sudoku)例子(1)

用STL实现先深搜索及先宽搜索
——数独 (sudoku) 例子 (1)
前面我们用STL容器实现了简单的DFS和BFS算法,为了检查它们的有效性,我们选择一个游戏——数独sudoku,先来实作出一个简单(但效率较差)的解法,测试了一下这两个DFS和BFS算法。
考虑到可能有些朋友还不太了解这个数独sudoku,所以我先来简单地介绍一下这个游戏的规则。如下图所示:

左边是一条典型的3x3数独问题,而右边则是该题的答案。数独的规则很简单,3x3的数独题就是一个9x9的方阵,方阵中的每一个格子可以填入1-9的数字,要求是:每行不能有重复的数字,每列也不能有重复的数字,而且9x9的方阵再划分为9个3x3的小方阵,要求每个小方阵中也不能有重复的数字。按照正常的数独规则,每个问题必须只能有一个答案,不过这个规则与我们的搜索算法无关,我们可以不理睬问题是否无解或有多个解,只管搜索就是了(前面的文章也介绍过,我们的DFS和BFS算法中有一个模板参数,可以由使用者来决定只找出一个解或是找出所有解)。
现在开始来实作这个数独程序。前面给出的DFS和BFS算法是一个泛型算法,它需要我们针对不同的具体问题,提供两个东西,然后搜索算法才可以得以执行。
首先是一个表示问题状态的类,在这个例子中,就是要设计一个表示数独问题状态的类。最简单的想法就是用一个二维数组来表示数独问题的一个状态,对于3x3数独问题,我们用一个9x9的二维数组来表示问题状态,数组中的每个元素取值为0-9,其中0表示空格,1-9表示相应已固定的数字。根据我们的DFS和BFS算法要求,这个类需要提供两个成员函数:void nextStep(vector&)和bool isTarget();前者以自身状态为起点,返回所有可能的下一步状态,由于可能的下一步状态数量不定,所以需要用vector&来返回;后一个成员函数则比较简单,返回一个bool值来判断自身状态是否满足解答条件。除此之外,我还准备为这个类增加流输入和流输出函数,用以读入问题和输出答案。所以,这个类的定义如下:
class SudokuState
{
public:
 SudokuState() {}
 void nextStep(vector&) const;
 bool isTarget() const;
 
 friend ostream& operator<< (ostream& os, const SudokuState& s);
 friend istream& operator>> (istream& is, SudokuState& s);
 
private:
 int data_[SUDOKU_DIMS][SUDOKU_DIMS];
 ...
};
 
我们先从简单的bool isTarget()着手。这个很简单,只要对二维数组中的所有元素进行逐个检查,如果全部不为0,则表示方阵中的所有格子已经填满,即已经完成了解答。当然,我们本应在isTarget()中检查这些数字是否满足数独的规则要求,不过,我们可以通过在nextStep()中确保填入的每个数字满足规则来确保这一点,因而在isTarget()中我们就可以省略这一检查了。isTarget()如下:
bool SudokuState::isTarget() const
{
 for (int row = 0; row < SUDOKU_DIMS; ++row)
 {
      for (int col = 0; col < SUDOKU_DIMS; ++col)
      {
          if (data_[row][col] == 0)
          {
              return false;
          }
      }
 }
 return true;
}
 
nextStep()是算法的核心,所以相对复杂一些,不过我们也可以有所选择。我首先选择的是实作一个简单的算法,当然它的效率会相对低一些,我决定把对算法的优化延后处理。这个简单的算法就是:扫描表示问题状态的二维数组,当发现第一个空格时,就针对这个空格找出全部可供选择的数字(即与空格所在行、列、小方阵中已有的数字均不重复),每一个可选的数字代当前问题状态的一个下一步可选状态。按照DFS和BFS算法的要求,我们把所有可选的下一步状态插入作为参数传入的vector对象中返回给DFS和BFS。
void SudokuState::nextStep(vector& vs) const
{
    SudokuState newState;
    bool pos[SUDOKU_DIMS];
    for (int row = 0; row < SUDOKU_DIMS; ++row)
    {
        for (int col = 0; col < SUDOKU_DIMS; ++col)
        {
            if (data_[row][col] == 0) // 有一个空格,找出其可能填入的值
            {
                fill_n(pos, SUDOKU_DIMS, true);
                for (int i = 0; i < SUDOKU_DIMS; ++i) // 排除同行、同列的值
                {
                    checkValue(pos, data_[i][col]);
                    checkValue(pos, data_[row][i]);
                }
                int rs = row - (row % SUDOKU_ROWS); // 排除同一小方阵的值
                int cs = col - (col % SUDOKU_COLS);
                for (int i = 0; i < SUDOKU_ROWS; ++i)
                {
                    for (int j = 0; j < SUDOKU_COLS; ++j)
                    {
                        checkValue(pos, data_[rs+i][cs+j]);
                    }
                }
                for (int i = 0; i < SUDOKU_DIMS; ++i)
                {
                    if (pos[i]) // 找到一个可能的候选值
                    {
                        newState = *this;
                        newState.data_[row][col] = i+1; // 把候选值填入空格
                        vs.push_back(newState); // 作为下一步的可选状态
                    }
                }
                return; // 每步只处理一个空格
            }
        }
    }
}
 
在nextStep()中,我们定义了一个bool数组pos[],它用于记录空格所在的行、列、小方阵中已经有哪些数字,只有那些不重复的数字才可以填入空格。函数中调用了另一个成员函数checkValue(),它用于将pos数组中对应已有数字的bool值置为false,由于它很简短,我把它放在类的定义中。
class SudokuState
{
public:
...
private:
    static void checkValue(bool* pos, int i)
    {
        if (i != 0)
        {
            pos[i-1] = false;
        }
    }
};
 
流输入和流输出函数也很简单,只需如常从流中读入或向流输出二维数组中的元素即可,如下:
ostream& operator<< (ostream& os, const SudokuState& s)
{
    for (int i = 0; i < SUDOKU_DIMS; ++i)
    {
        for (int j = 0; j < SUDOKU_DIMS; ++j)
        {
            os << s.data_[i][j] << " ";
        }
        os << endl;
    }
    return os;
}
 
istream& operator>> (istream& is, SudokuState& s)
{
    int v;
    for (int i = 0; i < SUDOKU_DIMS; ++i)
    {
        for (int j = 0; j < SUDOKU_DIMS; ++j)
        {
            is >> v;
            s.data_[i][j] = (v < 1 || v > SUDOKU_DIMS) ? 0 : v;
        }
    }
    return is;
}
 
至此,问题状态类SudokuState的各组成部分已经全部列出。我们在其中看到有三个常量,它们的定义如下:
const int SUDOKU_ROWS = 3;
const int SUDOKU_COLS = 3;
const int SUDOKU_DIMS = SUDOKU_ROWS * SUDOKU_COLS;
数独游戏可以多种不同的大小,有2x2、3x3、4x4、5x5的,也有2x3、3x4、4x5的,所以我们用两个常量来表示数独游戏的大小,这样就可以很容易地把这个程序用于解决其它大小的数独游戏。
 
DFS和BFS算法需要的第二个东西是一个函数指针或函数对象,它接受一个const T1&参数,并返回一个bool值,传入的参数为搜索算法找到的一个答案状态,函数可以按自己的方法处理它(我们将打印它),然后返回一个bool值来表示是否继续搜索其它答案(我们只想找出一个答案就好了,所以返回了true)。由于我们的处理方法比较简单,我就直接使用传统的函数指针好了,如果你有兴趣用函数对象,我想也不是什么问题。
bool PrintResult(const SudokuState& r)
{
 cout << "Solution : /n" << r << endl;
 return true;
}
你看,由于我们的SudokuState类已经提供了流输出函数,所以打印它非常简单。如果你想找到所有答案,那么你把函数的返回值改为false好了。
 
DFS和BFS算法所需要的二样东西我们都准备好了,现在是时候写主函数了。如你所想,主函数也没有几行,它首先从cin读入一个数独问题,然后调用DFS或BFS来搜索答案,如果找到则给出所有答案以及答案的总数,就是这么多了:
int main()
{
    SudokuState start;
    cin >> start;
    cout << "Problem :/n" << start << endl;
 
    int n = DepthFirstSearch(start, &PrintResult);
    //int n = BreadthFirstSearch(start, &PrintResult);
    if (n == 0)
    {
        cout << "Solution not found!/n";
    } else {
        cout << n << " solution(s) found!/n";
    }
 
    return 0;
}
 
你可以看到,我们可以选择使用DFS还是BFS,它们都可以找到正确的答案。同时,由于我们的结果处理函数PrintResult返回了true,它表示找到一个答案就停止搜索,你会发现对于多数数独问题,通常DFS会比BFS快一些。这只能说明对于数独这个具体问题,DFS的平均效率比BFS更好些。
如果你把PrintResult改为返回false,它表示要求找出所有答案,即需要完成整棵问题状态树的搜索,这样的话无论是DFS还是BFS,它们的搜索量是一样,所以它们的速度就会不相上下。
前面也说过,这是我第一次尝试这个DFS/BFS算法,为简单起见,我采用了最简单的解法而暂不考虑效率。下一次,我将尝试改进一下数独问题解法的效率。注意,不是DFS/BFS搜索算法的效率,由于它们是针对通用问题的,我认为可以改进的地方已不多。我要改进的是SudokuState::nextStep()成员函数中的效率,如果可以让它更智能地返回最佳的下一步状态,也即让状态树中的下一层结点分支最少,就应该可以有效地改善算法的效率。
 

你可能感兴趣的:(算法,游戏,os,vector,class,优化)