搜索(4)dfs的剪枝与优化

目录

一、剪枝方法概述

1.优化搜索顺序

2.排除等效冗余

3.可行性剪枝

4.最优化剪枝

5.记忆化搜索

二、例题

1.小猫爬山

二、数独问题

三、木棒

四、生日蛋糕

总结


 

一、剪枝方法概述

1.优化搜索顺序

1如在小猫爬山中,按照猫的重量递减的顺序来搜索

2在数独问题中,优先搜索能填的合法数字最少的位置。

2.排除等效冗余

在搜索过程中,如果我们能够判定从搜索树的当前节点上沿着某几条不同的分支的子树是等价的,那么我们只需要对一条子树进行搜索。

3.可行性剪枝

在搜索过程中及时对当前状态进行检查,如果发现分支已经到达递归边界,就执行回溯。

4.最优化剪枝

记录当前已经搜到的最优值,如果目前花费的代价超过了最优值,则直接剪枝

5.记忆化搜索

二、例题

1.小猫爬山

直觉:遍历所有猫,深搜过程记录当前已经开的车数量,记录已经搜过了几只猫。

深搜过程中,由于猫的重量不可能每次掐好填满某辆缆车,因此先遍历之前的缆车看能不能放下,若可以就放,不行就新开一辆。每次dfs下一层之后都需要回溯,因为不确定放到哪个缆车才是最优方案

剪枝:如果已经开的车的数量大于要求,就剪枝(可行性)

       如果当前开的车的数量大于目前最优解,剪枝(最优化)

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N =20;

int n;
int w;
int c[N];
int cab[N];//记录某条缆车装了多少重量
int ans=N;

//对于每一只猫有两种决策 放入已租用的缆车 和 直接新开一个缆车
void dfs(int now,int cnt)//已经搜索了几只猫   当前用了多少个缆车
{
    if(cnt>=ans) return ;
    if(now==n)
    {
        ans=cnt;
        return;
    }

    for(int i=1;i<=cnt;i++)//分配到已租用的缆车
    {
        if(cab[i]+c[now]<=w)
        {
            cab[i]+=c[now];
            dfs(now+1,cnt);
            cab[i]-=c[now];
        }
    }
    //新开
    cab[cnt+1]+=c[now];
    dfs(now+1,cnt+1);
    cab[cnt+1]-=c[now];

}


int main()
{
    cin>>n>>w;
    for(int i=0;i<n;i++)
        cin>>c[i];
    sort(c,c+n,greater<int>());
    dfs(0,0);
    cout<<ans;
    return 0;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4221289/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二、数独问题

 先看看简单版数独

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =11;
bool row[N][N],col[N][N],cell[N][N][N];
char g[N][N];


bool dfs(int x,int y)
{
    if(y==9)  x++,y=0;
    if(x==9)
    {
        for(int i=0;i<9;i++)
            cout<<g[i]<<endl;
        return true;
    }
    if(g[x][y]!='.')  return dfs(x,y+1);
    
    
    for(int i=0;i<9;i++)
    {
        if(!row[x][i]&&!col[y][i]&&!cell[x/3][y/3][i])   
        {
            g[x][y]=i+'1';
            row[x][i]=col[y][i]=cell[x/3][y/3][i]=true;
            if(dfs(x,y+1))  return true;
            row[x][i]=col[y][i]=cell[x/3][y/3][i]=false;
            g[x][y]='.';
        }
    }
    
    return false;
    
}

int main()
{
    for(int i=0;i<9;i++)  
    {
        cin>>g[i];
        for(int j=0;j<9;j++)
        {
            if(g[i][j]!='.') 
            {
                int t=g[i][j]-'1';
                row[i][t]=col[j][t]=cell[i/3][j/3][t]=true;
            }
        }
        
    }
    
    
    dfs(0,0);
    return 0;
}

现在考虑剪枝:

优化搜索顺序:当有几个空位的数字确定时,由于数独的特性,会导致其他很多空位的选择变少,因此我们要从可选择数字最少的地方开始深搜。

排除等效冗余:任意一个状态下,我们只需要找一个位置填数即可,而不是找所有的位置和可填的数字.
位运算:很明显这里面check判定很多,我们必须优化这个check,所以我们可以对于,每一行,每一列,每一个九宫格,都利用一个九位二进制数保存,当前还有哪些数字可以填写.

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N =9,M=1<<N;

int ones[M],map[M];//某个状态能填的数的个数  lowbit出的该填的数
int row[N],col[N],cell[3][3];
char str[100];

void init()
{
    for(int i=0;i<N;i++) row[i]=col[i]=(1<<N)-1;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            cell[i][j]=(1<<N)-1;
}

void draw(int x,int y,int t,bool is_set)//在某个位置填入或是删除t 更新插入这个数带来的影响
{
    if(is_set) str[x*N+y]='1'+t;
    else str[x*N+y]='.';
    
    int v=1<<t;
    if(!is_set) v=-v;//如果是删除  下面就要加上v
    
    row[x]-=v;
    col[y]-=v;
    cell[x/3][y/3]-=v;
}

int lowbit(int x)
{
    return x&-x;
}

int get(int x,int y)
{
    return row[x]&col[y]&cell[x/3][y/3];
}

bool dfs(int cnt)
{
    if(!cnt) return true;
    
    int minv=10;
    int x,y;

    for(int i=0;i<N;i++)
        for(int j=0;j<N;j++)
            if(str[i*N+j]=='.')
            {
                int state=get(i,j);
                if(ones[state]<minv)
                {
                    minv=ones[state];
                    x=i,y=j;
                }
            }
            
    int state=get(x,y);
    for(int i=state;i;i-=lowbit(i))
    {
        int t=map[lowbit(i)];
        draw(x,y,t,true);
        if(dfs(cnt-1)) return true;
        draw(x,y,t,false);
    }
    return false;
            
}



int main()
{
    for(int i=0;i<N;i++) map[1<<i]=i;
    for(int i=0;i<1<<N;i++)
        for(int j=0;j<N;j++)
            ones[i]+=i>>j&1;
    
    while(cin>>str,str[0]!='e')
    {
        init();//初始化为每个位置每个数都可以填
        
        int cnt=0;
        for(int i=0,k=0;i<N;i++)//i,j是坐标 k是字符串下标
            for(int j=0;j<N;j++,k++)
                if(str[k]!='.')
                {
                    int t=str[k]-'1';
                    draw(i,j,t,true);
                }
                else cnt++;//可填的空位数
        dfs(cnt);
        puts(str);
    }
}

三、木棒

 直觉想法:设定原始木棒长度len,当前正在拼的木棒长度cab,当前正在拼第stick个原始木棒,接下来该从哪个木棒开始选,上次选了第i根,则last=i+1,拼新一根时last=0)。当cab==len时,拼下一根。从last开始枚举,试探将别的木棒拼入,能拼入的要求是:没被选择过、cab+这根木棒的长度小于len。拼入之后就继续dfs,没有木棒可以拼入就return false

