前言
前两节课蒟蒻君给大家讲解了dfs的基本用法,蒟蒻君来给大家讲一下它的时间复杂度优化~
铺垫一下
1.搜索树和状态
我们可以根据搜索状态构建一张抽象的图,图上的一个节点就是一个状态,而图上的边就是状态之间转移的关系,包括继续dfs或者回溯。
搜索树里每个节点上记录的就是执行到这个状态时的值,对于每个数,我们都有几种选择,都会使目前的状态产生分支。
简单来说,剪枝后的搜索树每个状态有k种选择时,它就是一个完全k叉树(不会的小伙伴可以把这句话当空气)。
2.举个栗子
接下来蒟蒻君现编一道题…
#include
using namespace std;
const int MAX_N = 15; // n的最大值,这里设它为15
int x, n, sum; // 输入数据
int res; // 总方案数
int a[MAX_N];
void dfs(int i, int N, int S) {
// 递归终止条件:所有数都搜索完了(也可以是找到n盘食物了,同理)
if (i == x) {
if (N == n && S == sum) {
// 如果食物数量和重量都符合要求,就把答案+1
++res;
}
return ;
}
dfs(i + 1, N, S); // 如果不选当前菜
dfs(i + 1, N + 1, S + a[i]); // 如果选当前菜
}
int main() {
cin >> x >> n >> sum;
for (int i = 0; i < x; ++i) {
cin >> a[i];
}
dfs(0, 0, 0); // 从编号为0的菜开始搜索,最开始有0个菜,重量和为0
cout << res << '\n';
return 0;
}
当前,目前的时间复杂度为O(2^n),明显有点太慢了。
一会我们会讲到四种剪枝优化,其中有一些就是针对这种哒。
知识梳理
剪枝,顾名思义就是从搜索树上剪掉一些没用的子树,让时间复杂度降低。
一、可行性剪枝
还是刚才的问题。
当n=2的时候,如果当N=2时,已经选了2个数,再继续选就是没有意义了,所以我们可以直接减去这个搜索分支。也就是:
再比如,一旦现在的值已经>sum了,也没必要继续搜索了。
#include
using namespace std;
const int MAX_N = 15; // n的最大值,这里设它为15
int x, n, sum; // 输入数据
int res; // 总方案数
int a[MAX_N];
void dfs(int i, int N, int S) {
if (N > n) {
// 剪枝1
return ;
}
if (S > sum) {
// 剪枝2
return ;
}
// 递归终止条件:所有数都搜索完了(也可以是找到n盘食物了,同理)
if (i == x) {
if (N == n && S == sum) {
// 如果食物数量和重量都符合要求,就把答案+1
++res;
}
return ;
}
dfs(i + 1, N, S); // 如果不选当前菜
dfs(i + 1, N + 1, S + a[i]); // 如果选当前菜
}
int main() {
cin >> x >> n >> sum;
for (int i = 0; i < x; ++i) {
cin >> a[i];
}
dfs(0, 0, 0); // 从编号为0的菜开始搜索,最开始有0个菜,重量和为0
cout << res << '\n';
return 0;
}
二、最优性剪枝
对于求最优解的问题,有些时候可以用最优性剪枝。
比如求解迷宫最短路径类的问题,如果当前步数已经大于等于答案了,就可以停止搜索。
此外,在判断是否有可行解的过程中,只要有一个解,后面的搜索就不用进行啦(可以使用exit(0)),这是最优性剪枝的一种特例。
int res = 0x3f3f3f3f; // 答案
int n, m;
......
void dfs(......, int stp) {
// stp表示当前步数
if (stp >= res) {
return ;
}
if (符合条件) {
// 排除来不是最优解的情况后,此时stp肯定是目前的最优解
res = stp;
return ;
}
......
}
三、重复性剪枝
对于某些搜索方式,一个方案可能会被搜索多次,这样是没必要了。
还是那道题…
我们其实不需要像之前那样每次dfs都把每个都枚举了。因为我们只关注最这串数的和,而不关注顺序。所以我们可以使用重复性剪枝。
我们设定选出的数的下标是递增的,所以我们可以设置一个变量表示上次枚举的下标,这次从上次+1开始枚举就不会有重复的了。
bool eaten[MAX_N]; // eaten[i]表示第i个食物是否被吃过
void dfs(int N, int S, int pos) {
...... // 判断边界 + 可行性剪枝
for (int i = pos; i <= n; ++i) {
if (!eaten[i]) {
// 没吃过就吃
eaten[i] = true;
dfs(N + 1, S + a[i], i + 1); // 下一个的位置在这个的后面
eaten[i] = false; // 回溯,把食物吐出来(蒟蒻君还可以继续吃...)
}
}
}
四、奇偶性剪枝
大家先看一道题(上次农场里那几只挑剔的小猪又登场了…):
这道题其实就是很简单的dfs+剪枝,别的难点都没用到(蒟蒻君好不容易挑哒)。
本题思路分为以下几步:
#include
using namespace std;
const int N = 70;
int n;
int res;
int maxn, minn = N;
int box[N]; // step2
void dfs(int pre, int sum, int x, int y) {
if (pre == 0) {
// step4
cout << x << '\n';
exit(0);
}
if (sum == x) {
// step3
dfs(pre - 1, 0, x, maxn);
return ;
}
for (int i = y; i >= minn; --i) {
// step2 & step3
if (box[i] > 0 && i + sum <= x) {
--box[i];
dfs(pre, sum + i, x, i);
++box[i]; // 回溯
if (sum == 0 || sum + i == x) {
// step4
break;
}
}
}
}
int main() {
cin >> n;
while (n--) {
int t;
cin >> t;
if (t > 50) {
// 过滤
continue;
}
++box[t];
res += t;
maxn = max(maxn, t); // step1
minn = min(minn, t);
}
int en = res >> 1; // 由于res一直在改变,所以需要在循环前定义一个变量存储
for (int i = maxn; i <= en; ++i) {
if (res % i == 0) {
dfs(res / i, 0, i, maxn);
}
}
cout << res << '\n';
return 0;
}
dfs的课程到此结束,接下来蒟蒻君将为大家讲解bfs~