一,回溯算法与递归方法的对比
递归:关注代码实现 ---- > 回溯:关注问题解决
方法 ---- > 算法
二,回溯算法的定义
回溯算法 == 问题状态求解树 + 深搜 + 剪枝优化
三,思考问题方向:
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;
}