算法竞赛进阶指南 搜索 0x23 剪枝

剪枝,就是减小搜索树规模、尽早排除搜索树中不必要的分支的一种手段。形象地看,就好像剪掉了搜索树的枝条,故被称为“剪枝”。在深度优先搜索中,有以下几类常见的剪枝方法:

1、优化搜索顺序
在一些搜索问题中,搜索树的各个层次、各个分支之间的顺序不是固定的。不同的搜索顺序会产生不同的搜索树形态,其规模大小也相差甚远。例如:
(1)在上一节的“小猫爬山”问题中,把小猫按照重量递减的顺序进行搜索。
(2)在上一节的“Sudoku”问题中,优先搜索“能填的合法数字”最少的位置。

2、排除等效冗余
在搜索过程中,如果我们能够判定从搜索树的当前节点上沿着某几条不同分支到达的子树是等效的,那么只需要对其中的一条分支执行搜索。我们会在本节的“Sticks”问题中看到该剪枝的应用。
另外,就如我们在上一节的“Sudoku”问题中提出的,初学者一定要避免重叠、混淆“层次”与“分支”,避免遍历若干棵覆盖统一状态空间的等效搜索树。

3、可行性剪枝
在搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界,就执行回溯。这就好比我们在道路上行走时,远远看到前方是一个死胡同,就应该立即折返绕路,而不是走到路的尽头再返回。
某些题目条件的范围限制是一个区间,此时可行性剪枝也被称为“上下界剪枝”。

4、最优性剪枝
在最优化的搜索过程中,如果当前花费的代价已经超过了当前搜到的最优解,那么无论采取多么优秀的策略到达递归边界,都不可能更新答案。此时可以停止对当前分支的搜索,执行回溯。

5、记忆化
可以记录每个状态的搜索结果,在重复遍历一个状态时直接检索并返回。这就好比我们对图进行深度优先遍历时,标记一个节点是否已经被访问过。
不过,读者可能已经发现,在“小猫爬山”与“Sudoko”问题中,我们的搜索算法遍历的状态空间其实是**“树“型,不会重复访问**,所以不需要进行记录。

1、AcWing 167. 木棒

题意 :

  • 乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。
  • 然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。
  • 请你设计一个程序,帮助乔治计算木棒的可能最小长度。

思路 :

  • 我们可以从小到大枚举原始木棒的长度len(也就是枚举答案)。当然,len应该是所有木棍长度总和sum的约数,并且原始木棒的根数cnt就等于 sum / len
  • 对于枚举的每个len,我们可以依次搜索每根原始木棒由哪些木棍拼成。具体地将,搜索所面对的状态包括:已经拼好的原始木棒根数正在拼的原始木棒的当前长度每个木棍的使用情况
  • 在每个状态下,我们从尚未使用的木棍中选择一个,尝试拼到当前的原始木棒里,然后递归到新的状态递归边界就是成功拼好cnt根原始木棒,或者因无法继续拼接而宣告失败。
  • 这个算法效率比较低,我们来依次考虑几类剪枝:
    1、优化搜索顺序
    把木棍长度从大到小排序优先尝试较长的木棍
    (私以为这里是这样考虑的:由于本题求的是木棒的最小长度,与搭配顺序没有关系,因此是一个组合问题而不是排列问题,为了避免出现排列的情况,我们需要定义一个顺序来枚举木棍来组成当前这根木棒;那么我们对木棍是从小到大还是从大到小呢?如果用从大到小,因为每个木棒长度是不变的,枚举木棍时,先枚举大的,后面的空间就少了,这样能装下的木棍的种类就少了,也就是说减少了分支的数量)
    2、排除等效冗余
    (1)可以限制先后加入一根原始木棒的木棍长度是递减的。这是因为先拼上一根长度为x的木棍,再拼上一根长为y的木棍(x < y),与先拼上y再拼上x显然是等效的,只需要搜索其中一种
    (2)对于当前原始木棒,记录最近一次尝试拼接的木棍长度。如果分支搜索失败回溯,不再尝试向该木棒中拼接其他相同长度的木棍(必定也会失败)
    (3)如果在当前原始木棒中“**尝试拼入的第一根木棍”**的递归分支就返回失败,那么直接判定当前分支失败,立即回溯。这是因为在拼入这跟木棍前,面对的原始木棒都是“空”的(还没有进行拼接),这些木棒是等效的。木棍拼在当前的木棒中失败,拼在其他木棒中一样会失败。
    (4)如果在当前原始木棒中拼入一根木棍后,木棒恰好被拼接完整,并且“接下来拼接剩余原始木棒”的递归分支返回失败,那么直接判定当前分支失败,立即回溯。该剪枝可以用贪心来解释,“再用1根木棍恰好拼完当前原始木棒”必然比“再用若干根木棍拼完当前原始木棒”更好。
    上述(1)至(4)分别利用“同一根木棒上木棍顺序的等效性“”等长木棍的等效性“”空木棒的等效性“和”贪心“,剪掉了搜索树上诸多分支,使得搜索效率大大提升。
