常用的剪枝方式
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(所有木棍总长度) length(木棒长度)∣sum(所有木棍总长度)
剪枝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
u≤R≤R(u+1)−1.
同时还有体积的限制
假设
u
u
u往下的层数总体积是v,总蛋糕体积是n,那么剩下的体积是
n
−
v
n - v
n−v.
n
−
v
≥
R
2
H
n - v \geq R^2 H
n−v≥R2H,当H = 1, 推出
R
R
R的上界,
n
−
v
≥
R
\sqrt{n - v} \geq R
n−v≥R.
综上,可以求出
u
≤
R
(
u
)
≤
min
{
R
(
u
+
1
)
−
1
,
n
−
v
}
u \leq R(u) \leq \min\{R(u + 1) - 1, \sqrt{n - v} \}
u≤R(u)≤min{R(u+1)−1,n−v}.
高度H的范围
H
H
H也有相应的范围
由
n
−
v
≥
R
2
H
n - v \geq R^2H
n−v≥R2H, 推出
H
≤
n
−
v
R
2
H \leq \frac{n - v}{R^2}
H≤R2n−v
因此
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} \}
u≤H(u)≤min{H(u+1)−1,R2n−v}
前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) 前u层体积最小值mins(u) 前u层表面积最小值v+minv(u)<=n(当前体积+前u层体积最小值不能比总体积大s+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+S1−u≥ans, 则已经不能优化最优解,直接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.
S1∼u=k=1∑u2RkHk=Ru+12k=1∑uRkHkRu+1>Ru+12k=1∑uRk2Hk.
第一个等式中 最后一个>号是因为
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
n−v=k=1∑uRk2Hk
综上
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;
S1∼u>Ru+12(n−v).S+Ru+12(n−v)≥ans,可以直接return;
代码
递归的时候按照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;
}