2.2.2 DFS之剪枝

常用的剪枝方式常用的剪枝方式

1.优化搜索顺序

大部分情况下,我们应该搜索分支较少的节点。
在这里插入图片描述
如上图,同样搜索到第3层, 4个节点。 从搜索分支较少的点开始搜索,可能可以提前剪枝。

2.排除等效冗余

比如,要搜索一个组合数,从n个苹果中选取m个苹果, 一共有多少种选择方案
先选1,再选2(12)

先选2,再选1(21)
效果是一样的。
因此要在搜索的时候,尽量不搜索重复的状态

3.可行性剪枝

搜到一半不合法, 可以剪枝

4.最优性剪枝

当前搜索到的状态,无论如何比当前搜到最优解差,可以提前退出

5.记忆化搜索(dp)

AcWing 165. 小猫爬山

分析

从前往后依次枚举每只小猫,每次枚举把当前的小猫放到哪辆车上。
u = 0 u = 0 u=0表示当前枚举到的小猫
在这里插入图片描述

1.先安排比较重的猫。

3.如果发现某只猫放到某辆车上已经超重,直接return;

4.如果发现新开的车的数量 > ans, 直接return;

代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 18;

int n, m;
int w[N];
int sum[N];
int ans = N;

void dfs(int u, int k){
    // 最优性剪枝
    if (k >= ans) return;
    if (u == n){
        ans = k;
        return;
    }
    
    for (int i = 0; i < k; i ++ ){
        if (sum[i] + w[u] <= m){ // 可行性剪枝
            sum[i] += w[u];
            dfs(u + 1, k);
            sum[i] -= w[u];
        }
    }
    
    // 新开一辆车
    sum[k] = w[u];
    dfs(u + 1, k + 1);
    sum[k] = 0;// 恢复现场
}
int main(){
    cin >> n >> m;
    for (int i = 0; i < n; i ++ ) cin >> w[i];
    
    // 优化搜索顺序
    sort(w, w + n);
    reverse(w, w + n);
    
    dfs(0, 0);
    
    cout << ans << endl;
    
    return 0;
}

AcWing 166. 数独

在这里插入图片描述

分析

1.优化搜索顺序

选择分支最少的格子

3.可行性剪枝

当前枚举的数字不能与行,列,九宫格重复

4.最优性剪枝

因为题目是找可行方案,不是找最优解,此剪枝无

位运算优化

找出来行0~9个位置上。
0:表示不能用,1表示当前位置可以用
☑️ 1 2 3 4 5 6 7 8 9
行 0 1 0 0 1 1 1 0 0
因此,当前行可以用的数字有2, 5, 6, 7
所以,可以用9位的二进制数(0-511)表示当前行可以用的数有哪些



九宫格
求一个交集(&&),来表示总的九宫格哪些数可以用

&&后比如数字是
☑️ 1 2 3 4 5 6 7 8 9 (表示哪些数可以用)
☑️ 0 0 1 0 0 0 0 0 1 (二进制)
表示 当前位置上可以选3, 5, 9

如何从二进制中取出可以用的数

即知道第二行的二进制数,求出3,5,9

lowbit运算

代码

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

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

int ones[M], map[M]; // ones打表记录当前的二进制数有多少个1, map[2^k] 返回k
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; // 这里1表示可以填,0表示不能填
    
    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){
    // 处理字符串
    if (is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';
    
    // 还需要将九宫格填上
    int v = 1 << t; // 因为这里1表示填上,如果is_set == true,表示填上数字,所以原位置1需要清空成0,
    if (!is_set) 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; // cnt表示九宫格有哪些数空着,dfs到0就可以返回了
        for (int i = 0, k = 0; i < N; i ++ )
            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);
    }
    
    return 0;
}

AcWing 167. 木棒

在这里插入图片描述

分析

在这里插入图片描述
木棒 (没被打断前的)
木棍(题目给的数据)

先枚举木棒的长度,然后从前往后搜索,去木棍中搜索可以拼接成木棒长度的方案

剪枝1

