一,回溯算法与递归方法的对比
递归:关注代码实现 ---- > 回溯:关注问题解决
方法 ---- > 算法
二,回溯算法的定义
回溯算法 == 问题状态求解树 + 深搜 + 剪枝优化
三,思考问题方向:
1.得到问题状态搜索树
2.切勿妄想一步到位:先实现,再优化
3.搜索剪枝优化:没有固定方法,具体问题,具体分析
典例:八皇后luogup1219
优化方式1:状态压缩

有一表示状态的01数组,那么就可以将数组压缩成一个二进制数
优化方式2:快速枚举
当使用一个二进制数表示01数组,我们可以通过位运算直接取得二进制数中的最末尾的1,避免了无效位的访问。
如何取得二进制数t的最末尾1:最末尾1 = t & (-t);


如何构建循环,不断去取二进制t中的1:t -= t & (-t);

优化方式3:斜边表示
1.正斜线num:num = i + j - 1;

2.反斜线num: num = i - j + n

典例:奇怪的电梯luoguP1135(bfs better)

当在阴影部分出现绿色节点表示的状态时,停止递归。历史答案可以用数组等结构储存;
典例:P1036选数
组合型选择方式,只关注选择的组合的内容;
典例:P1443马的遍历(bfs)
优化1: 方向数组,记录偏移量
优化2: bfs每一步得到的都是最小步数
#define MAX_N 400
//方向数组
int dir[8][2] = {
{2, 1}, {-2, 1}, {2, -1}, {-2, -1},
{1, 2}, {1, -2}, {-1, 2}, {-1, -2}
};
int dis[MAX_N + 5][MAX_N + 5];
struct Node {
Node(int x, int y, int s) : x(x), y(y), s(s) {}
int x, y, s;
};
void bfs(int n, int m, int a, int b) {
queue<Node> q;
q.push(Node(a, b, 0));
dis[a][b] = 0;
while (!q.empty()) {
Node now = q.front();
q.pop();
//方向数组使代码更简洁
for (int k = 0; k < 8; k++) {
int dx = now.x + dir[k][0];
int dy = now.y + dir[k][1];
if (dx < 1 || dx > n) continue;
if (dy < 1 || dy > m) continue;
if (dis[dx][dy] != -1) continue;
q.push(Node(dx, dy, now.s + 1));
dis[dx][dy] = now.s + 1;
}
}
return ;
}
int main() {
int n, m, a, b;
scanf("%d%d%d%d", &n, &m, &a, &b);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dis[i][j] = -1;
}
}
bfs(n, m, a, b);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (j > 1) printf(" ");
printf("%d", dis[i][j]);
}
printf("\n");
}
return 0;
}
典例:迷宫P1605

典例:吃奶酪P1433


t表示剩余的可选状态(1,3,5), 6则是当前路径的最后一个点,用dp二维数组记录最小历史记录,二维数组的两个引索共同标记着一类历史状态。
典例:单词接龙P1019
//判断s1后缀是否等于s2前缀
int f(string& s1, string s2) {
for (int i = s1.size() - 1; i >= 1; i--) {
int flag = 1;
for (int j = 0; i + j < s1.size(); j++) {
//s2[s2.size()] == '\0',当j == s2.size(),循环一定会break,不会产生非法访问
if (s1[i + j] == s2[j])continue;
flag = 0;
break;
}
if (flag == 1)return s1.size() - i;
}
return 0;
}
典例:P1032 字串变换
一、迭代加深搜索
1.迭代加深搜索 简介
迭代加深是一种每次限制搜索深度的深度优先搜索(DFS)。
它的本质还是深度优先搜索,只不过在搜索的同时带上了一个深度 d,当d达到设定的深度时就返回,一般用于找最优解。如果一次搜索没有找到合法的解,就让设定的深度加一,重新从根开始。
这里不妨回想一下BFS搜索算法:这里我们要达到的目的都是寻求最优解,那么迭代加深搜索与BFS分别应用于什么样的场景呢?
我们知道 BFS 的基础是一个队列,队列的空间复杂度很大,当状态比较多或者单个状态比较大时,使用队列的 BFS 就显出了劣势。事实上,迭代加深就类似于用 DFS 方式实现的 BFS,它的空间复杂度相对较小。
当搜索树的分支比较多时,每增加一层的搜索复杂度会出现指数级爆炸式增长,这时前面重复进行的部分所带来的复杂度几乎可以忽略,这也就是为什么迭代加深是可以近似看成BFS的。
适用场景
在大多数的题目中,广度优先搜索还是比较方便的,而且容易判重。当发现广度优先搜索在空间上不够优秀,而且要找最优解的问题时,就应该考虑迭代加深搜索。
3.优点/缺点
1).优点
-
空间开销小,每个深度下实际上是一个深度优先搜索,不过深度有限制,而 DFS 的空间消耗小是众所周知的;
-
利于深度剪枝。
2).缺点
重复搜索:回溯过程中每次 depth 变大都要再次从头搜索(其实,前一次搜索跟后一次相差是微不足道的)。
#include<iostream>
#include<string>
#include<vector>
#include<unordered_map>
using namespace std;
int ans = 100;
unordered_map<string, int> dp;
void dfs(string a, string &b, int k, int& n, vector<string>& from, vector<string>& to) {
//得到最优解,不再递归
if (ans != 100)return;
dp[a] = k;
if (a == b) {
ans = k;
return;
}
//到达递归最深处,直接返回
if (k >= n)return;
for (int i = 0, I = from.size(); i < I; i++) {
int pos = a.find(from[i], 0);
while (pos != -1) {
string temp = a;
temp.erase(pos, from[i].size());
temp.insert(pos, to[i]);
//历史优化剪枝
if (dp.find(temp) == dp.end() || dp[temp] >= k + 1) {
dfs(temp, b, k + 1, n, from, to);
}
pos = a.find(from[i], pos + 1);
}
}
return;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL);
string a, b;
cin >> a >> b;
vector<string> from, to;
string temp_from, temp_to;
while (cin >> temp_from >> temp_to) {
from.push_back(temp_from);
to.push_back(temp_to);
}
//迭代加深
for (int i = 1; i <= 10; i++) {
dfs(a, b, 0, i, from, to);
if (ans != 100) break;
}
if (ans <= 10)cout << ans;
else cout << "NO ANSWER!";
return 0;
}