【AcWing算法提高课】2.2.2DFS之剪枝

本文介绍了如何在深度优先搜索(DFS)中利用剪枝技术提高算法效率,包括优化搜索顺序、排除等效冗余、可行性剪枝和最优性剪枝。通过小猫爬山、数独、木棒和生日蛋糕四个实例详细阐述了剪枝方法在不同问题中的应用,如小猫爬山中的权重排序、数独中的位运算加速等。

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

零、剪枝

如果我们在搜索某些状态前,能预先知道这些状态及之后的状态肯定不会得到一个合法方案,那么就可以提前返回,从而达到提高搜索效率的效果。

常用的剪枝方法:

  1. 优化搜索顺序:大部分情况下,应该优先搜索分支较少的节点;
  2. 排除等效冗余:尽量保证不搜索重复的状态 (在不考虑顺序时,用组合方式搜索);
  3. 可行性剪枝:若继续搜下去所有状态都不合法,可以提前退出;
  4. 最优性剪枝:若当前花费已经差于当前最优解,可以提前退出;
  5. 记忆化搜索:即动态规划,在纯搜索剪枝中很少用到,本小节直接略过。

一、小猫爬山

165.小猫爬山 题目链接

先考虑一个可行的搜索方案:对于每个小猫,它可以放进当前所有的缆车中任意一辆 (前提不超重),或者新开一个缆车,选择上述任何一个方案,继续搜索,然后回溯。直到所有猫全部坐上缆车,得到一种合法方案,与当前最优解取最小值。

在这里插入图片描述
接下来考虑剪枝:

  1. 优化搜索顺序:把所有猫按照重量从大到小排序,从重猫开始枚举,这使得下一只猫能够放的缆车数量尽可能少,从而减少了分支数量;
  2. 排除等效冗余:按照组合方式枚举,记录当前选择的小猫的下标,下次搜索时从该下标之后的小猫开始枚举;
  3. 可行性剪枝:小猫坐上某辆缆车后不能超重;
  4. 最优性剪枝:如果当前缆车数量不小于当前最优解,直接返回。

代码实现:

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

using namespace std;

const int N = 20;

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

void dfs(int u, int k){  //搜索到的小猫下标为u,当前共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);  //优化搜索顺序,由于sort从小到大排序,需要将数组翻转
    
    dfs(0, 0);
    
    cout << ans << endl;
    
    return 0;
}

二、数独

166数独 题目链接

先考虑一个可行的搜索方案:每次随意选择一个空位置,枚举所有能填在这个位置的数,填入一个数,然后继续搜索,回溯,填入下一个数。直到所有格子都填上了数,直接输出方案。

接下来考虑剪枝:

  1. 优化搜索顺序:每次选择能填数字最少的格子;
  2. 排除等效冗余:在能填的数的数量相同时,选择排在最前面的格子;
  3. 可行性剪枝:某个格子中填的数在其所处行、列、九宫格内都未曾出现过;
  4. 最优性剪枝:由于本题是可行性问题,没有最优性剪枝。

除了搜索剪枝之外,还可运用位运算来快速找出某个格子能填哪些数:
9 9 9 位二进制数代表行、列、九宫格内填过的数字的状态,某个格子能填一个数当且仅当其所在行、列、九宫格的三个状态二进制数对应的一位均为 1 1 1,直接用与运算即可;在与运算后使用 l o w b i t lowbit lowbit 运算,可以快速得出这个格子总共能填哪些数。

代码实现:

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

using namespace std;

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

int ones[M], map[M];
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){  //在(x,y)处写入或清除数t,is_set=1代表写入,=0代表清除
    if (is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';
    
    int v = 1 << t;
    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 1;
    
    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, 1);
        if (dfs(cnt - 1)) return 1;
        draw(x, y, t, 0);
    }
    
    return 0;
}

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 ++)
            for (int j = 0; j < N; j ++, k ++)
                if (str[k] != '.'){
                    int t = str[k] - '1';
                    draw(i, j, t, 1);
                }
                else cnt ++;
                
        dfs(cnt);
        
        puts(str);
    }
    
    return 0;
}