#include 
#include 
#include 
using namespace std;
const int N = 70;

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

bool dfs(int u, int cur, int start) {
    if (u * length == sum) return true;
    if (cur == length) return dfs(u + 1, 0, 0);
    
    for (int i = start; i < n; ++ i) {
        if (st[i]) continue;
        int l = sticks[i];
        if (cur + l <= length) {
            st[i] = true;
            if (dfs(u, cur + l, i)) return true;
            st[i] = false;
            
            if (!cur) return false;
            if (cur + l == length) return false;
            
            int j = i;
            while (j < n && sticks[j] == l) ++ j;
            i = j - 1;
        }
    }
    return false;
}

int main() {
    while (scanf("%d", &n), n) {
        memset(st, 0, sizeof st);
        sum = 0, length = 0;
        for (int i = 0; i < n; ++ i) {
            scanf("%d", &sticks[i]);
            if (sticks[i] > 50) continue;
            sum += sticks[i];
            length = max(length, sticks[i]);
        }
        sort(sticks, sticks + n);
        reverse(sticks, sticks + n);
        for (int i = 0; i < n; ++ i) {
            if (sticks[i] > 50) {
                st[i] = true;
            }
        }
        while (true) {
            if (sum % length == 0 && dfs(0, 0, 0)) {
                printf("%d\n", length);
                break;
            }
            else length ++ ;
        }
    }
}

2、AcWing 168. 生日蛋糕

题意 :

  • 制作一个体积为 Nπ 的 M 层生日蛋糕,每层都是一个圆柱体
  • 设从下往上数第 i 层蛋糕是半径为 Ri,高度为 Hi 的圆柱。
  • 当 iR_i+1 且 Hi>H_i+1。
  • 由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积 Q 最小。
  • 令 Q=Sπ ,请编程对给出的 N 和 M,找出蛋糕的制作方案(适当的 Ri 和 Hi 的值),使 S 最小。
  • 输出仅一行,是一个正整数 S(若无解则 S=0)。