剪枝:

1.最优性剪枝:从小到大枚举长度,如果成功就直接break了。

2.可行性剪枝:可以预先知道在len长度下将有多少根木棒,如果当前已经拼好的木棒超过这个数,直接返回false:当该木棍在开头和结尾都不可以使用的时候,那么该方案就失败了

3.排除等效冗余:如果长度为l的木棒已经被尝试过,回溯之后继续枚举时,也不需要在枚举长度为l的其他木棒,因为子树都是等价的。

补充:

如果木棍在第一个位置不合法的话:假设我们把该木棍用在其他的位置合法了,那么我们就可以把顺序颠倒一下,把该木棒放到第一个也是合法的,该假设与条件不符。
同理:如果木棍在最后一个位置不合法的话:假设我们把该木棍用在其他的位置合法了,那么我们就可以把顺序颠倒一下,把该木棒放到最后一个也是合法的,该假设与条件不符。

但是,放到中间的木棍就不一样了,因为这种情况也可能是该木棍和前面组成该木棒的某一个木棍不合适,所以才造就了他的不合法。在这种情况下,可能换一种方法就合适了

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N =70;

int a[N];
int n;
int len;
int cnt;
bool st[N];


//正在拼第stick根原始木棒
//cab当前在拼的原始木棒的长度
//接下来该从那个木棒开始选择(上一次选了i,last=i+1,拼新一根时last=0)
bool dfs(int stick,int cab,int last)
{
    if(stick>cnt) return true;
    if(cab==len) return dfs(stick+1,0,0); //拼新一根 last置为0;
    
    int fail=0;//剪枝2
    for(int i=last;i<n;i++)
    {
        if(!st[i]&&cab+a[i]<=len&&fail!=a[i])
        {
            st[i]=true;
            if(dfs(stick,cab+a[i],i+1)) return true;
            fail=a[i];
            st[i]=false;
            if(cab==0||cab+a[i]==len) return false;
        }
    }
    return false;
}



int main()
{
    while(cin>>n,n!=0)
    {
        int val=0,sum=0;
        for(int i=0;i<n;i++)
        {
            cin>>a[i];
            sum+=a[i];val=max(val,a[i]);
        }
        sort(a,a+n);
        reverse(a,a+n);
        
        for(len=val;len<=sum;len++)
        {
            if(sum%len) continue;
            
            cnt=sum/len; //该有多少根原始木棒
            memset(st,false,sizeof st);
            if(dfs(1,0,0)) break;
        }
        cout<<len<<endl;
    }
}

四、生日蛋糕

  外表面:侧面积加上表面

搜索框架:从下往上搜索,枚举每层蛋糕的半径和高度,本题蛋糕层数的计数从上往下

搜索面对的状态:正在枚举第dep层,当前外表面积s,当前体积v和第dep+1层的高度和半径。因此我们可以用 h 和r 两个数组记录每层的高度和半径