l e n g t h ( 木 棒 长 度 ) ∣ s u m ( 所 有 木 棍 总 长 度 ) length(木棒长度) | sum(所有木棍总长度) lengthsum(

剪枝2 优化搜索顺序

从大到小枚举
从前往后搜索,应该保证未来搜索空间少,因此枚举较长的木棍

剪枝3 排除等效冗余

3.1 组合数枚举

选择 1 2 3 和 3 2 1 效果相同,所以应该按照组合的方式去搜索
人为规定,拼木帮的时候,木棍的下标从小到大。

递归的时候传一个start参数即可,比如当前枚举5,下一次从5 + 1开始枚举

3.2 排除冗余2

如果当前木棍加到当前木棒中失败了,则直接略过后面所有长度相等的木棍
证明:
在这里插入图片描述

假设木棍3与木棍4长度相同
木棍3放到木棒2失败,放到后面的木棒(比如木棒3中)但木棍4可以放到木棒2
因为木棒3与木棒4长度相同,所以可以交换两者,与假设木棒3不能放到木棒2,矛盾。
因此木棍4放到木棒2也一定不成立

3.3 等效冗余3

如果当前木棍是木棒的第一根木棍,失败了,则当前方案一定失败。
在这里插入图片描述

证明:
假设当前木棍3是木棒的第一根木棍,失败了,但是当前方案是合法的方案。
那么木棍3,一定是在后面的木棒中,木棍3可以在后面的木棍中交换到第1个位置,再与木棒2交换,与假设木棒3是第一根并且拼接失败,矛盾。

3.4 等效冗余

如果木棍是木棒地最后一根木棍,失败了,则当前方案是不合法的,可以直接回溯
在这里插入图片描述

证明:
假如木棍3不能放到木棒2,但总的方案合法, 即木棍3可以放到其他木棒上去了,可以发现木棒3与木棒2最后一段长度相同,因此可以交换。与假设,矛盾。

可行性剪枝

如果当前木棍 + 当前木棒长度已经 > length(木棒长度) continue

代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 80;

int w[N];
bool st[N];
int n;
int sum, length;

bool dfs(int u, int cur, int start){ // u 当前枚举到木棒编号, cur当前枚举到木棒长度,start 当前枚举到木棍编号
    if (u * length == sum) return true;
    if (cur == length) return dfs(u + 1, 0, 0); // 注意这里start一定要从0开始,因为有些木棍在上一层中没有枚举。
    // 同 AcWing 1118. 分成互质组     if (flag) dfs(g + 1, 0, tc, 0); 异曲同工

    for (int i = start; i < n; i ++ ){
        if (st[i] || cur + w[i] > length) continue;
        
        st[i] = true;
        if (dfs(u, cur + w[i], i + 1)) return true;
        st[i] = false;
        
        // 代码到这里,表示当前木棍i不能放入当前木棒u
        // 剪枝3-3, 3-4
        if (!cur || cur + w[i] == length) return false; // !cur表示当前木棍是第1根
        // cur[i] + w[i] == length 表示放到最后也失败
        
        int j = i;
        while (j < n && w[j] == w[i]) j ++;
        i = j - 1;
    }
    
    return false;
}

int main(){
    while (cin >> n, n){
        memset(st, 0, sizeof st);
        sum = 0;
        
        for (int i = 0; i < n; i ++ ) {
            cin >> w[i];
            sum += w[i];
        }
        // 剪枝2 优化搜索顺序
        sort(w, w + n);
        reverse(w, w + n);
        
        length = 1;
        while(true){
            // 剪枝1
            if (sum % length == 0 && dfs(0, 0, 0)){
                cout << length << endl;
                break;
            }
            length ++;
        }
    }
    
    return 0;
}

AcWing 168. 生日蛋糕

在这里插入图片描述

分析

优化搜索顺序

自底向上搜索, 然后在每层内部,因为算表面积的时候半径R是平方级别,因此先枚举R,再枚举H,并且从大到小来枚举。

可行性剪枝

半径R的范围

当前枚举的是第 u u u层, 因为层数一次递增1,2, … ,u,因此 u ≤ R ≤ R ( u + 1 ) − 1 u\leq R \leq R(u + 1) - 1 uRR(u+1)1.
同时还有体积的限制
假设 u u u往下的层数总体积是v,总蛋糕体积是n,那么剩下的体积是 n − v n - v nv.
n − v ≥ R 2 H n - v \geq R^2 H nvR2H,当H = 1, 推出 R R R的上界, n − v ≥ R \sqrt{n - v} \geq R nv R.
综上,可以求出
u ≤ R ( u ) ≤ min ⁡ { R ( u + 1 ) − 1 , n − v } u \leq R(u) \leq \min\{R(u + 1) - 1, \sqrt{n - v} \} uR(u)min{R(u+1)1,nv }.

高度H的范围

H H H也有相应的范围
n − v ≥ R 2 H n - v \geq R^2H nvR2H, 推出 H ≤ n − v R 2 H \leq \frac{n - v}{R^2} HR2nv
因此 u ≤ H ( u ) ≤ min ⁡ { H ( u + 1 ) − 1 , n − v R 2 } u \leq H(u) \leq \min\{H(u + 1) - 1, \frac{n - v}{R^2} \} uH(u)min{H(u+1)1,R2nv}

前u层体积最小值和表面积最小值

m i n v ( u )   前 u 层 体 积 最 小 值 m i n s ( u )   前 u 层 表 面 积 最 小 值 v + m i n v ( u ) < = n ( 当 前 体 积 + 前 u 层 体 积 最 小 值 不 能 比 总 体 积 大 s + m i n s ( u ) < a n s ( 当 前 表 面 积 + 前 u 层 表 面 积 最 小 值 , 不 能 比 当 前 最 优 解 大 , 如 果 左 边 > = a n s , 不 能 优 化 最 优 解 , 直 接 r e t u r n ; minv(u) \ 前u层体积最小值 \\ mins(u) \ 前u层表面积最小值\\ v + minv(u) <= n (当前体积 + 前u层体积最小值 不能比总体积大 \\ s + mins(u) < ans (当前表面积 + 前u层表面积最小值, 不能比当前最优解大, \\如果左边>= ans, 不能优化最优解,直接return ; minv(u) umins(u) uv+minv(u)<=n+us+mins(u)<ans(+u,,>=ans,return;

表面积公式与体积公式之间的不等式关系(🌟🌟🌟🌟🌟)

用前 u u u层的体积估算当前 u u u层的最小表面积是多少,如果当前体积 s + S 1 − u ≥ a n s s + S_{1-u} \geq ans s+S1uans, 则已经不能优化最优解,直接return;

S 1 ∼ u = ∑ k = 1 u 2 R k H k = 2 R u + 1 ∑ k = 1 u R k H k R u + 1 > 2 R u + 1 ∑ k = 1 u R k 2 H k . S_{1 \sim u} = \sum_{k = 1}^{u} 2R_k H_k = \frac{2}{R_{u + 1}} \sum_{k = 1}^{u} R_kH_kR_{u + 1} > \frac{2}{R_{u + 1}} \sum_{k = 1}^{u} R^2_k H_k. S1u=k=1u2RkHk=Ru+12k=1uRkHkRu+1>Ru+12k=1uRk2Hk.
第一个等式中 最后一个>号是因为 R u + 1 > R u R_{u + 1} > R_u Ru+1>Ru
在这里插入图片描述
n − v = ∑ k = 1 u R k 2 H k n - v = \sum_{k = 1}^{u} R^2_k H_k nv=k=1uRk2Hk
综上
S 1 ∼ u > 2 ( n − v ) R u + 1 . S + 2 ( n − v ) R u + 1 ≥ a n s , 可 以 直 接 r e t u r n ; S_{1 \sim u} > \frac{2(n - v)}{R_{u + 1}} . \\ S + \frac{2(n - v)}{R_{u + 1}} \geq ans,可以直接return; S1u>Ru+12(nv).S+Ru+12(nv)ansreturn;

代码

递归的时候按照u的反方向搜索

#include <iostream>
#include <cmath>
using namespace std;
const int N = 25, INF = 1e9;

int n, m;
int minv[N], mins[N];
int R[N], H[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){ // 走到这一步,表示表面积没超,并且可以优化当前ans
        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;
    }

    R[m + 1] = H[m + 1] = INF;
    // 按照u的反方向搜索
    dfs(m, 0, 0);

    if (ans == INF) ans = 0;
    cout << ans << endl;

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值