剪枝

本文介绍了搜索算法中的剪枝技术,包括优化搜索顺序、排除重复情况、可行性剪枝、最优性剪枝和记忆化等策略。通过举例说明在数独、拼木棍和生日蛋糕体积计算等题目中的应用,阐述了不同剪枝方法如何有效减少搜索空间,提高算法效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  剪枝可谓是搜索的灵魂所在,我们知道搜索是个愣头青小伙,一路撞到底可能都撞不到答案,他还可能要撞很多次。所以有什么方法可以让他撞的次数少一点呢?我们知道搜索会形成一个搜索树,这其中有很多的枝杈,但是他们中许多其实是无用或者重复的,我们就可以把他们都”剪“掉,或者我们可以使用别的方法去减少枝杈,这样的过程称为剪枝,我们之后的搜索题目都可以有体现。
  常见的套路剪枝方法有这几种:
1、优化搜索顺序:有时候需要由大到小倒序。
2、排除重复情况:如果一个状态之前已经在之前被搜索过,那么我们没必要再搜索一次。
3、可行性剪枝:如果一个状态本来就无法达到最终状态,那么我们根本不需要再进行下去。形象的理解就是我们如果看到前面有一堵墙,那么我们就不会再走这条路,因为这条路是不通的,而不是一直走到撞墙在回去。
4、最优性剪枝:如果当前花费的代价已经超过了当前的最优解,那么也不需要再搜索下去了。
5、记忆化:记录每个状态的搜索结果,在重复遍历一个状态时直接返回。相当于我们在对图进行深搜的时候,标记一个节点是否被访问过。记忆化搜索也可以用在动态规划上。


【例题】Sudoku1(poj2676)

  题意就是一个数独游戏。

  爆搜,无剪枝可过。

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int a[10][10];
bool check(int x,int y,int now)
{
    for(int i=0;i<9;i++)
        if(a[x][i]==now)
            return false;
    for(int i=0;i<9;i++)
        if(a[i][y]==now)
            return false;
    int u=x-x%3,v=y-y%3;
    for(int i=u;i<u+3;i++)
        for(int j=v;j<v+3;j++)
            if(a[i][j]==now)
                return false;
    return true;    
}
bool flag;
void dfs(int x,int y)
{
    if(flag || x==9)
    {
        flag=true;
        return;
    }
    while(a[x][y])
    {
        if(y==8)
        {
            y=0;
            x++;
            if(x==9){flag=true; return;}
        }
        else y++;
    }
    for(int i=1;i<=9;i++)
    {
        if(check(x,y,i))
        {
            a[x][y]=i;
            if(y==8) dfs(x+1,0);
            else dfs(x,y+1);
            if(flag) return;
            a[x][y]=0;
        }
    }
}
char s[10][10];
int main()
{
    int T;scanf("%d",&T);
    while(T--)
    {
        for(int i=0;i<9;i++)
        {
            scanf("%s",s[i]);
            for(int j=0;j<9;j++){
                a[i][j]=s[i][j]-'0';
            }
        }
        flag=false; dfs(0,0);
        for(int i=0;i<9;i++)
        {
            for(int j=0;j<9;j++)
                printf("%d",a[i][j]);
            printf("\n");
        }
    }
    return 0;
}

