信息竞赛:DFS类搜索专题详细讲解

说明:

1.该讲解主要针对已经大致学过搜索,但是想复习或想提高能力或正在备战NOIP的oier

2.红色字体为重点

1.DFS

简介:不加优化的DFS效率很低,常常用于暴力枚举,在NOIP考试中直接考察裸的搜索概率不大。

常用方面:枚举子集,枚举排列,搜索枚举答案,图或树的遍历

模板:

void dfs(int i, ...){
	if(i==边界){
		处理;
		return 0; 
	}
	for(枚举所有状态){
		其他处理; 
		dfs(i+1, ...); 
		其他处理;
	}
}

2.DFS+剪枝(重难点)

简介:DFS+剪枝与DFS不同就在于剪枝,加上适当的剪枝的DFS速度会极大的加快,往往考察DFS+剪枝的题分数取决于你剪枝的强度,在早年的NOIP考试中经常考察,而且常常作为压轴题出现,近年来考察力度有所减少,但它还是面对写不了正解的题的得分利器

常用方面:需要枚举答案或数据范围偏小的题

模板:

void dfs(int i, ...){
	if(i==边界){
		处理;
		return 0; 
	}
	if(满足剪枝条件) return;//不再搜索下面的状态,剪枝 
	for(枚举所有状态){
		其他处理; 
		dfs(i+1, ...); 
		其他处理;
	}
}

我们先看一个简单的例子:

射击问题 

时间限制:5秒  内存限制:64M

【问题描述】    
射击运动员每次训练他都会连续打出 n 发子弹,他只有平均达到或超过 9 环才算通过。现请你编程计算出通过共有哪些情况。每发子弹击中的环数是一个 0-10 之间的整数,0 环表示脱靶,10 环表示击中靶心。 
     
【输入格式】    
     
  仅一行,包含一个正整数n,表示打出的子弹数。
     
【输出格式】    
  若干行,每行由 n 个用空格隔开的数字组成,表示某一组符合条件的环数(按N个数字排列的字典序输出)。最后一行输出一个数,表示符合条件的总方案数。
【数据范围】    
  1 <= n <= 11

分析:

单纯搜索:

我们直接搜索他每一次可以打的环数,然后判断是否满足条件就可以了

时间复杂度O(11^n),肯定要超时

代码:

#include
int ans=0,n,a[12];
void operate(int k,int num){
	if(k>n){//边界 
		if(num>=9*n){//处理 
			ans++;
			for(int i=1;i

优化(剪枝):

我们考虑他已经射击了k-1次,已经共达到sum环,那么如果接下来(从第k次开始)每次射击都是10环都不能使他平均环数达到9环,那么接下来的情况我们就不需要继续搜索了,此时剪枝即可。

剪枝条件:if(sum+10*(n-k+1)<9*n) \,\,\,return;

优化后的代码:

#include
int out[15];
void print(int pnt){//快速输出 
	int p=0;
	if(pnt==0) {putchar('0');return;}
	else while(pnt)
		out[p++]=pnt%10,pnt/=10;
	for(int j=p-1;j>=0;j--) putchar('0'+out[j]);
}
int ans=0,n;
int a[12];
void operate(int k,int num){
	if(k>n){//边界 
		if(num>=9*n){//处理 
			ans++;
			for(int i=1;i

优化后速度提升是巨大的,再加上快速输出优化,我的程序运行速度轻松达到榜首

拓展训练(题目推荐):

接下来的题目难度开始加大了,其中大部分是当年NOIP的压轴题,均需要有足够强的剪枝才能通过,如果做不来请看讲解。

T1:

难度:XX(基础题)

题目:(NOIP2001)数的划分  题解:见T2

T2:

难度:XXXX(难题,下同)

题目:(NOIP2003)传染病控制  题解:见T4

T3:

难度:XXXX

题目:(NOIP2004)虫食算  题解:见T4

T4:

难度:XXXX

题目:(NOIP2010)靶形数独  题解:见T4

T5:

难度:XXXX

题目:(NOIP2011)Mayan游戏  题解:见T3

3.双端DFS

简介:

该类DFS常用于子集型问题的搜索(也就是枚举子集的问题);

如果单纯DFS枚举子集,时间复杂度为:O(2^n)

但是双端DFS时间复杂度为:O(2\sqrt{2^n}),这是一个比较巨大的优化

实现方法:

  1. 先写第一个DFS计算在[1,n/2]子集枚举状态并存入map,或hash表
  2. 写第二个DFS,计算在[n/2+1,n]的子集枚举状态,然后直接在map或hash表中查找需要的状态就可以了

模板:

void dfs1(int i, ...){
	if(i>n/2){
		存入map或hash表;
		return; 
	}
	dfs1(i+1, ...);
	dfs1(i+1, ...);
}
void dfs2(int i, ...){
	if(i>n){
		查找map或hash表,并与当前状态合并; 
		其他操作; 
		return;
	}
	dfs2(i+1, ...);
	dfs2(i+1, ...);
}

例题讲解:

超大背包问题    


时间限制:1秒  内存限制:64M
【问题描述】    
    有重量和价值分别为wi,vi的n个物品。从这些物品中挑选总重量不超过W的物品,求所挑选方案中价值总和的最大值。 
【输入格式】    
  第一行为整数n和W,分别表示物品数量和重量限制。接下来的n行,每行两个整数,表示一个物品的重量wi和价值vi。
【输出格式】    
  一个整数,表示最大价值。
【输入样例】
4 5
2 3
1 2
3 4
2 2    
【输出样例】        
7
【数据范围】        
1<=n<=40 , 1<=wi,vi<=10^15 , 1<=W<=10^15

分析:

  1. 首先这道题不能用动态规划,因为背包太大
  2. 其次单纯枚举子集要超时,此时我们考虑双端dfs枚举子集
  3. 具体细节见代码
  4. 其实代码并还可以优化,方法是在dfs1之后dfs2之前进行处理,添加记忆化,具体方法请读者思考

代码:

#include
#include
#include
#include
using namespace std;
#define ll long long
int n;
ll ans=0,W,w[42],v[42]; 
mapmp;
map::iterator it;
void dfs1(int i,ll wsum,ll vsum){
	if(i>n/2){//存入map
		if(mp[wsum]n){
		it=mp.upper_bound(W-wsum);//直接调用即可
		it--;
		while(it!=mp.begin()){
			ans=max(ans,it->second+vsum);
			it--;
		}
		return;
	}
	if(wsum+w[i]<=W) dfs2(i+1,wsum+w[i],vsum+v[i]);
	dfs2(i+1,wsum,vsum);
}
int main(){
	cin>>n>>W;
	for(int i=1;i<=n;i++) cin>>w[i]>>v[i];
	dfs1(1,0,0);
	dfs2(n/2+1,0,0);
	cout<

例题二:

难度:XXX+

注意啦:这个题目不再是子集枚举了,而是另一种双端搜索,也就是先从起点搜索状态,再从终点搜索状态直到相遇

(NOIP2002)字串变换(见T2)

拓展训练(题目推荐):

T1:

难度:XXX+

[USACO09NOV]灯Lights

注意:这道题子集枚举之前需要状态压缩,也就是将灯的状态用二进制方法表示,开关灯用异或(^)表示

4.迭代加深搜索

简介:

  1. 这种算法主要用于求最小步数方面,我们先得出一个k表示应该在k步之内解决问题,然后搜索,如果在搜索途中我们结合限定的k值发现接下来无法满足在k步之内得到答案就返回
  2. 迭代加深搜索的题目可以比较难,比如埃及分数,倒水问题,编辑书稿,等等。
  3. 迭代加深搜索在NOIP中似乎并不怎么考,加之时间有限,下面就不展开介绍了。
  4. 其实对于相当一部分,DFS+剪枝算法也可以解决,但是埃及分数似乎不可以,大家有兴趣可以研究研究。

你可能感兴趣的:(信息竞赛专题讲解,信息竞赛知识点)