DFS剪枝例题

常见的剪枝方式

一、优化搜索顺序

大部分情况下,我们应该优先搜索分支较少的结点;

在没有剪枝的情况下,因为最终都会枚举完全部的点,所以是一样的;

但是在有剪枝的情况下,走分支较少的点,剪枝的效果更明显;

如下图所示,红色勾起来的代表剪枝处;

在这里插入图片描述

二、排除等效冗余

简单来说,如果不考虑顺序,那么优先考虑组合,而不要考虑排列

即不要搜索重复状态;

比如现在有 < 1 , 2 > 和 < 2 , 1 > <1,2>和<2,1> <1,2><2,1>,不考虑顺序的话,枚举一个就够了;

三、可行性剪枝

在搜索的时候,发现搜到一半不合法了,就可以提前退出了;

四、最优性剪枝

比如我们要求一个最小值,发现当前搜到的值已经大于等于最优解了,就可以提前退出了;

五、记忆化搜索(DP)

一般这种跟DP归在一起,主要还是由上面四种手段来剪枝;

例题

小猫爬山

题面

小猫爬山
在这里插入图片描述
在这里插入图片描述

思路

对于每只猫来说,我们可以考虑放到之前的某辆车里,也可以新开一辆车;

因此我们只需要枚举这两种情况即可;

接着考虑剪枝;

显然先放重的猫(剩余空间少了),我们的分支会少一些;

再配合上常规的可行性剪枝与最优化剪枝即可;

Code

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 1e1 + 10;

int n,w;

int a[N],ans = 1e9;

int sum[N];

void dfs(int now,int group){
    if(group >= ans) return; //最优化剪枝
    if(now == n + 1){
        ans = group;
        return;
    }
    for(int i=1;i<=group;++i){
        if(sum[i] + a[now] <= w){ //可行性剪枝
            sum[i] += a[now];
            dfs(now+1,group);
            sum[i] -= a[now];
        }
    }
    sum[++group] = a[now];
    dfs(now+1,group);
    sum[group--] = 0;
}

void solve(){
    cin >> n >> w;
    for(int i=1;i<=n;++i) cin >> a[i];
    //优化搜索顺序
    sort(a+1,a+1+n,greater<int>());
    dfs(1,1);
    cout << ans << '\n';
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}

数独

题面

传送门
在这里插入图片描述

思路

对于每个空格来说,我们可以尝试着填1~9每个数字;

然后爆搜即可;


接着考虑优化,每一个行,每一个列,每一个九宫格都不能有重复的数字;

那么我们可以考虑使用状压,用一个数来表示某行、某列、某个九宫格的状态;

我们用 1 1 1表示某一位可以用,用 0 0 0表示某一位不能用;

那么我们只要按位与,就可以知道当前哪些状态是可以用的;


接着考虑搜索顺序,我们想尽可能的搜索分支较少的状态;

那么什么状态的分支少?

不难想到如果某个状态的二进制表示上1的数量越少,那么可以填的数字就越少,因此搜索的分支越少;

Code

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

typedef long long ll;

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

//值是一个状态
int row[N],col[N],cell[3][3]; 

int log_2[M],ones[M]; //有几个1

char str[110];

void init(){
    //初始化log_2,ones
    for(int s=0;s<M;++s)
        for(int j=0;j<N;++j)
            ones[s] += (s >> j & 1);
    for(int i=0;i<N;++i) log_2[1<<i] = i;
    for(int s=1;s<M;++s)
        if(log_2[s] == 0) log_2[s] = log_2[s-1];
}
//flag = 1在(i, j)处填上u + 1,
//否则把(i, j)处的u + 1删掉
void draw(int i,int j,int u,bool flag){
    if(flag) str[i*N+j] = '1' + u;
    else str[i*N+j] = '.';
    int v = 1 << u; //直接xor某一位,自然0变1 1变0
    row[i] ^= v;
    col[j] ^= v;
    cell[i/3][j/3] ^= v;
}
int get_state(int i,int j){ //返回的状态1越多,说明选择越多
    //都为1说明行、列、九宫格不冲突
    return row[i] & col[j] & cell[i / 3][j / 3];
}
int lowbit(int x){
    return x & -x;
}
bool dfs(int cnt){ //cnt 表示还有多少个格子要填
    //全部填上则ok
    if(cnt == 0){
        return true;
    }
    //找出选择分支最少的 优化搜索顺序
    int minv = 1e9,px,py;
    for(int i=0;i<N;++i)
        for(int j=0;j<N;++j)
            if(str[i*N+j] == '.'){
                int state = get_state(i,j);
                if(ones[state] < minv){
                    minv = ones[state];
                    px = i,py = j;
                }
            }
    int state = get_state(px,py);
    //看看这一位填哪个数更合适
    for(int i=state;i;i-=lowbit(i)){
        //每一次枚举lowbit(i)这一位
        int u = log_2[lowbit(i)];
        draw(px,py,u,true);
        if(dfs(cnt - 1)) return true;
        draw(px,py,u,false);
    }
    return false;
}
void solve(){
    init();
    while(cin >> str,str[0] != 'e'){
        //一开始每个位置都是可以放的
        //某一位是1表示可以放
        for(int i=0;i<N;++i)
            col[i] = row[i] = M - 1;
        for(int i=0;i<3;++i)
            for(int j=0;j<3;++j)
                cell[i][j] = M - 1;
        int cnt = 0;
        for(int i=0;i<N;++i)
            for(int j=0;j<N;++j)
                if(str[i*N+j] != '.'){
                    int u = str[i*N+j] - '1';
                    draw(i,j,u,1);
                }
                else ++cnt;
        dfs(cnt);
        cout << str << '\n';
    }
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}