【例题】Sudoku2(poj3074)

  题意还是一个数独游戏

  在爆搜的基础上用位运算剪枝。由于原来的搜索方法是从左上角一个一个找到右下角,其中有两个可以优化的地方:
  1、原来的我们在找到一个位置的时候有可能它已经被填写,我们还需要找到它往后没有被填的位置,消耗了大量时间。我们需要精准的找到未被填写的位置。
  2、我们填数的方式太过于笨拙。我们思考人如何填写数独,一般先找到最容易确定的位置(也就是未填写的最少)来填写,一步一步推出来。
  
  所以我们把每一行,每一列,每一个九宫格用一个9位二进制表示,一开始全部为1,填数则将对应位变为0。我们可以对于一行一列一个九宫格通过与运算可以得出一个val,他就代表在当前行列九宫格没有填的数的二进制表示,我们可以通过lowbit运算找到没有填的位,再找到最小的这样的位置填写。

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int row[10],col[10],tub[20];
int cnt[600],num[600];
char str[10][10];
//填了的为0,没填的为1,可以用lowbit找出没填的 
int get(int x,int y){return (x/3*3)+(y/3);}
void change(int x,int y,int k)
{
    row[x]^=1<<k;
    col[y]^=1<<k;
    tub[get(x,y)]^=1<<k;
}
bool dfs(int now)
{
    if(now==0) return true;
    int tmp=10,x,y;
    for(int i=0;i<9;i++)
        for(int j=0;j<9;j++)
        {
            if(str[i][j]!='.') continue;
            int val=row[i] & col[j] & tub[get(i,j)];//没填的 
            if(!val) return false;
            if(cnt[val]<tmp)
            {//找到最容易确定的位置(也就是未填写的最少)来填写 
                tmp=cnt[val];
                x=i,y=j;    
            }
        }
    int val = row[x] & col[y] & tub[get(x,y)];
    for(;val;val-=val&-val)
    {
        int k=num[val&-val];
        str[x][y]=k+'1';
        change(x,y,k);
        if(dfs(now-1)) return true;
        change(x,y,k);
        str[x][y]='.';
    }
    return false;
}
char s[100];
int main()
{
    for(int i=0;i<1<<9;i++)
        for(int j=i;j;j-=j&-j)
            cnt[i]++;//计算一个数的二进制位有几个1 
    for(int i=0;i<9;i++)
        num[1<<i]=i;
    while(~scanf("%s",s) && s[0]!='e')
    {
        for(int i=0;i<9;i++)
            for(int j=0;j<9;j++)
                    str[i][j]=s[i*9+j];
        for(int i=0;i<9;i++) row[i]=col[i]=tub[i]=(1<<9)-1;
        int tot=0;
        for(int i=0;i<9;i++)
            for(int j=0;j<9;j++)
            {
                if(str[i][j]!='.') change(i,j,str[i][j]-'1');
                else tot++;
            }
        dfs(tot);
        for(int i=0;i<9;i++)
            for(int j=0;j<9;j++)
                s[i*9+j]=str[i][j];
        puts(s);
    }
    return 0;
}

【例题】Sticks(poj1011)

  给出一堆小木棍,将这些小木棍拼成一些大木棍,要求每个大木棍的长度相同。问大木棍最小是多少。

  非常经典的搜索题,写一下我在草稿纸上写下的思路吧:

  大体思路:枚举长度len(保证sum%len==0),搜索大木棍的组成。
  则搜索函数的参数有
dfs(已经处理完的大木棍数量,现在正在处理的大木棒已经增加的长度,当前正在处理的小木棒的下标)
  那么剪枝有:
  1、枚举因数len时可以用sqrt来枚举,枚举复杂度降至2*sqrt(n)
  2、从大到小排序,让大的先尝试
  3、记录一个last,表示上次拼接的小木棍长度,那么假如这个长度不成功则相同长度的都不需要再考虑。、
  4、检查木棍时,任意一个失败都意味着前面出现了问题,就可以直接退出了。(这是一个大剪枝,因为我们大部分情况都是失败的)

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<cmath>
using namespace std;
const int N=110;
int a[N],n;
int len,sum,sticks;
bool vis[N],flag;
bool cmp(int x,int y){return x>y;}
bool dfs(int cnt,int now,int k)
{
    if(cnt>sticks) 
        return true;
    if(now==len)
        return dfs(cnt+1,0,1);

    int last=0;
    for(int i=k;i<=n;i++)
        if(!vis[i] && now+a[i]<=len && last!=a[i])
        {
            vis[i]=true;
            if(dfs(cnt,now+a[i],i+1)) return true;
            last=a[i];
            vis[i]=false;
            if(now==0) return false;
        }
    return false;
}
int main()
{
//  freopen("a.in","r",stdin);
//  freopen("a.out","w",stdout);
    while(scanf("%d",&n)&&n)
    {
        sum=0;
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            sum+=a[i];
        }
        sort(a+1,a+n+1,cmp);
        flag=false; int ans=99999999;
        int i;
        for(i=1;i<=sqrt(sum);i++)
        {
            if(sum%i==0 && i>=a[1])
            {
                len=i;
                sticks=sum/len;
                memset(vis,0,sizeof(vis));
                if(dfs(1,0,1)) ans=min(ans,len);
            }
            if(i*i!=sum && sum%i==0 && (sum/i)>=a[1])
            {
                len=sum/i;
                sticks=sum/len;
                memset(vis,0,sizeof(vis));
                if(dfs(1,0,1)) ans=min(ans,len);
            }
        }
        printf("%d\n",ans);
    }
}

【例题】 生日蛋糕(poj1190)

7月17日是Mr.W的生日,ACM-THU为此要制作一个体积为Nπ的M层生日蛋糕,每层都是一个圆柱体。 
设从下往上数第i(1 <= i <= M)层蛋糕是半径为Ri, 高度为Hi的圆柱。当i < M时,要求Ri > Ri+1且Hi > Hi+1。 
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积Q最小。 
令Q = Sπ 
请编程对给出的N和M,找出蛋糕的制作方案(适当的Ri和Hi的值),使S最小。 
(除Q外,以上所有数据皆为正整数) 

