迭代加深
定义个层数上限max_depth
,一层一层来搜索, 防止进到无底洞
迭代加深与bfs的区别
宽搜:会将当前层所有节点加入到队列,空间复杂度:指数级别
迭代加深: 只会沿着某条路径搜索,时间复杂度O(n)
迭代加深适合的问题
某些状态层数比较深,但是答案在比较浅的位置,可以保证程序进入到死胡同,提升效率。
后面还有IDA*算法配合迭代加深算法,这是bfs办不到的
效率问题
一层一层,会重复的搜索效率是否会变差。
比如刚开始搜1层, 第2次搜1,2层, 第k次搜1,2, …k层。中间会有重复的层数搜索。
答案:效率不会变差
10层2叉树
前9层总共节点数
2 0 + 2 1 + . . . + 2 9 = 2 10 − 1 2^0 + 2^1 + ... + 2^9 = 2^{10} -1 20+21+...+29=210−1
所有节点连起来,最差情况也才等于第10层节点数,如果是多叉树,那前面的层数相比于下一层而言可以忽略不计。
AcWing 170. 加成序列
分析
可以大致的估算下n的序列范围
1, 2, 8 ,16, 32, 64, 128.
可以发现构造128,只需要长度为8
可以发现最大深度非常大, 1,2, 3, 4,… 99, 100.
但答案由于是最短序列,可能在比较浅的层数
剪枝
优化搜索顺序
依次考虑每个位置的数, 第1个填1, 第2个位置填2,依次往后填数
剪枝1:优先枚举较大的数
更快的到达n,层数较少
剪枝2:排除等效冗余
前面的数字可以用两次,比如前面有5
个数, 那么总共有 C 5 2 + 5 C^2_5 + 5 C52+5种选法。
可能存在前两个数和相等情况. 1, 2, 3, 4. 1 + 4 = 2 + 3. 这样是一种等效冗余
可以递归中开个bool
枚举每个数是否被枚举过,如果枚举过就跳过
代码
xnuohz:这题不需要恢复现场是不是因为可能有不同的path[i] + path[j]得到的值相同,只要其中一个搜不到答案,其他的组合也没必要搜了
yxc:对滴,这里和恢复现场无关,只是个剪枝,避免重复搜索。
#include <iostream>
using namespace std;
const int N = 110;
int path[N];
int n;
bool dfs(int u, int depth){
if (u > depth) return false;
if (path[u - 1] == n) return true;
bool st[N] = {
0};
for (int i = u - 1; i >= 0; i -- )
for (int j = i; j >= 0; j -- ){
int s = path[i] + path[j];
if (s > n || s <= path[u - 1] || st[s]) continue;
st[s] = true; // st数组不是当前状态的一部分
// st只是保证当前层搜索中不搜出重复的节点
path[u] = s; // 因为同一层下一个状态u会被其他状态覆盖掉,因此
// path[u]不需要恢复现场
if (dfs(u + 1, depth)) return true;
}
return false;
}
int main(){
path[0] = 1;
while (cin >> n, n) {
int max_depth = 1;
while (!dfs(1, max_depth)) max_depth ++;
for (int i = 0; i < max_depth; i ++ )
cout << path[i] << ' ';
cout << endl;
}
return 0;
}
双向DFS
可以剔除橙色部分区域,提高搜索效率
AcWing 171. 送礼物
分析
看起来像背包问题,背包问题的时间复杂度O(NV), 题目中 V ≤ 2 31 − 1 V \leq 2^{31 - 1} V≤231−1, 显然会超时
n比较小46,可以爆搜
依次枚举每个物品选/不选。时间复杂度 O ( 2 46 ) O(2^{46}) O(246),也会超时.
选择双向dfs, O ( 2 23 ) O(2^{23}) O(223), 8 ∗ 1 0 6 8 * 10^6 8∗106不会超时
先将前面一半物品能够凑出来的重量搜索一遍(预处理),然后再搜后面一半物品,再搜后面一半物品的时候,可以查表预处理的结果
代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 46;
typedef long long LL;
int n, m, k;
int w[N];
int weights[1 << 25], cnt = 1; // 重量0也算,因此cnt中算了0, cnt = 1
int ans;
void dfs1(int u, int s){
if (u == k){
weights[cnt ++] = s;
return ;
}
dfs1(u + 1, s); // 不选u
if ((LL) s + w[u] <= m) dfs1(u + 1, s + w[u]);
}
void dfs2(int u, int s){
if (u >= n){
// u >= n表示已经计算出后面所有数的总和的情况,包括选/不选
// u >= n 而不是 u == n 是因为 当 n = 2的时候, k = n / 2 + 2,永远 > n,就会死递归。
int l = 0, r = cnt - 1;
while (l < r){
int mid = l + r + 1 >> 1;
if ((LL) s + weights[mid] <= m) l = mid;
else r = mid - 1;
}
ans = max(ans, s + weights[l]);
return ;
}
dfs2(u + 1, s);
if ((LL) s + w[u] <= m) dfs2(u + 1, s + w[u]);
}
int main(){
cin >> m >> n;
for (int i = 0; i < n; i ++ ) cin >> w[i];
sort(w, w + n);
reverse(w, w + n);
k = n / 2 + 2;
dfs1(0, 0);
sort(weights, weights + cnt);
cnt = unique(weights, weights + cnt) - weights;
dfs2(k, 0);
cout << ans << endl;
return 0;
}
IDA*
IDA* 一般配合迭代加深
搜索的时候一圈一圈的扩展,每次搜索会有max_depth
上限
每次搜索的时候,会预估下最少需要多少步到达答案。
如果从
(当前点出发 + 预估步数(当前点到真实值的估价值) >= max_depth
)
那么,递归可以提前退出
这里需要跟A*算法一样,需要保证估价函数 <= 真实值
AcWing 180. 排书
分析
可以枚举书的长度,当长度为i的时候
起点可以从1 ~ n - i + 1
因此长度为i的段有 n - i + 1种
会剩下 n - i 个数, 会有 n - i + 1个空档可以放,最前面那个空档是原来的位置,因此有n - i 个空档
长度为i的决策,总共有(n - i + 1) * (n - i) 种选择
再枚举一遍i, 可以计算总的选择方案
题目n 最大15
总的方案数 = 15 ∗ 14 + 14 ∗ 13 + 2 ∗ 1 15 * 14 + 14 * 13 + 2 * 1 15∗14+14∗13+2∗1
注意: 将前面的书插入到后面跟后面的书插入到前面是同一种。因此还需要/ 2
1 ∗ 2 + 2 ∗ 3 + . . . + n ∗ ( n + 1 ) = n ∗ ( n + 1 ) ∗ ( n + 2 ) 3 1 * 2 + 2 * 3 + ... + n * (n + 1) = \frac{n * (n + 1) * (n + 2)}{3} 1∗2+2∗3+...+n∗(n+1)=3n∗(n+1)∗(n+2)
14 ∗ 15 ∗ 16 / 3 = . . . 14 * 15 * 16 / 3 = ... 14∗15∗16/3=...
最后再/ 2 = 560
题目最多有4次选择, 56 0 4 560^4 5604必定超时,可以用双向宽搜/双向dfs可以过, 时间复杂度 56 0 2 560^2 5602
也可以IDA*做法
可以分析当前这个排列,最少需要多少步,可以变成排好序的。
考虑后继关系
1~ 2, 2 ~ 3, n - 1 ~ n
排好序n个数中,有n - 1个后继关系
对于图中截取的红色段移动到后面最多只会影响3个数的后继关系,可以统计下当前序列中有多少个后继关系是不正确的,记为tot
.
那么最少需要 ⌈ t o t / 3 ⌉ = ⌊ t o t + 2 3 ⌋ \lceil tot / 3\rceil =\lfloor \frac{tot + 2}{3} \rfloor ⌈tot/3⌉=⌊3tot+2⌋ 注意:(左边上取整,右边是下取整,仔细看).
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 15;
int w[5][N];
int q[N];
int n;
int f(){
int tot = 0;
for (int i = 0; i + 1 < n; i ++ )
if (q[i + 1] != q[i] + 1)
tot ++;
return (tot + 2) / 3;
}
bool dfs(int depth, int max_depth){
if (depth + f() > max_depth) return false;
if (f() == 0) return true;
for (int len = 1; len <= n; len ++ )
for (int l = 0; l + len - 1 < n; l ++ ){
int r = l + len - 1;
for (int k = r + 1; k < n; k ++ ){
memcpy(w[depth], q, sizeof q);
int x, y; // 位置移动见上图,先移动绿色r + 1 ~ k
// 然后直接在绿色后面接原来数组的l ~ r那段
for (x = r + 1, y = l; x <= k; x ++ , y ++ ) q[y] = w[depth][x];
for (x = l; x <= r; x ++, y ++ ) q[y] = w[depth][x];
if (dfs(depth + 1, max_depth)) return true;
memcpy(q, w[depth], sizeof q);
}
}
return false;
}
int main(){
int T;
cin >> T;
while (T -- ){
cin >> n;
for (int i = 0; i < n; i ++ ) cin >> q[i];
int depth = 0;
while (depth < 5 && !dfs(0, depth)) depth ++;
if (depth >= 5) puts("5 or more");
else cout << depth << endl;
}
return 0;
}
AcWing 181. 回转游戏
分析
直接打表记录下 图片中每个数的位置。
由于每次操作会从外面移进来一个数,从里面移出一个数。
比如拉动题目中的“A”,只会往中心进来1个数。最少需要8 - (当前中心重复数最大的数量)
可以使得中心全是同一个数
f() = 8 - (当前中心重复数最大的数量)
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 24;
int op[8][7] = {
{
0, 2, 6, 11, 15, 20, 22},
{
1, 3, 8, 12, 17, 21, 23},
{
10, 9, 8, 7, 6, 5, 4},
{
19, 18, 17, 16, 15, 14, 13},
{
23, 21, 17, 12, 8, 3, 1},
{
22, 20, 15, 11, 6, 2, 0},
{
13, 14, 15, 16, 17, 18, 19},
{
4, 5, 6, 7, 8, 9, 10}
};
int opposite[8] = {
5, 4, 7, 6, 1, 0, 3, 2};
int center[8] = {
6, 7, 8, 11, 12, 15, 16, 17};
int q[N];
int path[100];
int f(){
static int sum[4];
memset(sum, 0, sizeof sum);
for (int i = 0; i < 8; i ++ ) sum[q[center[i]]] ++;
int maxv = 0;
for (int i = 1; i <= 3; i ++ ) maxv = max(maxv, sum[i]);
return 8 - maxv;
}
void operate(int x){
int t = q[op[x][0]]; //找到q中op位置上第一个数
for (int i = 0; i < 6; i ++ ) q[op[x][i]] = q[op[x][i + 1]];
q[op[x][6]] = t;
}
bool dfs(int depth, int max_depth, int last){
if (depth + f() > max_depth) return false;
if (f() == 0) return true;
for (int i = 0; i < 8; i ++ ){
if (opposite[i] != last){
operate(i);
path[depth] = i;
if (dfs(depth + 1, max_depth, i)) return true;
operate(opposite[i]);
}
}
return false;
}
int main(){
while (cin >> q[0], q[0]){
for (int i = 1; i < N; i ++ ) cin >> q[i];
int depth = 0;
while (!dfs(0, depth, -1)) depth ++;
if (!depth) printf("No moves needed");
else {
for (int i = 0; i < depth; i ++ ) printf("%c", path[i] + 'A');
}
printf("\n%d\n", q[6]);
}
return 0;
}