常见的剪枝方式
一、优化搜索顺序
大部分情况下,我们应该优先搜索分支较少的结点;
在没有剪枝的情况下,因为最终都会枚举完全部的点,所以是一样的;
但是在有剪枝的情况下,走分支较少的点,剪枝的效果更明显;
如下图所示,红色勾起来的代表剪枝处;
二、排除等效冗余
简单来说,如果不考虑顺序,那么优先考虑组合,而不要考虑排列;
即不要搜索重复状态;
比如现在有 < 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=k∗length
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总=S第u层的上侧面积+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只存在一次方的;
因此
- 层间:从下到上
- 层内:先枚举半径再枚举高,半径由大到小,高度由大到小
剪枝二
考虑可行性剪枝,假设当前是第 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 n−V;
因为本题中忽略 π \pi π,有以下转换;
因此有 u ≤ R ( u ) ≤ { R ( u + 1 ) − 1 , n − V } m i n u≤R(u)≤\{R(u+1)-1,\sqrt{n-V} \}_{min} u≤R(u)≤{R(u+1)−1,n−V}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} u≤H(u)≤{H(u+1)−1,R2n−V}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是当前表面积之和;
必须满足下面两个式子:
- V + m i n V ( u ) ≤ n V+minV(u)≤n V+minV(u)≤n
- 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;
}