回溯法解N皇后问题
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”到前一个结点,尝试别的路径。
回溯法主要包括两种形式:子集树和排列树。
- 子集树概念:当所给问题是从n个元素的集合S中找出S满足的某种性质的子集时,相应的解空间树称为子集树。例如,0-1背包问题,要求在n个物品的集合S中,选出几个物品,使物品在背包容积C的限制下,总价值最大(即集合S的满足条件<容积C下价值最大>的某个子集)。
另:子集树是从集合S中选出符合限定条件的子集,故每个集合元素只需判断是否(0,1)入选,因此解空间应是一颗满二叉树。 - 排列树概念:当问题是确定n个元素满足某种性质的排列时,相应的解空间称为排列树。排列树与子集树最大的区别在于,排列树的解包括整个集合S的元素,而子集树的解则只包括符合条件的集合S的子集。
理解不了两种形式的朋友请仔细阅读参考文献一,笔者认为,根结点到每个叶结点都是一条路径,子集树问题是求一条条路径中的部分结点,排列树就是求满足条件的一条条路径。
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯,相当于走另外的路径了。始终记住,路径是由一个个结点构成的。
回溯法有典型的代码框架,参考文献一中有,下面列出的框架是从labuladong文章(参考文献二)中看到的代码框架,个人认为思路很清晰:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
在for中还可以用if剪枝,跳过不合法的选项,下面八皇后的C++代码中有体现。
vector<vector<string>> res;
//判断同一行、主对角线(左上方)、副对角线(右上方)是否已经存在皇后。
//由于每一次递归调用传入的列不同,所以不需要判断同一列存在皇后。
//每次递归调用只检查row行前是否冲突,所以第row行下方不用判断。
bool isValid(vector<string>& board, int row, int col)
{
int n = board.size();
//检测列是否有皇后冲突
for (int i = 0; i < n; i++)
{
if (board[i][col] == 'Q')
return false;
}
//检测右上方
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++)
{
if (board[i][j] == 'Q')
return false;
}
//检测左上方
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--)
{
if (board[i][j] == 'Q')
return false;
}
return true;
}
//路径:board中小于row的那些行都已经成功放置了皇后
//选择列表:第row行的所有列都是放置皇后的选择
//结束条件:row超过board的最后一行
void backtrack(vector<string>& board, int row)
{
if (row == board.size())//8皇后问题中,row=8时是第9行,此时前8行都放好了皇后,就可以把结果保存起来了。
{
res.push_back(board);
return;//由于if里有return,所以下面不用else。
}
int n = board[row].size();
for (int col = 0; col < n; col++)
{
//排除不合法的选择,
if (!isValid(board, row, col))
continue;
//做选择
board[row][col] = 'Q';
//进入下一层决策树
backtrack(board, row + 1);
//取消选择
board[row][col] = '.';
}
}
vector<vector<string>> solveNQueens(int n)
{
//初始化board为二维容器大小为n*n,都初始化为'.'
vector<string> board(n, string(n, '.'));
backtrack(board, 0);
return res;
}
参考文献:
回溯法实例详解
回溯算法详解(修订版)