1. 题目来源
链接:170. 加成序列
2. 题目解析
迭代加深: 当我们在 dfs
过程中,搜索树可能会很深,然而答案所在位置可能比较浅。此时可能用 bfs
会更好,但是当层数较深时,当前层状态数量就很多,bfs
队列存储不下。此时,dfs
迭代加深就是非常好的选择。
迭代加深在搜索时,针对答案前面的层数会反反复复搜到,但是并不影响它整体的效率。因为相较于最后一层那恐怖的指数级状态而言,前面所有层的状态数量之和都无法与之比较。所以,迭代加深针对前面几层的重复搜索是可以接受的。
当学习 IDA*
算法后,迭代加深就可与之搭配,而这是 bfs
所不能及的。
手写笔记,有一点图,便于理解。
题意抽象:构造一个严格单调递增序列,其第一个元素是 1,最后一个元素是 n
,中间任意元素均是在它之前的两个元素之和,其中这两个元素可以是同一个,要求构造出一个长度最短的序列。
思考:
- 当最坏情况时:
1,2,3,4,5,... ,99,100
最深需要 100 层,越到后面状态数量越多,分支越多。 - 最好来讲,
1,2,4,8,16,32,64,128
只需要 8 层就能搜到 128。 - 所以答案不会处在较深层,所以可以使用迭代加深,按层
dfs
,在较浅的层数中将其搜索出来,迭代加深在此非常适合。
搜索顺序:顺序依次枚举序列中的每一个数是什么即可,枚举当前位置可以填的所有情况,递归即可,一定能够搜到所有方案。
优化:
- 优化搜索顺序:优先搜索较大的数,使其越接近
n
,后面的层数会少很多,分支也就少很多。故在枚举当前数填什么的时候,需要枚举前两个数的和,这两个数均应该从大到小枚举,这样和也是从大到小的。 - 排除等效冗余:
- 针对同一个序列,当前位置可能可以填同一个数,但是这样的方案是一样的,后续的分支也是一样的,没有实际含义。故可以针对当前位置可填数开一个
bool
数组,记录当前位置哪些数已经被枚举到了,就不用重复填了。例如情况,1 2 3 4 x
,这个x
若是 5 的话,可以由 1 4 构成,也可由 2 3 构成,则出现重复情况,使用st
数组可有效避免。 - 当前位置要的是前两个数的和,跟两数出现顺序无关,故采用组合枚举的思想,人为定义顺序枚举即可。
- 针对同一个序列,当前位置可能可以填同一个数,但是这样的方案是一样的,后续的分支也是一样的,没有实际含义。故可以针对当前位置可填数开一个
时间复杂度: O ( 指 数 级 ) O(指数级) O(指数级)
空间复杂度: O ( n ) O(n) O(n)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 105;
int n;
int a[N];
bool st[N];
// 当前层,设定的最大层
bool dfs(int u, int depth) {
if (u > depth) return false;
if (a[u - 1] == n) return true;
memset(st, 0, sizeof st); // 当前位置判重,不必重复出现
// 从大到小组合类型枚举
for (int i = u - 1; ~i; i -- )
for (int j = i; ~j; j -- ) { // 枚举组合
int t = a[i] + a[j];
if (st[t] || t > n || t <= a[u - 1]) continue;
st[t] = true;
a[u] = t;
if (dfs(u + 1, depth)) return true;
// a[u] = 0; // 在这不必要恢复现场,因为下次 a[u] = t' 会被直接覆盖掉
}
return false;
}
int main() {
a[0] = 1;
while (scanf("%d", &n), n) {
int depth = 1;
while (!dfs(1, depth)) depth ++ ; // 从第 1 层开始,设定最大 depth 层搜索
// 在第 depth 层搜到结果,里面有 depth 个元素
for (int i = 0; i < depth; i ++ ) printf("%d ", a[i]);
puts("");
}
return 0;
}