木棒

题面

传送门
在这里插入图片描述
在这里插入图片描述

思路

首先明确大方向,线性的暴力枚举大棒的长度,不断尝试是否可行;

接着考虑剪枝


剪枝一、

因为大棒都是等长的,因此小棒长度的总和必然是大棒的倍数;

s u m = k ∗ l e n g t h sum = k * length sum=klength

sum为小棒长度和、k为大棒个数、length为大棒长度
剪枝二、

优化搜索顺序,我们想搜索的分支尽可能少;

那么我们应该先枚举长度较大的小棒,这样剩下的地方少,选择自然就少;

剪枝三、

因为我们只关心大棒的长度,而不关心小棒是如何组成大棒的;

因此我们只需要枚举组合,而不需要排列

即小棒 1 , 2 , 3 1,2,3 1,2,3构成大棒与 1 , 3 , 2 1,3,2 1,3,2构成大棒是相同的;

剪枝四、

如果当前小棒加到当前大棒失败了,那么后面长度相同的小棒我们是可以忽略的;

证明

可以考虑反证法,假设当前小棒 u u u放到当前大棒失败了,我们换上等长小棒 v v v却成功了;

因为长度相同,因此我们可以交换 u u u v v v,与假设矛盾;

剪枝五、

如果大棒的第一根小棒失败了,那么后面就不必再搜索了,一定失败;

证明

考虑反证法,假设这根小棒 3 3 3放第一根失败了,但是它最终却能成功;

那么小棒 3 3 3会放到另一根棒子中;

假设放蓝色大棒失败,放绿色大棒成功;

在这里插入图片描述
因为棒子内的顺序是无所谓的,因此我们可以把小棒 3 3 3换到绿棒的开头;

又因为蓝棒和绿棒在本题中都是大棒,没有区别;

设蓝棒的编号为大棒 1 1 1,绿棒的编号为大棒 2 2 2

因此我们可以交换蓝棒和绿棒,这样绿棒的编号变成大棒 1 1 1了;

而小棒 3 3 3在大棒 1 1 1的开头居然成功了,这与我们假设是相反的;

剪枝六、

如果小棒是大棒的最后一根棒子失败了,那么最终一定失败;

证明

考虑反证法

假设小棒 3 3 3在大棒 2 2 2结尾失败了,在大棒 3 3 3中成功了;

因为是结尾,那么所剩长度必然相同;

在这里插入图片描述
那么我们可以交换图中相等的两端,将小棒 3 3 3换回去;

发现居然成功了,与假设矛盾;

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 1e2 + 10;

int a[N],n;

bool used[N];

int sum,length;//小棒和、大棒长度

//第几个大棒、当前大棒已获得长度、从哪个小棒开始枚举
bool dfs(int u,int len,int start){
    if(u * length == sum) return 1;
    if(len == length) return dfs(u+1,0,1);
    //剪枝三、组合枚举
    for(int i=start;i<=n;++i){
        //可行性剪枝
        if(used[i] || len + a[i] > length) continue;
        used[i] = 1;
        if(dfs(u,len+a[i],i+1)) return 1;
        used[i] = 0;

        //剪枝五、开头失败必不行
        if(len == 0) return 0;

        //剪枝六、结尾失败必不行
        if(len + a[i] == length) return 0;

        //剪枝四、忽略相同长度
        for(int j=i;j<=n&&a[i] == a[j];++j){
            i = j;
        }
    }
    //如果可以的话上面就返回true了 到这肯定不行
    return 0;
}

void solve(){
    memset(used,0,sizeof used);
    sum = 0;
    for(int i=1;i<=n;++i){
        cin >> a[i];
        sum += a[i];   
    }
    //剪枝二、先枚举长度大的
    sort(a+1,a+1+n,greater<int>());
    length = 1;
    while(1){
        //剪枝一、必须为倍数
        if(sum % length == 0 && dfs(1,0,1)){
            cout << length << '\n';
            return;
        }
        ++length;
    }
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    while(cin >> n,n){
        solve();
    }
    return 0;
}

生日蛋糕

题面

传送门
在这里插入图片描述
在这里插入图片描述

思路

我们设定最上面的那一层为第一层,如下图所示;

