回溯算法
算法思想
走一步看一步,走一步判断一步。当出现非法的情况时,算法可以回退到之前的情景,可以是返回一步,有时候可以返回多步,然后再去尝试别的路径和办法。这也就意味着,想要采用回溯算法,就必须保证,每次都有多种尝试的可能。
步骤
- 判断当前情况是否非法,如果非法就立即返回;
- 当前情况是否已经满足递归结束条件,如果是就将当前结果保存起来并返回;
- 当前情况下,遍历所有可能出现的情况并进行下一步的尝试;
- 递归完毕后,立即回溯,回溯的方法就是取消前一步进行的尝试。
代码模板
function fn(n) {
// 第一步:判断输入或者状态是否非法?
if (input/state is invalid) {
return;
}
// 第二步:判读递归是否应当结束?
if (match condition) {
return some value;
}
// 遍历所有可能出现的情况
for (all possible cases) {
// 第三步: 尝试下一步的可能性
solution.push(case)
// 递归
result = fn(m)
// 第四步:回溯到上一步
solution.pop(case)
}
}
八皇后问题
在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
本题采用回溯法解决。
时间复杂度:O(n!),其中 n 是皇后数量。回溯的过程,其实就是n的一个全排列。
思路分析
- 按行遍历,若该行找到符合要求的位置,行+1;
- 进入新行后,判断每列是否符合条件。对行,列,对角线进行标记,其中,对角线用到行列之和,行列之差为定值这一性质。
- 每次遍历列以后,进行回溯,将标记出来的位置又标记回去。
- 使用递归算法(递归算法的·两点在于:半路出家)。
自写代码(清晰,看这个)
void hang(int n)//行数
{
for(从1到8循环列)
{
if(该点符合条件:1.flag[col]=1(true,表示未被标记),即该点不在上一行的皇后(注意,只是上一行的皇后)覆盖的列方向;2.d1和d2等于1,不超过上对角线和下对角线的边界)
{
place[n] = col;//第n行第col列摆上了皇后
flag[col] = false;//此时标记该列
d1[n-col+7] = false;//标记上对角线
d2[n+col] = false;//标记下对角线
if(n<7)//未结束递归摆放下一行的皇后
hang(n+1)//行+1;
else
print();
//回溯:考虑其他可行方案
flag[col] = true;
d1[n-col+7] = true;
d2[n+col] = true;
}
}
}
void print()
{
cout<<number;//第number个解
int table[8][8] = {0};//初始化设置表,全部定义为0;
for(col=0;col<8;col++)
{
table[col][place[col]] = 1;
两个for循环,用i,j打印table;
}
}
int main()
{
hang(0);
return 0;
}
疑虑解答
1. 我不能接受呀!为什么标记出来的位置可以标回去呀?
能提出这样问题的我,显然是没有理解“递归”的魅力。本题用到的回溯法充分展现了递归半路出家的魅力。即:假设我是一个和尚,我离家出走;假设我离家出走,我往某个方向走…如果假设到最后,我能够修成佛。那么,视为成功,打印出这种修佛路线。所以,每一次的”前进“,其实都是假设。
2. 原来上上行标记了该处,撤销的标记把上上行标记的地方也被撤销了怎么办?
好问题,这个问题困扰了我很久。显然,因为我没有冷静的分析。以四皇后的一种情况举例:
我们是先判断出能不能进行,然后把整个八行都遍历完了,再倒着一行一行撤回的标记。(为了打印输出)
网上的代码实现(不用看)
此代码是百度百科上co的,具体的代码后续补充在评论区。
#include <iostream>
using namespace std;
const int N = 8;
int arr[10], total_cnt;
// arr记录每一行(X)皇后的Y坐标
bool isPlaceOK(int *a, int n, int c) {
for (int i = 1; i <= n - 1; ++i) {
if (a[i] == c || a[i] - i == c - n || a[i] + i == c + n)
return false;
//检查位置是否可以放
//c是将要放置的位置
//a[i] == c如果放在同一列,false
//a[i] -+ i = c -+ n 如果在对角线上,false
}
return true;
}
void printSol(int *a) {
for (int i = 1; i <= N; ++i) { //遍历每一行
for (int j = 1; j <= N; ++j) { //遍历每一列
cout << (a[i] == j ? "X" : "-") << " ";;
} //如果标记数组中这一行的皇后放在j位置,则输出X,否则输出-,
//用空格分隔
cout << endl; //每一行输出一个换行
}
cout << endl; //每一组数据一个换行分隔
}
void addQueen(int *a, int n) {
if (n > N) { //n代表从第一行开始放置
printSol(a);
total_cnt++;
return ;
}
for (int i = 1; i <= N; ++i) { //i从第1列到第N列遍历
if (isPlaceOK(a, n, i)) {
a[n] = i; //如果可以放置,就把皇后放在第n行第i列
addQueen(a, n + 1);
}
}
}
int main() {
addQueen(arr, 1);
cout << "total: " << total_cnt << " solutions.\n";
return 0;
}