三、木棒

167.木棒 题目链接

先考虑一个可行的搜索方案:从小到大枚举木棒的长度,对于某一固定长度,从前往后拼接每一根木棒,若所有木棍都能用上,说明方案可行,得到长度最小值。

接下来考虑剪枝:

  1. 木棍总长度必须是枚举的木棒长度的倍数,木棒长度从最大木棍长度开始枚举;
  2. 优化搜索顺序:类似小猫爬山一题,在拼接某一固定长度的木棒时,把所有木棍按长度从大到小排序,从长的木棍开始枚举,从而减少分支数量;
  3. 排除等效冗余:
    (1) 按照组合方式枚举,记录当前选择的木棍的下标,下次搜索时从该下标之后的木棍开始枚举;
    (2) 如果当前木棍加到当前棒中失败了,直接略过后面所有等长木棍;
    (3) 如果尝试拼入当前木棒 i i i第一根木棍 u u u 失败了,则直接返回失败;
    证明:用反证法,假设存在一种可行方案,则木棍 u u u 会在 i i i 之后的某一根木棒 j   ( j ≥ i ) j\ (j\ge i) j (ji) 中出现,此时将木棒 j j j 中的木根重新排列,使得木棍 u u u 处在木棒 j j j 的最前端,再将木棒 i i i j j j 调换,这样操作后得到的仍然是一种可行方案,但此时木棍 u u u 是木棒 i i i 的第一根木棍,矛盾。
    (4) 如果木棍 u u u 是拼成木棒 i i i 的最后一根木棍,且继续拼接剩余的木棒失败了,则直接返回失败;
    证明:用反证法,假设存在一种可行方案,则木棍 u u u 会在 i i i 之后的某一根木棒 j   ( j > i ) j\ (j>i) j (j>i) 中出现,且木棒 i i i 最后长度为 l e n ( u ) len(u) len(u) 的部分由不少于一根的木棍拼成,此时将木棍 u u u 与这一部分交换,得到的仍然是一种可行方案,但此时继续拼接剩余的木棒是可行的,矛盾。
  4. 可行性剪枝:拼上当前一根木棍后长度大于枚举的木棒长度,失败;
  5. 最优性剪枝:由于本题是可行性问题,没有最优性剪枝。

代码实现:

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

using namespace std;

const int N = 70;

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

bool dfs(int u, int s, int start){
    if (u * length == sum) return 1;
    if (s == length) return dfs(u + 1, 0, 0);
    
    //剪枝3-1,从start开始枚举
    for (int i = start; i < n; i ++){
        if (st[i]) continue;
        if (s + w[i] > length) continue;  //可行性剪枝
        
        st[i] = 1;
        if (dfs(u, s + w[i], i + 1)) return 1;
        st[i] = 0;
        
        //剪枝3-3
        if (!s) return 0;
        
        //剪枝3-4
        if (s + w[i] == length) return 0;
        
        //剪枝3-2
        int j = i;
        while (j < n && w[j] == w[i]) j ++;
        i = j - 1;
    }
    
    return 0;
}

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

四、生日蛋糕

168.生日蛋糕 题目链接

先考虑一个可行的搜索方案:自底向上搜索,对于每一层蛋糕,枚举其可能的半径和高度,然后继续搜索上一层蛋糕,直到层数与体积与所给题中所给数据相等,更新最小值。