在这里插入图片描述


表面积分为侧面积与圆面积;

不难想到无论有多少层,圆面的总面积总是最下面那一层的上侧面积

因此我们可以推出表面积的公式;

设最下面一层为第 u u u层;

S 总 = S 第 u 层 的 上 侧 面 积 + S 每 层 的 侧 面 积 S_总=S_{第u层的上侧面积} + S_{每层的侧面积} S=Su+S

S 总 = π R u 2 + ∑ i = 1 u 2 π R i H i S_总=\pi R_u^2+\sum_{i=1}^u2\pi R_iH_i S=πRu2+i=1u2πRiHi


接着考虑体积;

体积是每一层的体积之和,因此有;

V 总 = ∑ i = 1 u π R i 2 H i V_总=\sum_{i=1}^u \pi R_i^2 H_i V=i=1uπRi2Hi


因为题目要求半径与高度是从上层往下层递增的;

那么显然,第一层的最小半径和最小高度都为 1 1 1

第二层的最小半径和最小高度都为 2 2 2

以此类推;

有了上面的这些,我们就可以暴搜了;

现在考虑剪枝;


剪枝一、

考虑优化搜索顺序,我们期望先搜索分支数量较少的;

那么我们应该从最底层往最上层搜,因为下层占用的体积和表面积肯定比上层多;

而占用多了剩下的空间允许的分支肯定小了;

在同一层中,我们应先枚举 R R R再枚举 H H H

因为公式中, R R R是存在二次方的,而 H H H只存在一次方的;

因此

  1. 层间:从下到上
  2. 层内:先枚举半径再枚举高,半径由大到小,高度由大到小
剪枝二

考虑可行性剪枝,假设当前是第 u u u层,设 R ( i ) R(i) R(i)表示第 i i i层的半径;

因为半径从 1 1 1开始,到第 u u u层至少是 u u u

上限之一是必须比下一层小

上限之二是考虑剩余体积;

假设已经使用体积 V V V,那么剩余体积为 n − V n-V nV;

因为本题中忽略 π \pi π,有以下转换;

在这里插入图片描述

因此有 u ≤ R ( u ) ≤ { R ( u + 1 ) − 1 , n − V } m i n u≤R(u)≤\{R(u+1)-1,\sqrt{n-V} \}_{min} uR(u){R(u+1)1,nV }min

剪枝三

可行性剪枝,与上一个剪枝类似,这里我们优化高度 H H H

与剪枝二同理,第 u u u层的高度至少为 u u u;

上界也是类似的;

在这里插入图片描述

因此有 u ≤ H ( u ) ≤ { H ( u + 1 ) − 1 , n − V R 2 } m i n u≤H(u)≤\{H(u+1)-1,\frac{n-V}{R^2} \}_{min} uH(u){H(u+1)1,R2nV}min

剪枝四、

可行性剪枝、最优化剪枝;

这个很常见,就列出来不解释了;

m i n V ( u ) minV(u) minV(u)是第 u u u层的最小体积, m i n S ( u ) minS(u) minS(u)是第 u u u层的最小表面积;

V V V是当前体积之和, S S S是当前表面积之和;

必须满足下面两个式子:

  1. V + m i n V ( u ) ≤ n V+minV(u)≤n V+minV(u)n
  2. S + m i n S ( u ) ≤ a n s S+minS(u)≤ans S+minS(u)ans
剪枝五、

这个优化是最难想到的,是优化数学公式 + 最优化剪枝;

下图中只写出了侧面积,因为圆面的面积是一个可以直接算出来的值;

设已有的表面积为 S S S,已有的体积为 V V V;

假设当前搜索到第 u u u层;

如果已有的表面积加上第一层到第 u u u层的表面积预估值已经大于等于目前最优答案,那么可以直接return

在这里插入图片描述

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>

using namespace std;

typedef long long ll;

const int N = 1e2 + 10;

int n,m;

int R[N],H[N];

int minV[N],minS[N];

int ans = 1e9;

//第几层,已有面积,已有体积
void dfs(int u,int S,int V){
    //剪枝四、可行性,最优性剪枝
    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;
    }
    //剪枝一、先枚举r再枚举h,从大到小
    //剪枝二,三、限制r和h的上下界
    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 extra = (u == m?r*r:0);
            R[u] = r,H[u] = h;
            dfs(u-1,S+extra+2*r*h,V+r*r*h);
            //不用回溯 因为会直接覆盖的
        }
    }
}

void solve(){
    cin >> n >> m;
    for(int i=1;i<=m;++i){
        //这是最小的情况
        //第i层 半径为i、高度为i
        minV[i] = i * i * i + minV[i-1];
        minS[i] = 2 * i * i + minS[i-1];
    }
    //哨兵,处理边界
    R[m+1] = H[m+1] = 1e9;
    dfs(m,0,0);
    if(ans == 1e9) cout << 0 << '\n';
    else cout << ans << '\n';
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值