回溯法在生活中其实是十分常见的,比如在9*9数独游戏中,我们根据同行、同列,同块中不能出现重复数字的原则补充未完成的表格,当有依据地推理出所有可以填写的空格时,经常会遇到某些不确定的格子,这些格子总是具有多选1的情况,此时,我们需要首先代入一个选项,并继续推理下去,当同行同列不重复的原则出现矛盾时,我们就可以发现我们之前代入的选项是错的,由此排除,并代入其他可能的选项继续尝试,这就是一次回溯的过程。
算法概念
回溯法是一种选优搜索法,又称为试探法,主要流程为按照某种约束或条件逐步执行搜索过程,从而达到我们所需的目标,当约束无法满足或目标无法达成时,就退回到某一步重新选择,也就是回溯的过程。
回溯法的核心在于约束、行为、目标三个方面。比如,在前文提到的数独游戏中,“同列同行同块不重复”的原则就是回溯法的约束,“下一个空白格子中可以填写1-9的数字”就是搜索的行为,“填满整张表格”就是我们要达到的目标,每次我们填写一个格子就是一次搜索过程。
回溯法的本质是递归,之所以能够使用递归就是因为回溯法的每一步搜索都是向前的,且遵循相同的搜索原则,同时达到目标或目标无法达成的场景完成了对递归过程结束的约束。由此,只要我们将一个问题判定为可回溯的问题,我们就能够很快地写出回溯算法的实现。
算法应用公式
回溯法 = 行为(逐个xxxxx) + 约束(xxx应该xxxx) + 目标(最终xxxx)
回溯代码 = 约束检查函数 + 目标截止的行为递归函数
经典应用场景
八皇后问题
问题场景
八皇后问题是回溯法的经典场景,问题描述如下:在8*8的国际象棋棋盘上摆放八个皇后,使其不能互相攻击,也就是说任意两个皇后都不能出现在同一行、同一列、同一斜线上,有多少种摆法?(92)
解法
前文提到,回溯法的核心在于约束、搜索方向、目标三方面。
在八皇后问题中,由于每行每列不能有超过1个皇后,而8个皇后想要放在8行的棋盘中,则每行有且只有一个皇后,所以我们只要逐一确定每一行皇后应该摆放在哪一列,当所有皇后均摆放完成,我们便可以得到摆放情况了。
由此,我们可以得到如下的信息:对于八皇后问题,约束就是“每一行、列、斜线上有且只有1个皇后”,行为就是“确定下一行放皇后的位置”,目标则是“完成8枚棋子的摆放”,每次达到目标后统计达成次数即可。
代码编写思路
我们一步步来讲解如何完成代码编写:
A. 约束检查函数 check
第一步选择约束部分编写,是因为约束部分通常包含问题中所需的各种关键值,可以帮助我们尽早确定需要的变量。八皇后问题中,约束为
- 同行只有一个皇后
- 同列只有一个皇后
- 同斜线最多只有一个皇后
我们的行为是逐行确定皇后位置,所以每一行一定只有一个皇后,易知约束函数首先需要的两个参数是行号int line
和列号int row
;此时,为了检查第二条约束,我们需要判断当前列与其之前所有行上是否有重复的皇后,所以,我们还需要一个int board[8]
来保存每一行中皇后的列号;已有列号数据,检查第三条约束自然也是很简单
inline bool check(int line, int row, int board[]){ // 约束
for(int i = 0; i < line; ++i){
int existRow = board[i];
if(existRow == row){ // 列是否重复
return false;
}
else if(line-i == abs(row-existRow)){ // 斜线是否重复
return false;
}
}
return true;
}
B. 行为递归函数 placeQueen
在参数设定上,首先,八皇后问题的行为是逐行确定皇后的位置,所以,要有一个参数存储当前搜索到的行号;其次,综合约束函数,还需要board[8]
存储列信息;最后,题目要求是计算出方案数量,我们还需要一个num
用来统计结果数。
作为一个递归函数,placeQueen需要有一个截止设定,也就是我们的目标——完成所有皇后拜访,也就是说,当前行号要大于棋盘行号
if(line > 7){ // 目标
num++;
cout << "right!" << endl;
return ;
}
行为中的主要环节就是确定这一行中可以摆放的位置,所以对当前行的8个位置逐一进行约束检查,满足则允许摆放,并进入下一行。
注意:当允许摆放时,board[8]
的对应值要修改,操作完成后为了防止偏差要修改回来
for(int i = 0; i < 8; ++i){
if(check(line, i, board)){
board[line] = i;
placeQueen(line+1, board, num); // 行为
board[line] = 8;
}
}
代码
#include <iostream>
#include <cmath>
using namespace std;
inline bool check(int line, int row, int board[]){ // 约束
for(int i = 0; i < line; ++i){
int existRow = board[i];
if(existRow == row){
return false;
}
else if(line-i == abs(row-existRow)){
return false;
}
}
return true;
}
void placeQueen(int line, int board[], int& num){
if(line > 7){ // 目标
num++;
return ;
}
for(int i = 0; i < 8; ++i){
if(check(line, i, board)){
board[line] = i;
placeQueen(line+1, board, num); // 行为
board[line] = 8;
}
}
}
int main(){
int num = 0;
int board[8];
for(int i = 0; i < 8; ++i){
board[i] = 0;
}
placeQueen(0, board, num);
cout << num << endl;
return 0;
}
数独问题
问题场景
一个大的99表格是由9个33的方格组合而成的。对于每一个33的表格,都各自需要填入1-9这9个不同的数,同时使得这99的表格的每一行以及每一列上没有重复的数字,已知方格的一部分,请将表格填满。
解法
同理,我们还是要分析数独问题的三个方面,填写数独时,我们通常会是不断用排除法填写确定的格子,不确定的格子再依次尝试可能的选项,对于代码实现我们当然不可以这样做,而是对于81个格子按照顺序依次进行填写,如果已有预先给定的值则继续处理下一个,当我们发现某个尝试的值产生矛盾时,回溯到最近的回溯点。三个要素的含义前文已经给出,这里对其进行进一步说明:
- 约束:同列、同行、同块的9个格子均由1-9这9个数字构成
- 行为:对每一行的每一个格子进行赋值,一行完成后从下一行的第一个开始,如果该格子已有数据则选择跳过
- 目标:最后右下角的格子已经有数据填充
代码
这里直接给出代码
#include <iostream>
using namespace std;
bool check(int line, int row, int value, int board[][9]){
for(int i = 0; i < 9; ++i){
if(board[line][i] == value){
return false;
}
if(board[i][row] == value){
return false;
}
}
line /= 3;
row /= 3;
for(int i = line*3; i < line*3+3; ++i){
for(int j = row*3; j < row*3+3; ++j){
if (board[i][j] == value){
return false;
}
}
}
return true;
}
void printSudoku(int board[][9]){
for(int i = 0; i < 9; ++i){
for(int j = 0; j < 9; ++j){
cout << board[i][j] << " ";
}
cout << endl;
}
cout << "success" << endl;
}
void setNum(int line, int row, int board[][9]){
if(line>8){
printSudoku(board);
return;
}
bool succ = false;
if(board[line][row] == 0){
for(int i = 1; i < 10; ++i){
if(check(line, row, i, board)){
board[line][row] = i;
if(row < 8){
setNum(line, row+1, board);
}
else{
setNum(line+1, 0, board);
}
board[line][row] = 0;
}
}
}
else{
if(row < 8){
setNum(line, row+1, board);
}
else{
setNum(line+1, 0, board);
}
}
}
int main(){
int board[9][9] = {
{3, 0, 8, 0, 0, 0, 6, 0, 0},
{0, 0, 0, 0, 7, 4, 3, 0, 8},
{7, 1, 0, 6, 0, 0, 0, 2, 0},
{0, 6, 7, 5, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 4, 0, 2, 6, 9},
{9, 0, 0, 0, 8, 0, 0, 1, 0},
{0, 5, 0, 3, 2, 0, 0, 4, 0},
{0, 7, 0, 0, 0, 5, 1, 0, 0},
{0, 0, 0, 4, 0, 1, 5, 3, 0}
};
setNum(0, 0, board);
return 0;
}