在搜索过程中,上表面的面积之和等于最低层的圆面积,因此可以在第M层直接累加到s中,这样在第M-1层之前的搜索中,只需要计算侧面积

剪枝:

1.上下界剪枝:取极限情况,在枚举第dep层时,r一定要大于等于dep,因为r递增且为正整数。考虑极限情况,r一定要小于dep+1层的半径-1,取极限情况:当前是最后一层且H最小=1,则r最大值一定小于sqrt(N-v)。因为\pi R^2H=\pi (N-v).

        其次,h的取值要在dep到dep+1层的H再-1。同样的还要小于(N-v)/R^2.

2.优化搜索顺序:R为平方项,对体积影响大,所以先枚举R,在枚举H

3.可行性剪枝:

可以预处理从上往下前i层的最小侧面积和体积。

如果当前体积v加上1~dep-1的最小体积大于N,则剪枝

4.最优化剪枝:如果当前表面积s大于当前最优值,则剪枝

5.最优化剪枝:利用h和r数组, 1~dep-1层的体积可以表示出来,表面积也可以表示出来。

n-v=\sum _{k=1}^{dep-1} h(k)*r(k)^2

S=2\sum _{k=1}^{dep-1}h(k)*r(k)

S=\frac{2}{r(dep)}*\sum _{k=1}^{dep-1} h(k)*r(k)*r(dep)\geq \frac{2}{r(dep)}*\sum h(k)*r(k)^2

\geqslant \frac{2(n-v)}{r(dep)}

所以当\frac{2(n-v)}{r(dep)}+s大于已经搜到的答案时,剪枝

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int N=25,INF=1e9;

int n,m;
int minv[N],mins[N];
int H[N],R[N];
int ans=INF;

void dfs(int u,int v,int s)
{
    if(v+minv[u]>n) return ;
    if(s+mins[u]>=ans) return ;
    if(s+2*(n-v)/R[u+1]>ans) return ;

    if(u==0)
    {
        if(v==n) ans=s;
        return;
    }
    for(int r=min(R[u+1]-1,(int)sqrt(n-v));r>=u;r--)
        for(int h=min(H[u+1]-1,n-v/r/r);h>=u;h--)
        {
            int t=0;
            if(u==m) t=r*r;
            R[u]=r,H[u]=h;
            dfs(u-1,v+r*r*h,s+2*r*h+t);
        }
}



int main()
{
    cin>>n>>m;

    for(int i=1;i<=m;i++)
    {
        minv[i]=minv[i-1]+i*i*i;
        mins[i]=mins[i-1]+2*i*i;
    }

    if(minv[m]>n)//无解
    {
        puts("0");
        return 0;
    }

    R[m+1]=H[m+1]=1e9;//  因为R,H的搜索范围  且dfs用到R[m+1]

    dfs(m,0,0);
    cout<<ans<<endl;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4231379/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结

来自《算法竞赛进阶指南》:
搜索算法面对的状态可以看做一个多元组,其中每一元都是问题中的一个维度。一定要注意提取这些信息,构建合适的搜索框架

剪枝过程实际就是针对每个维度与该维度的边界条件,加以推导,得到一个相应的不等式来减少搜索树分支的扩张。

为了进一步提高搜索的效率,除了当前花费的代价外,还可以计算未来至少需要花费的代价进行预算。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DFS深度优先搜索)是一种常见的图遍历算法,它使用递归或栈的方式,从一个顶点出发,沿着一条路径一直到达最深的节点,然后回溯到上一层继续遍历其他节点。DFS常被用于解决图的连通性问题、路径问题等。在实际应用中,可以使用DFS进行状态搜索、图的遍历、拓扑排序等。 剪枝是指在搜索过程中,通过一系列的策略判断,提前终止当前搜索分支,并跳过一些无用的搜索路径,从而减少搜索时间。剪枝的核心在于提前排除某些明显不符合条件的状态,以减少无效搜索的时间开销,提高效率。在算法设计中,剪枝通常会利用一些特定的性质或条件进行判断,从而缩小搜索空间。 动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划通常用于求解最优化问题,它通过定义状态和状态转移方程,采用自底向上的思路,逐步求解每个子问题的最优值,最终得到原问题的最优解。动态规划的核心是存储已经计算过的子问题的解,避免了重复计算。 贪心算法是一种基于局部最优解的策略,它通过每一步选择在当前状态下最优的解,以期望得到全局最优解。贪心算法的基本思想是由局部最优解推导出全局最优解,通常通过贪心选择性质、最优子结构和贪心选择构成三部分。贪心算法相比其他算法,如动态规划,它的优势在于简单、高效,但缺点在于不能保证获取到全局最优解,只能得到一个近似解。 综上所述,DFS剪枝、动态规划和贪心算法算法设计和问题求解中都发挥着重要的作用。具体使用哪种算法取决于问题的性质和要求,需要在实际应用中进行综合考虑和选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值