【暖*墟】#DFS# 深搜技巧与优化

73 篇文章 0 订阅
7 篇文章 0 订阅

目录

一. DFS的实现

【需要注意的地方】

二. 剪枝技巧的实现

1.可行性剪枝。

2.最优性剪枝。

3.记忆化搜索。

4.搜索顺序剪枝。


一. DFS的实现

int dfs(int t)
{
    if(满足输出条件) //dfs出口
    {
        输出解;
    }
    else
    {
        for(int i=1;i<=尝试方法数;i++)
            if(满足进一步搜索条件)
            {
                为进一步搜索所需要的状态打上标记; //记忆化
                dfs(t+1);
                恢复到打标记前的状态; //也就是说:回溯一步
            }
    }
}

【需要注意的地方】

1.第一个if是符合输出解的条件,第二个if是符合进一步搜索的条件。

2.下一步搜索时,不是使用return search(t+1),直接search(t+1)。

3.for循环之后的if可以是多个。

4.for循环边界,例如:

  1. 方向是四个,那么边界肯定就是4;(注意,数组一般从0开始)
  2. 素数环需要尝试1至20,那么边界就是20。

 

二. 剪枝技巧的实现

1.可行性剪枝。

如果当前条件不合法就不再继续搜索,直接return。又称“上下界剪枝”,一般的搜索都会加上。

dfs(int x){
if(x>n)return;
if(!check1(x))return;
....
return;
}

 

2.最优性剪枝。

 如果当前条件所花费的代价已经超过了当前搜到的最优解,那么剩下的搜索就可以剪掉。

我们利用某个函数估计出此时条件下答案的当前最值,继续下一步判断。

long long ans=987474477434487ll;
... Dfs(int x,...){
	if(x... && ...){ ans=....; return ...;}
	if(check2(x)>=ans) return ...;	//最优性剪枝 
	for(int i=1;...;++i){
		vis[...]=1; 
		dfs(...);
		vis[...]=0;
	}
}

一般实现:在搜索取和最大值时,如果后面的全部取最大仍然不比当前答案大就可以返回。

在搜和最小时同理,可以预处理后缀最大/最小和进行快速查询。

 

3.记忆化搜索。

如果对于相同情况下必定答案相同,就可以把这个情况的答案值存储下来,以后再次搜到可以直接调用。

注意不能搜出环来,不同情况间不能互相依赖。或者是排除等效冗余的情况。

long long ans=987474477434487ll;
... Dfs(int x,...){
	if(x... && ...){ ans=....; return ...;}
	if(vis[x]!=0) return f[x]; vis[x]=1; //记忆化剪枝
	for(int i=1;...;++i){
		vis[...]=1; 
		dfs(...);
		vis[...]=0;
		f[x]=...;
	}
}

 

4.搜索顺序剪枝

在一些迷宫题,网格题,或者其他搜索中可以贪心的题,搜索顺序显得十分重要。

在迷宫、网格类的题目中,以左上->右下为例,右下左上就明显比左上右下优秀。

在一些推断搜索题中,从已知信息最多的地方开始搜索显然更加优秀。

在一些题中,先搜某个值大的,再搜某个值小的(比如树的度数,产生答案的预计(A*)),速度明显更快。