思路 :

  • 搜索框架:从下往上搜索,枚举每层的半径和高度作为分支
  • 搜索面对的状态有:正在搜索蛋糕第dep层,当前外表面面积s,当前体积v,第dep + 1层的高度和半径。不妨用数组h和r分别记录每层的高度和半径。
  • 整个蛋糕的“上表面”面积之和等于最底层的圆面积,可以在第M层直接累加到s中。这样在第M - 1层往上的搜索中,只需要计算侧面积。
  • 剪枝:
    1、上下界剪枝:
    在第dep层时,只在下面的范围内枚举半径和高度即可
    首先,枚举 R ∈ [ d e p , m i n ( ⌊ N − v ⌋ , r [ d e p + 1 ] − 1 ) ] R \in [dep, min(\lfloor{\sqrt{N-v}}\rfloor, r[dep+1]-1)] R[dep,min(⌊Nv ,r[dep+1]1)]
    其次,枚举 H ∈ [ d e p , m i n ( ⌊ ( N − v ) / R 2 ⌋ , h [ d e p + 1 ] − 1 ) ] H \in [dep,min(\lfloor{(N-v)/R^2}\rfloor,h[dep+1]-1)] H[dep,min(⌊(Nv)/R2,h[dep+1]1)]
    上面两个区间右边界中的式子可以通过圆柱体积公式 π R 2 H = π ( N − v ) \pi R^2H=\pi(N-v) πR2H=π(Nv)得到
    2、优化搜索顺序
    在上面确定的范围中,使用倒序枚举
    3、可行性剪枝
    可以预处理出从上往下前i( 1 ≤ i ≤ M 1 \leq i \leq M 1iM)层的最小体积和侧面积。显然,当第1~i层的半径分别取1,2,3,…,i,高度也分别取1,2,3,…,i时,有最小体积与侧面积。
    如果当前体积v加上1~dep-1层的最小体积大于N,可以剪枝
    4、最优性剪枝一
    如果当前表面积s加上1~dep-1层的最小侧面积大于已经搜到的答案,剪枝
    5、最优性剪枝二
    利用h与r数组,1~dep-1层的体积可表示为 n − v = ∑ k = 1 d e p − 1 h [ k ] ∗ r [ k ] 2 n-v= \sum^{dep-1}_{k=1}h[k]*r[k]^2 nv=k=1dep1h[k]r[k]2,1~dep-1层的表面积可表示为 2 ∑ k = 1 d e p − 1 h [ k ] ∗ r [ k ] 2 \sum^{dep-1}_{k=1}h[k]*r[k] 2k=1dep1h[k]r[k]
    因为 2 ∑ k = 1 d e p − 1 h [ k ] ∗ r [ k ] = 2 r [ d e p ] ∗ ∑ k = 1 d e p − 1 h [ k ] ∗ r [ k ] ∗ r [ d e p ] ≥ 2 r [ d e p ] ∗ ∑ k = 1 d e p − 1 h [ k ] ∗ r [ k ] 2 ≥ 2 ( n − v ) r [ d e p ] 2 \sum^{dep-1}_{k=1}h[k]*r[k]= \frac{2}{r[dep]}* \sum^{dep-1}_{k=1}h[k]*r[k]*r[dep] \geq \frac{2}{r[dep]}* \sum^{dep-1}_{k=1}h[k]*r[k]^2 \geq \frac{2(n-v)}{r[dep]} 2k=1dep1h[k]r[k]=r[dep]2k=1dep1h[k]r[k]r[dep]r[dep]2k=1dep1h[k]r[k]2r[dep]2(nv),所以当 2 ( n − v ) r [ d e p ] + s \frac{2(n-v)}{r[dep]}+s r[dep]2(nv)+s大于已经搜到的答案时,可以剪枝
  • 加上以上五个剪枝后,搜索算法就可以快速求出该问题的最优解了
  • 为了进一步提高剪枝的效果,除了当前花费的“代价”之外,我们还可以对未来至少需要花费大代价进行预算,这样更容易接近每个维度的上下界。例如本题中的前dep-1层最小体积、最小侧面积就是这种想法。剪枝5则通过表面积与体积之间的关系,对不等式进行缩放。
#include 
#include 
using namespace std;
const int N = 25, INF = 1e9;

int n, m;
int R[N], H[N];
int minv[N], mins[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)); 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;
            H[u] = h; R[u] = r;
            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
    }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    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] = INF, H[m + 1] = INF;
    
    dfs(m, 0, 0);
    
    if (ans == INF) cout << 0;
    else cout << ans;
}

你可能感兴趣的:(dfs,算法,剪枝,机器学习)