算法初探系列3 -深度优先搜索之剪枝策略

前言
前两节课蒟蒻君给大家讲解了dfs的基本用法,蒟蒻君来给大家讲一下它的时间复杂度优化~
铺垫一下
1.搜索树和状态
我们可以根据搜索状态构建一张抽象的图,图上的一个节点就是一个状态,而图上的边就是状态之间转移的关系,包括继续dfs或者回溯。
搜索树里每个节点上记录的就是执行到这个状态时的值,对于每个数,我们都有几种选择,都会使目前的状态产生分支。
简单来说,剪枝前的搜索树每个状态有k种选择时,它就是一个完全k叉树(不会的小伙伴可以把这句话当空气)。
2.举个栗子
接下来蒟蒻君现编一道题…

  • 题目
    蒟蒻君今天想吃x斤食物 (蒟蒻君是吃货没错) ,现在有n盘食物,第i盘食物有a[i]斤,蒟蒻君想知道有几种吃法。
  • 分析一下
    对与每个食物,我们都有选和不选两种选择(即k=2),我们在每次搜索中都要尝试这两种选择,达到条件计数器+1.
  • 植树~
    让我们来看一下这道题的搜索树叭(蒟蒻君画得不咋地)
    我们用S来记录当前总斤数和,用N来记录选择的食物的个数。
    在这里插入图片描述
  • 代码
#include <bits/stdc++.h>
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 <bits/stdc++.h>
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;	// 回溯,把食物吐出来(蒟蒻君还可以继续吃...)
		}
	}
}

四、奇偶性剪枝
大家先看一道题(上次农场里那几只挑剔的小猪又登场了…):

  • 题目
    农场主嫌这些小猪又懒又馋 (猪猪不都是这样吗) ,决定给它们一个考验。
    他给猪猪们出了一个n * m的迷宫,其中有起点、终点、墙和平地。小猪猪们只能向上下左右走,不能爬墙 (它们貌似也不会爬墙)
    它们走每步需要一个小时的时间(因为它们实在是太胖了),到第T个小时时,农场主就会不耐烦,把猪粮送给隔壁的老母猪。
    猪猪们很饿,想让你判断一下是否可以吃到猪粮。
    起点:‘A’
    终点:‘B’
    平地:’.’
    墙:’*’
  • 分析
    这里会的小伙伴可能会想到小学奥数证明题中的染色问题,其中的一个方法就是黑白染色。
    在这里插入图片描述
    如图,每隔一个格子染成黑色,其余的为白色。
    在这个图中,每走一步就会变一次色。
    我们还可以看出来,如果行数+列数为计数,当前格子就为黑色。否则为白色。
    从A走到B要奇数次(即A、B两格不同色),而T为偶数,或者要偶数次,而T为奇数,则可以剪枝。
    剪枝例题
    在这里插入图片描述
  • 分析

这道题其实就是很简单的dfs+剪枝,别的难点都没用到(蒟蒻君好不容易挑哒)。
本题思路分为以下几步:

  1. 预处理所有木棍的总长度,保证枚举答案的值能被总长度整除。
  2. 每根木棍的长度可用hash存储,预处理最长的和最短的木棍的长度,dfs时从最大长度到最小长度枚举。
  3. 记录当前长度,下次dfs从当前长度-1来枚举(重复性剪枝)。
  4. 若某组拼接不成立,且此时已拼接的长度为0或当前已拼接的长度与刚才枚举的长度之和为最终枚举的答案时,则可直接跳出循环(可行性剪枝)。
  • 代码
#include <bits/stdc++.h> 
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~

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 23
    评论
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蒟蒻一枚

谢谢鸭~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值