用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()成员函数中的效率,如果可以让它更智能地返回最佳的下一步状态,也即让状态树中的下一层结点分支最少,就应该可以有效地改善算法的效率。