大多是使用回溯算法的题目都符合以下条件:
- 输出可以看作一个n元组(x1,x2,…,xn),例如八皇后问题。
问题的解可以表示为n元组:(x1,x2,…,xn),xi∈Si,Si为有穷集合。(x1,x2,…,xn)具有完备性,即(x1,x2,…,xn)是合理的,则(x1,x2,…,xi) (i<n)一定合理 - 元组需要满足一些约束条件。
回溯算法是在状态空间树上跳跃式地进行深度优先搜索,即用判定函数考察x[k]的取值,如果x[k]是合理的就搜索以x[k]为根节点的子树,如果x[k]取完了所有的值,便回溯到x[k-1]。
个人认为回溯算法和暴力法有很大的相似度,但是回溯算法会在每次构造解的过程中进行剪枝。整个选择过程可以构成一棵状态空间树,树的根可以代表查找解之前的初始状态。大多数情况下可以用DFS的方法来生成状态空间树。如果当前结点是有希望的,可以继续深度遍历,否则回溯到父母节点。
首先来看一道经典例题
一.八皇后问题
1.问题描述
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。
2.问题分析
回溯法是求解皇后问题最经典的方法。算法的思想在于如果一个皇后选定了位置,那么下一个皇后的位置便被限制住了,下一个皇后需要一直找直到找到安全位置,如果没有找到,那么便要回溯到上一个皇后,那么上一个皇后的位置就要改变,这样一直递归直到所有的情况都被举出。
首先思考一下,输出的元组中每一个分量,分开来看,在八皇后问题中,每一个分量都有八种可能,那么在每一层递归中我们都要穷举这八种可能,然后根据条件来判断该种结果是否合理。再者,因为不能在同一列,也为了避免重复,下一个皇后只能在上一个皇后的下面。这样思考问题就迎刃而解了。
#include<iostream>
using namespace std;
#define MAX 8;
//回溯法
int chessboard[8][8] = {
0 };
int num = 0;
bool check(int x, int y) //
{
for (int i = 0; i < x; i++) {
//检查同列是否有皇后
if (chessboard[i][y] == 1)
return false;
if (x - 1 - i >= 0 &&y-1-i>=0&&chessboard[x - 1 - i][y - 1 - i] == 1) //检查左斜线
return false;
if (x - 1 - i >= 0 &&y+1+i<=7&& chessboard[x-1-i][y+1+i] == 1) //检查右斜线
return false;
}
return true;
}
void show() {
// ++num;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
cout << chessboard[i][j];
}
cout << endl;
}
}
void putQueen(int x) {
int i;
for (i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) chessboard[x][j] = 0;
if (check(x,i)) {
chessboard[x][i] = 1;
if (x == 7) {
cout << "第" << num + 1 << "种解法" << endl;
show();
cout << endl;
++num;
}
else {
putQueen(x + 1);
}
}
}
}
int main()
{
putQueen(0);
cout << num << endl;
}
二.二进制手表
1.问题描述
二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。
例如,上面的二进制手表读取 “3:25”。
给定一个非负整数 n 代表当前 LED 亮着的数量,返回所有可能的时间。
2.问题分析
仔细想想,这和八皇后问题其实是一个套路。解题方法参考了一个大佬的文章。首先我们来个映射
0-3号表示时,后面的4-9号表示分,然后从10盏灯中选择n盏灯点亮。
#include<iostream>
#include<vector>
#include<string>
#include<unordered_map>
using namespace std;
vector<string>res;
unordered_map<int, int> timeHash = {
{
0,1},{
1,2},{
2,4},{
3,8},{
4,1},{
5,2},{
6,4},{
7,8},{
8,16},{
9,32} };
void backtrack(int num, int start, pair<int, int> & time)
{
if (num == 0)
{
if (time.first > 11 || time.second > 59)//判断合法性
return;
string temp_hour = to_string(time.first);
string temp_minute = to_string(time.second);
if (temp_minute.size() == 1)//如果minute只有一位要补0
temp_minute.insert(0, "0");
// res.push_back(temp_hour + ":" + temp_minute);//构造格式
cout << temp_hour << ":" << temp_minute << endl;
return;
}
for (int i = start; i < 10; i++)
{
if (time.first > 11 || time.second > 59)
continue;
pair<int, int>store = time;//保存状态
if (i < 4)
time.first += timeHash[i];
else
time.second += timeHash[i];
backtrack(num - 1, i + 1, time);//进入下一层,注意下一层的start是i+1,即从当前灯的下一盏开始
time = store;//恢复状态
}
}
void readBinaryWatch(int num) {
pair<int, int>time(0, 0);//初始化时间为0:00
backtrack(num, 0, time);
//return res;
}
int main()
{
readBinaryWatch(2);
}
但是需要特别注意的是,在这个题目中需要有状态的恢复,即我们所说的回溯到状态空间树中的父结点的状态,然后在对其进行改变,所以在我们递归到下一层时,首先应该保存一下上一层的状态。