圆柱公式 :
体积V = πr2h π r 2 h
侧面积A’ = π2rh π 2 r h
底面积A = πr2 π r 2
首先读题:
  本题忽略 π π 只将它后面的有理数进行计算,其次对于表面积,整个蛋糕的上表面面积之和等于最大圆的底面积。所以我们只需要计算侧面积,最底那层计算底面积即可。

  仍然是一道经典的搜索剪枝题目。我首先把题目所描述的(1~M从底向上)倒过来(1~M从上到底)方便处理,然后我们从下(最大那层)往上搜索。

搜索思路:
  那么我们首先爆搜,我们由于高度和半径下一层都要大于上一层,可以通过记录上一层的半径和高枚举当前一层的半径和高。枚举的下界自然就是当前层的层数(以半径为例,假设第一层半径为1,因为第二层的半径就最少比他大1,那么每一层最小的半径就是层数),枚举的上界呢就是上一层的半径或者高度减一。 但是最大那层的半径和高度的上限应该是多少呢?我们从极端情况考虑,当体积 N N 不变,半径r最大时,高度 h h 最小为1,那么r最大为 N N ,同理可得 h h 最大为N

剪枝:
  爆搜的思路大概就是这样,那么我们显然发现这样慢的不行,考虑几个剪枝。
1、枚举上限:我们可以通过圆柱体体积计算公式 πr2h=π(NsumV)[sumV] π r 2 h = π ( N − s u m V ) [ s u m V 为 当 前 体 积 和 ] 得到:
ri r i 的枚举范围为 depi d e p i min(NsumV,ri11) m i n ( N − s u m V , r i − 1 − 1 )
hi h i 的枚举范围为 depi d e p i min(NsumVr2,hi11) m i n ( N − s u m V r 2 , h i − 1 − 1 )

2、在第一条的枚举中倒序搜索。因为这样我们可以尽量快的找到合法状态。

3、还需要一些可行性剪枝。我们预处理到每层最小的体积和和最小的侧面积和,//如果当前体积加上剩下层的最小体积>N或者当前表面积加上剩下层的最小表面积>最优解就剪枝 。相当于看到前面是墙就转弯。

4、还是一个可行性剪枝,如果把剩下的体积全部使用,在表面积最小的情况下仍大于最优解,那么可以剪枝。这个剪枝需要推导一下公式。
2rh=S 2 ∗ r ∗ h = S
rrh=V r ∗ r ∗ h = V
V2/r=S 即 V ∗ 2 / r = S
S+sumS= S + s u m S =
V2/r+sumS= V ∗ 2 / r + s u m S =
(NsumV)2/r+sumS>=minn ( N − s u m V ) ∗ 2 / r + s u m S >= m i n n 时return

// luogu-judger-enable-o2
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
using namespace std;
int M,N;
int minn;
int minV[20],minS[20];
/*体积V = r^2h(pi)
侧面积A' = 2rh(pi) 
底面积A = r^2(pi)*/
//从上往下 1~M层 
void dfs(int now,int V,int S,int lastr,int lasth)
{//当前层数Now ,目前体积V,目前表面积S,上一层半径lastr,上一层高lasth 
    if(now==0)
    {
        if(V==N && minn>S)
            minn=S;
        return;
    }
    if(V+minV[now]>N || S+minS[now]>minn)
        return;//如果当前体积加上剩下层的最小体积>N或者当前表面积加上剩下层的最小表面积>最优解就剪枝 

    if( int(2*(N-V)/lastr) + S >= minn) 
        return;//把剩下的所有体积,全部使用表面积大于最优解 
    for(int i=min(int(sqrt(N-V)),lastr-1);i>=now;i--)//枚举下一层的半径
    {
        if(now==M) S=i*i;
        for(int j=min(int((N-V)/i*i),lasth-1);j>=now;j--)
        {
            dfs(now-1,V+i*i*j,S+i*j*2,i,j);
        }
    } 
}
int main()
{
    scanf("%d%d",&N,&M);
    minV[0]=minS[0]=0;
    for(int i=1;i<=M;i++)//预处理到某一层的最小侧面积和最小体积 
    {
        minV[i]=minV[i-1]+i*i*i;
        minS[i]=minS[i-1]+2*i*i;    
    }
    minn=999999999;
    dfs(M,0,0,sqrt(N),N);//当N时r最大为sqrt(N),h最大为N 
    if(minn==999999999) minn=0;
    printf("%d\n",minn);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值