接下来考虑剪枝:

  1. 优化搜索顺序:每一层先枚举半径 R R R 再枚举高度 H H H (因为在体积公式中,半径次数为 2 2 2,高度次数为 1 1 1),且两者的枚举顺序均为从大到小;
  2. 对于蛋糕第 u u u 层,其半径 R u R_u Ru 与高度 H u H_u Hu 存在一个范围:(设当前从第 u + 1 u+1 u+1 层到第 m m m 层总体积为 v v v)
    u ≤ R u ≤ m i n { R u + 1 − 1 , ⌊ n − v u ⌋ } u\le R_u\le min\{R_{u+1}-1, \lfloor\sqrt{\frac{n-v}{u}}\rfloor\} uRumin{Ru+11,unv ⌋} u ≤ H u ≤ m i n { H u + 1 − 1 , ⌊ n − v R 2 ⌋ } u\le H_u\le min\{H_{u+1}-1, \lfloor\frac{n-v}{R^2}\rfloor\} uHumin{Hu+11,R2nv⌋}
    由于前 u u u 层蛋糕的体积为 n − v n-v nv,第 u u u 层蛋糕的高度最小为 u u u,故有 u R u 2 ≤ n − v uR_u^2\le n-v uRu2nv,而当半径确定后,又有 H u R u 2 ≤ n − v H_uR_u^2\le n-v HuRu2nv
  3. 排除等效冗余:不会搜到相同的方案,无需剪枝;
  4. 可行性剪枝:预处理出蛋糕前 u u u 层的最小体积 m i n s v mins_v minsv (即第 i i i 层半径、高度均为 i i i 时),若当前从第 u + 1 u+1 u+1 层到第 m m m 层的体积为 v v v,且 v + m i n v u > n v+minv_u>n v+minvu>n,说明超出了给定体积,直接返回;
  5. 最优性剪枝:
    (1) 预处理出蛋糕前 u u u 层的最小(侧)表面积 m i n s u mins_u minsu (即第 i i i 层半径、高度均为 i i i 时),若当前从第 u + 1 u+1 u+1 层到第 m m m 层的表面积为 s s s,且 s + m i n s u ≥ a n s s+mins_u\ge ans s+minsuans,说明不可能更新最优解,直接返回;
    (2) 我们可以用当前剩余的体积 n − v n-v nv 和第 u + 1 u+1 u+1 层半径 R u + 1 R_{u+1} Ru+1 估算前 u u u 层蛋糕的(侧)表面积,观察前 u u u 层(侧)表面积 S S S 和体积 n − v n-v nv 的公式:
    S = ∑ k = 1 u 2 R k H k S=\sum\limits^u_{k=1}2R_kH_k S=k=1u2RkHk n − v = ∑ k = 1 u R k 2 H k n-v=\sum\limits^u_{k=1}R_k^2H_k nv=k=1uRk2Hk,注意到两式中含有相似部分,对 S S S 进行放缩:
    S = 2 R u + 1 ∑ k = 1 u R k R u + 1 H k > 2 R u + 1 ∑ k = 1 u R k 2 H k = 2 R u + 1 ( n − v ) S=\frac{2}{R_{u+1}}\sum\limits^u_{k=1}R_kR_{u+1}H_k>\frac{2}{R_{u+1}}\sum\limits^u_{k=1}R_k^2H_k=\frac{2}{R_{u+1}}(n-v) S=Ru+12k=1uRkRu+1Hk>Ru+12k=1uRk2Hk=Ru+12(nv)
    故当 s + 2 R u + 1 ( n − v ) > a n s s+\frac{2}{R_{u+1}}(n-v)>ans s+Ru+12(nv)>ans 时,一定有 s + S > a n s s+S>ans s+S>ans,说明不可能更新最优解,直接返回。

代码实现:

#include <iostream>
#include <cstring>
#include <algorithm>
#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){
        if (v == n) ans = s;  //注意如果层数到了但是体积没用完也是不合法方案,不能更新最小值
        return;
    }
    
    for (int r = min(R[u + 1] - 1, (int)sqrt((n - v) / u)); 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 ++){  //预处理出前i层的最小体积和表面积
        minv[i] = minv[i - 1] + i * i * i;
        mins[i] = mins[i - 1] + 2 * i * i;
    }
    
    R[m + 1] = H[m + 1] = INF;
    
    dfs(m, 0, 0);
    
    if (ans == INF) puts("0");
    else cout << ans;
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值