迭代加深
搜索时可能会遇到这样一种情况:
明明答案就在第一层!但是因为DFS的缘故浪费很多时间
迭代加深就是用来解决这个问题的算法
定义一个 max_depth ,每次搜索时,超过这一层就全部剪掉、
(相当于我们划定一个区域,在这个区域内找解,如果找不到,再扩大区域)
?迭代加深和BFS有什么区别呢
BFS用队列存储,浪费空间,迭代加深本质是还是DFS,只存储本条路径,还是O(n)的算法
例题
加成序列
满足如下条件的序列 X(序列中元素被标号为 1、2、3…m)被称为“加成序列”:
- X [ 1 ] = 1 X[1]=1 X[1]=1
- X [ m ] = n X[m]=n X[m]=n
- X [ 1 ] < X [ 2 ] < … < X [ m − 1 ] < X [ m ] X[1]<X[2]<…<X[m−1]<X[m] X[1]<X[2]<…<X[m−1]<X[m]
- 对于每个 k(2 ≤ k ≤ m)都存在两个整数 i 和 j (1 ≤ i , j ≤ k − 1,i 和 j 可相等),使得 X [ k ] = X [ i ] + X [ j ] X[k]=X[i]+X[j] X[k]=X[i]+X[j]。
你的任务是:给定一个整数 n,找出符合上述条件的长度 m 最小的“加成序列”。
如果有多个满足要求的答案,只需要找出任意一个可行解。
输入格式
输入包含多组测试用例。
每组测试用例占据一行,包含一个整数 n。
当输入为单行的 0 时,表示输入结束。
输出格式
对于每个测试用例,输出一个满足需求的整数序列,数字之间用空格隔开。
每个输出占一行。
数据范围
1 ≤ n ≤ 100
输入样例
5
7
12
15
77
0
输出样例
1 2 4 5
1 2 4 6 7
1 2 4 8 12
1 2 4 5 10 15
1 2 4 8 9 17 34 68 77
题意
给出 n 构造一个序列,要求第一个数是1,最后一个数是n,严格递增,且后面的数一定要是前面两个数之和(两个数可以是同一个数),输出一个长度最小的序列
思路
序列的最小规模:1 2 4 8 16 32 64 128 此时就已经超过100了,说明正确答案的深度不会很深,适合用迭代加深来做
层数从1开始,依次考虑每一位选什么数字
优化:
- 优化搜索顺序:优先枚举较大的数,层数较少,更快的找到 n
- 排除等效冗余:举个栗子:1 2 3 4 现在枚举下一个数,不管选择1+4还是2+3结果都是5,就可以不用计算两次了(方法是开一个bool数组存储每个数是否被用过)
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n;
int path[N];
bool st[N]; // 标记某数是否被用过
bool dfs(int u, int depth) // 分别是当前层数和最大层数
{
if (u > depth) return false; // 当前层数>最大层数
if (path[u - 1] == n) return true; // 最后一个数为n满足条件
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; // 大于最大值or小于前一个值or已被用过 都不满足条件
st[s] = true; // 标记s已被用过
path[u] = s; // 记录s
if (dfs(u + 1, depth)) return true; // 下一位
st[s] = false; // 恢复现场
}
return false;
}
int main()
{
path[0] = 1;
while (cin >> n, n)
{
memset(st, false, sizeof st);
int depth = 1;
while (!dfs(1, depth)) depth ++ ;
for (int i = 0; i < depth; i ++ ) cout << path[i] << ' ';
cout << '\n';
}
}
IDA*
IDA*是什么意思?
IDA*就是特殊的剪枝,一般情况下和迭代加深结合起来使用,需要定义一个max_depth,我们搜索到一个结点的时候就开始预估这个结点和正确答案的步数,如果说在这个步数内无论如何都找不到正确答案,那就不继续往下搜了提前退出
要求:估价函数<=真实值
例题
排书
给定 n n n 本书,编号为 1 ∼ n 1∼n 1∼n。
在初始状态下,书是任意排列的。
在每一次操作中,可以抽取其中连续的一段,再把这段插入到其他某个位置。
我们的目标状态是把书按照 1 ∼ n 1∼n 1∼n 的顺序依次排列。
求最少需要多少次操作。
输入格式
第一行包含整数 T T T,表示共有 T 组测试数据。
每组数据包含两行,第一行为整数 n n n,表示书的数量。
第二行为 n n n 个整数,表示 1 ∼ n 1∼n 1∼n 的一种任意排列。
同行数之间用空格隔开。
输出格式
每组数据输出一个最少操作次数。
如果最少操作次数大于或等于 5 次,则输出 5 or more
。
每个结果占一行。
数据范围
1 ≤ n ≤ 15 1 ≤ n ≤ 15 1≤n≤15
输入样例
3
6
1 3 4 6 2 5
5
5 4 3 2 1
10
6 8 5 3 4 7 2 9 1 10
输出样例
2
3
5 or more
题意
一列数,每次可以取出其中一个子串插到其他地方,问至少多少次能让数列递增
思路
采用迭代加深框架,每次估计当前序列最少要进行多少次操作才能把它变成排好序的序列
怎么确定估价函数呢?
我们观察每一位数的后继(也就是后面那个数),如果序列排好序,那么每个元素的后继都应该比元素本身大1
每操作一次会修改三个元素的后继,如下图所示:
因此我们先统计出有 tot 个元素的后继不正确,因为每次操作可以修改三个后继,我们假设三个后继都变成正确的了,所以最少进行的操作步数就是
⌈
t
o
t
3
⌉
\lceil\frac{tot}{3}\rceil
⌈3tot⌉,也就是
⌊
t
o
t
+
2
3
⌋
\lfloor\frac{tot+2}{3}\rfloor
⌊3tot+2⌋,用它作为估价函数可以保证一定满足条件
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 15;
int n;
int q[N];
int w[5][N];
int f() // 估价函数
{
int tot = 0; // 错误的后继数量
for (int i = 0; i < n - 1; 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; // 估价函数=0说明当前就是正确答案
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 ++ ) // 枚举挪到哪个位置(k是结尾位置
{
memcpy(w[depth], q, sizeof q); // 把原数组存到w里
// 挪位
int y = l;
for (int x = r + 1; x <= k; x ++, y ++ ) q[y] = w[depth][x];
for (int 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()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
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 ++ ; // 这里注意循环判断条件一定要把depth<5放前面!否则TLE
if (depth >= 5) cout << "5 or more\n";
else cout << depth << '\n';
}
}
挪位理解不了就画图
回转游戏
如下图所示,有一个 # 形的棋盘,上面有 1,2,3 三种数字各 8 个。
给定 8 种操作,分别为图中的 A ∼ H A∼H A∼H。
这些操作会按照图中字母和箭头所指明的方向,把一条长为 7 的序列循环移动 1 个单位。
例如下图最左边的 # 形棋盘执行操作 A A A 后,会变为下图中间的 # 形棋盘,再执行操作 C C C 后会变成下图最右边的 # 形棋盘。
给定一个初始状态,请使用最少的操作次数,使 # 形棋盘最中间的 8 个格子里的数字相同。
输入格式
输入包含多组测试用例。
每个测试用例占一行,包含 24 个数字,表示将初始棋盘中的每一个位置的数字,按整体从上到下,同行从左到右的顺序依次列出。
输入样例中的第一个测试用例,对应上图最左边棋盘的初始状态。
当输入只包含一个 0 的行时,表示输入终止。
输出格式
每个测试用例输出占两行。
第一行包含所有移动步骤,每步移动用大写字母
A
∼
H
A∼H
A∼H 中的一个表示,字母之间没有空格,如果不需要移动则输出 No moves needed
。
第二行包含一个整数,表示移动完成后,中间 8 个格子里的数字。
如果有多种方案,则输出字典序最小的解决方案。
输入样例
1 1 1 1 3 2 3 2 3 1 3 2 2 3 1 2 2 2 3 1 2 1 3 3
1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
0
输出样例
AC
2
DDHH
2
题意
井字格,每次可以拉动任意一条边,要求用最小的操作次数使中间的八个格子数字一样
思路
要求字典序最小,因此按照字典序搜索即可
每次往下搜所层数都很深,但是根据 搜索问题的玄学性, 直觉来看正确答案的层数不会很深,所以选择迭代加深的算法来做
迭代加深搜索的过程中我们加入估价函数,让迭代加深升级为IDA*
每拉动一次,中间的八个格子只有一个格子的数字会改变,所以先统计八个格子里出现次数最多的数字出现了多少次(记作 c n t cnt cnt),那最好的情况就是每操作一次,都使得一个不是最多数的数字变成最多数,因此可以将估价函数设置为 f ( ) = 8 − c n t f()=8-cnt f()=8−cnt,就可以保证这个数字一定小于等于正确答案
本题还有一个难点就是怎么表示出这几个操作,我们可以先打表,给格子编号,然后预先处理出几个操作的具体步骤
优化: 本次枚举的操作一定不能是上一次的逆操作
代码
#include <bits/stdc++.h>
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() // 估价函数
{
int sum[4] = {0};
for (int i = 0; i < 8; i ++ ) sum[q[center[i]]] ++ ; // 找到每个数出现几次
int s = 0;
for (int i = 1; i <= 3; i ++ ) s = max(s, sum[i]); // 找到出现最多的次数
return 8 - s;
}
void operate(int x) // 操作
{
int t = q[op[x][0]];
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) // depth:当前层数 max_depth:最大层数 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()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
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) cout << "No moves needed";
else
{
for (int i = 0; i < depth; i ++ ) cout << (char)(path[i] + 'A');
}
cout << '\n' << q[6] << '\n';
}
}