(原帖来自https://www.cnblogs.com/fenghaoran/p/6391016.html

 

【例题】

codevs1288 埃及分数

在古埃及,人们使用单位分数的和(形如1/a的, a是自然数)表示一切有理数。

如:2/3=1/2+1/6,但不允许2/3=1/3+1/3,因为加数中有相同的。

对于一个分数a/b,表示方法有很多种,但是哪种最好呢?

首先,加数少的比加数多的好,其次,加数个数相同的,最小的分数越大越 好。

如:19/45=1/3 + 1/12 + 1/180 19/45=1/3 + 1/15 + 1/45 19/45=1/3 + 1/18 + 1/30,

19/45=1/4 + 1/6 + 1/180 19/45=1/5 + 1/6 + 1/18. 最好的是最后一种,因为1/18最大。

给出a,b(0<a<b<1000),编程计算最好的表达方式。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define LL long long int
using namespace std;
LL a,b,depth,FLAG=1,zZ[101010],Ans[101010],Maxx=10101000;
LL gcd(LL a,LL b){return b>0?gcd(b,a%b):a;} //辗转相除法求最大公约数 
void dfs(LL now,LL a,LL b,LL last,LL depth){
    if(now==depth-1){
        if(a!=1)return;
        if(b<Maxx && b>last){
                zZ[now+1]=b;FLAG=0;Maxx=b;
                for(LL i=1;i<=now+1;++i){Ans[i]=zZ[i];}
        }
        return;
    }
    if(a*(last+1)>=b*(depth-now) || last>Maxx || a==0)return; //第一个是可行性剪枝,是个十字相乘式,建议移项看
    for(LL i=last+1,K=(depth-now)*b/a;i<K;++i)
    {
        LL newa=a*i-b,newb=b*i,G=gcd(newb,newa);
        newa/=G,newb/=G;zZ[now+1]=i;
        dfs(now+1,newa,newb,i,depth);zZ[now+1]=0;
    }
}
int main() {
    scanf("%lld %lld",&a,&b);
    if(a==1){printf("%lld",b);return 0;}
    for(int i=2;FLAG;++i)dfs(0,a,b,(b/a),i);                        //迭代搜索,i为深度 
    for(int i=1;Ans[i]!=0;++i)printf("%lld ",Ans[i]);
    return 0;
}

这道题可行性和最优性剪枝都要加,最后一个是因为要除a,是零就得剪掉。搜索顺序是按分母从小到大枚举的。

 

poj1011 Sticks

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;

/*【Sticks】

题意:给出n根小棒的长度stick[i],已知这n根小棒原本由若干根长度相同的长木棒(原棒)分解而来。求出原棒的最小可能长度。

思路:dfs+剪枝。蛮经典的题目,重点在于dfs剪枝的设计。先说先具体的实现:求出总长度sum和小棒最长的长度max,
则原棒可能的长度必在max~sum之间,然后从小到大枚举max~sum之间能被sum整除的长度len,
用dfs求出所有的小棒能否拼凑成这个长度,如果可以,第一个len就是答案。

下面就是关键的了,就是这道题dfs的实现和剪枝的设计:
1.以一个小棒为开头,用dfs看看能否把这个小棒拼凑成len长,用vis[i]记录下用过的小棒,然后继续以另外一个小棒为开头,以此类推。
2.小棒的长度从大到小排序。3.如果当前最长的小棒不能拼成len长,那么就返回前一步,不用再继续搜索这一种情况。
4.最重要的,就是比如说17,9,9,9,9,8,8,5,2……17与第一个9组合之后dfs发现不能拼成len,那么17就不和后面9组合了,而直接和8开始组合。  */

const int Max = 65;
int n, len, stick[Max];
bool flag, vis[Max];

bool cmp(int a,int b) { return a>b; }

void dfs(int dep, int now_len, int u){   // dep为当前已被用过的小棒数,u为当前要处理的小棒。
    if(flag) return;
    if(now_len == 0){   //  当前长度为0,寻找下一个当前最长小棒。
        int k = 0;
        while(vis[k]) k ++;  //  寻找第一个当前最长小棒。
        vis[k] = true;
        dfs(dep + 1, stick[k], k + 1);
        vis[k] = false;
        return;
    }
    if(now_len == len){  //  当前长度为len,即又拼凑成了一根原棒。
        if(dep == n) flag = true;   //  完成的标志:所有的n根小棒都有拼到了。
        else dfs(dep, 0, 0);
        return;
    }
    for(int i = u; i < n; i ++)
        if(!vis[i] && now_len + stick[i] <= len){
            if(!vis[i-1] && stick[i] == stick[i-1]) continue;  //  不重复搜索:最重要的剪枝。
            vis[i] = true;
            dfs(dep + 1, now_len + stick[i], i + 1);
            vis[i] = false;
        }
}

int main(){
    while(scanf("%d", &n) && n != 0){
        int sum = 0;
        flag = false;
        for(int i = 0; i < n; i ++){
            scanf("%d", &stick[i]);
            sum += stick[i];
        }
        sort(stick,stick+n,cmp); //从大到小排序。
        for(len = stick[0]; len < sum; len ++)
            if(sum % len == 0){  //  枚举能被sum整除的长度。
                memset(vis, 0, sizeof(vis));
                dfs(0, 0, 0);
                if(flag) break;
            }
        printf("%d\n", len);
    }
    return 0;
}

 

                                               ——时间划过风的轨迹,那个少年,还在等你。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值