常见的剪枝方式
- 优化搜索顺序:优先搜索分支数量少的
- 排除等效冗余
- 可行性剪枝
- 最优性剪枝
例题
小猫爬山
题面
翰翰和达达饲养了 N 只小猫,这天,小猫们要去爬山。
经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。
翰翰和达达只好花钱让它们坐索道下山。
索道上的缆车最大承重量为 W,而 N 只小猫的重量分别是 C1、C2……CN。
当然,每辆缆车上的小猫的重量之和不能超过 W。
每租用一辆缆车,翰翰和达达就要付 1 美元,所以他们想知道,最少需要付多少美元才能把这 N 只小猫都运送下山?
输入格式
第 1 行:包含两个用空格隔开的整数,N 和 W。
第 2…N+1 行:每行一个整数,其中第 i+1 行的整数表示第 i 只小猫的重量 Ci。
输出格式
输出一个整数,表示最少需要多少美元,也就是最少需要多少辆缆车。
数据范围
1≤N≤18,
1≤ Ci ≤W≤108
输入样例:
5 1996
1
2
1994
12
29
输出样例:
2
剪枝
-
优化搜索顺序:
先从重量大的开始搜索,在总重量一定时,单只猫的重量越大其分支一定越少,极端的情况当 w i = = m w_i == m wi==m 时只有一个分支
-
最优性剪枝:
当当前的缆车数量已近 > = >= >= 答案时一定不能更新答案
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 20 ;
int g[N] , w[N] ;
int n , m ;
int ans = N ;
void dfs(int u , int v ) { //第u只猫,现在在处理第v个缆车
if( v >= ans )return ;// 此时的v已经不能优化答案
if( u > n ) {
ans = v ; //更新答案
return ;
}
//将当前的猫放入已有的缆车中
for(int i = 1 ; i <= v ; i ++ ) {
if( g[i] + w[u] <= m ) {
g[i] += w[u] ;
dfs(u+1 , v) ;
g[i] -= w[u] ;
}
}
//放入新缆车中
g[v+1] = w[u] ;
dfs( u + 1 , v + 1 ) ;
g[v+1] = 0 ;
}
int main(){
cin >> n >> m ;
for(int i = 1; i <= n ; i ++ ) cin >> w[i] ;
dfs(1,1) ;
cout << ans << endl ;
return 0 ;
}
数独
题面
数独是一种传统益智游戏,你需要把一个 9×9 的数独补充完整,使得图中每行、每列、每个 3×3 的九宫格内数字 1∼9 均恰好出现一次。
请编写一个程序填写数独。
输入格式
输入包含多组测试用例。
每个测试用例占一行,包含 81 个字符,代表数独的 81 个格内数据(顺序总体由上到下,同行由左到右)。
每个字符都是一个数字(1−9)或一个 .
(表示尚未填充)。
您可以假设输入中的每个谜题都只有一个解决方案。
文件结尾处为包含单词 end
的单行,表示输入结束。
输出格式
每个测试用例,输出一行数据,代表填充完全后的数独。
输入样例:
4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end
输出样例:
417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936
剪枝
-
优化搜索顺序
每次选当前位置能填的数的数量最少的位置
#include<iostream>
using namespace std;
const int N = 9 , M = 1 << N ;
int ones[M] , map[M] ; //记录每个数中1的数量,map记录当前数是2的几次方
int lowbit(int x ) {return x & -x ; }
char s[110] ;
int row[N] , col[N] , cel[3][3] ; //用二进制记录每行、每列、每个小格子的状态
int get(int x , int y ) { //返回的数二进制对应位置1为可填的
return col[y] & row[x] & cel[x/3][y/3] ;
}
void draw(int x , int y , int u , bool flag ) { //flag记录是在(x,y)上的操作为填数/清空
if( flag ) s[x * N + y ] = u + '1' ;
else s[x * N + y ] = '.' ;
int v = 1 << u ;
if( !flag ) v = -v ;
col[y] -= v ;
row[x] -= v ;
cel[x/3][y/3] -= v ;
}
int init(){
//初始化所有位置都置为1
for(int i = 0 ; i < N ; i ++ ) row[i] = col[i] = M - 1 ;
for(int i = 0 ; i < 3 ; i ++ ){
for(int j = 0 ; j < 3 ; j ++ ) {
cel[i][j] = M - 1 ;
}
}
int cnt = 0 ; //记录有几个位置需要填写
for(int i = 0 ; i < N ; i ++ ) {
for(int j = 0 ; j < N ; j ++ ) {
if( s[i * N + j ] == '.' ) {
cnt ++ ;
}
else{
draw(i,j, s[i*N + j ] - '1' , 1 ) ;
}
}
}
return cnt ;
}
bool dfs(int cnt ){
if( !cnt ) return true ;
int x , y , minn = 10 ;
//找分支最小的节点
for(int i = 0 ; i < N ; i ++ ) {
for(int j = 0 ; j < N ; j ++ ) {
if( s[i*N + j ] == '.' ) {
int k = get(i,j) ;
if( ones[k] < minn) {
minn = ones[k] ;
x = i , y = j ;
}
}
}
}
//枚举当前位置填哪个数
for(int i = get(x,y) ; i ; i -= lowbit(i) ) {
int k = map[lowbit(i)] ;
draw(x,y,k,1) ;
if( dfs(cnt-1) ) return true ;
draw(x,y,k,0) ;
}
return false ;
}
int main(){
for(int i = 0 ; i < 9 ; i ++ ) map[1<<i] = i ;
for(int i = 0 ; i < M ; i ++ ) {
for(int j = i ; j ; j -= lowbit(j) ) {
ones[i] ++ ;
}
}
while( cin >> s ) {
if( *s == 'e' ) break ;
int p = init() ;
dfs(p) ;
puts(s) ;
}
return 0 ;
}
木棒
题面
乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。
然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。
请你设计一个程序,帮助乔治计算木棒的可能最小长度。
每一节木棍的长度都用大于零的整数表示。
输入格式
输入包含多组数据,每组数据包括两行。
第一行是一个不超过 64 的整数,表示砍断之后共有多少节木棍。
第二行是截断以后,所得到的各节木棍的长度。
在最后一组数据之后,是一个零。
输出格式
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。
数据范围
数据保证每一节木棍的长度均不大于 50。
输入样例:
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
输出样例:
6
5
剪枝
-
优化搜索顺序
从木棍最长的开始搜索
-
可行性剪枝
a.拼成的木棒的长度必须是木棍总长度的整数倍
b.当前木棍在某位置失败,则与其长度相同的木棍在此位置失败
c.若某木棍在一个木棒的第一个位置失败,则当前方案失败
证明:若当前方案存在解,则现在的木棍一定在之后的木棒中有对应的位置,将其与其所在木棒的第一根木棍交换位置,再将其所在木棒与当前木棒交换位置则得到当前木棍在当前木棒的第一个位置成功的方案,条件与假设冲突。
d.若某木棍在一个木棒的最后位置失败,则当前方案失败
证明:若当前方案存在解,则一定存在某些木棍的长度之和与当前木棍的长度相等,放在当前木棒的最后使答案成立,而当前木棍在之后的木棒中一定有对应位置,将这些长度之和与其相等的木棍与之交换,则得到此木棍在当前木棒末尾位置成立的答案,条件与假设冲突。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 66 ;
int w[N] , st[N] ;
int n , sum , len ;
bool dfs(int u , int v , int s ) { // 已经处理了u根木棒,当前木棒的长度为v,起始搜索的木棍下标为s
if( u * len == sum ) return true ;
if( v == len ) return dfs(u+1,0,0) ;
for(int i = s ; i < n ; i ++ ) {
if( !st[i] && v + w[i] <= len ) {
st[i] = 1 ;
if( dfs(u , v + w[i] , i + 1 ) ) return true ;
st[i] = 0;
//剪枝2.c
if( !v ) return false ;
//剪枝2.d
if( v + w[i] == len ) return false ;
//剪枝2.b
int j = i ;
while( j < n && w[j] == w[i] ) j ++ ;
i = j - 1 ;
}
}
return false ;
}
int main(){
while(cin >> n , n ) {
sum = 0 ;
len = 1 ;
memset(st,0,sizeof(st)) ;
for(int i = 0 ; i < n ; i ++ ) cin >> w[i] , sum += w[i] ;
sort(w,w+n) ;
reverse(w,w+n) ;
while(1){
//剪枝2.a
if( sum % len == 0 && dfs(0,0,0) ) {
cout << len << endl ;
break ;
}
len ++ ;
}
}
return 0 ;
}
生日蛋糕
题面
7 月 17 日是 Mr.W 的生日,ACM-THU 为此要制作一个体积为 Nπ 的 M 层生日蛋糕,每层都是一个圆柱体。
设从下往上数第 i 层蛋糕是半径为 Ri,高度为 Hi 的圆柱。
当 i<Mi<M 时,要求 Ri>Ri+1 且 Hi>Hi+1。
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积 Q 最小。
令 Q=Sπ ,请编程对给出的 N 和 M,找出蛋糕的制作方案(适当的 Ri 和 Hi 的值),使 S 最小。
除 Q 外,以上所有数据皆为正整数。
输入格式
输入包含两行,第一行为整数 N,表示待制作的蛋糕的体积为 Nπ。
第二行为整数 M,表示蛋糕的层数为 M。
输出格式
输出仅一行,是一个正整数 S(若无解则 S=0)。
数据范围
1≤N≤10000,
1≤M≤20
输入样例:
100
2
输出样例:
68
剪枝
-
优化搜索顺序
从体积大的开始枚举,即从最下层开始枚举
-
可行性剪枝
a. 提前处理出 1 ~ m 层的最小体积与面积,处理到某一层 u 时,若已用体积为 v , 若 v + m i n v [ u ] > n v + minv[u] > n v+minv[u]>n 则当前方案不可行,面积同理.
-
最优性剪枝
S 1 − u = ∑ k = 1 u 2 ∗ R k ∗ H k = 1 R u + 1 ∗ ∑ k = 1 u 2 ∗ R k ∗ H k ∗ R u + 1 > 2 R u + 1 ∗ ∑ k = 1 u R k 2 ∗ H k S_{1-u} = \sum_{k=1}^u 2 * R_k * H_k = \frac{1}{R_{u+1}} * \sum_{k=1}^u 2 * R_k * H_k * R_{u+1} > \frac{2}{R_{u+1}}*\sum_{k=1}^u R_k^2 * H_k S1−u=∑k=1u2∗Rk∗Hk=Ru+11∗∑k=1u2∗Rk∗Hk∗Ru+1>Ru+12∗∑k=1uRk2∗Hk
n − v = ∑ k = 1 u R k 2 ∗ H k n-v = \sum_{k=1}^u R_k^2 *H_k n−v=∑k=1uRk2∗Hk
⇒ \Rightarrow ⇒
S 1 − u > 2 ∗ ( n − v ) / R u + 1 S_{1-u} > 2*(n - v ) / R_{u+1} S1−u>2∗(n−v)/Ru+1
若s + S1-u >= ans , 则一定无法更新答案
#include<iostream>
#include<cmath>
using namespace std;
const int N = 22 ;
int R[N] , H[N] ;
int minv[N] , mins[N] ;
int n , m , inf = 0x3f3f3f3f ;
int ans = inf ;
void dfs(int u , int s , int v ) {
if( s + mins[u] >= ans ) return ;
if( v + minv[u] > n ) 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 - minv[u-1] ) / u) ) ; r >= u ; r -- ) {
for(int h = min( H[u+1] - 1 , ( n - v - minv[u-1] ) / r / r ) ; h >= u ; h -- ){
R[u] = r , H[u] = h ;
int t = 0 ;
if( m == u ) t = r * r ;
dfs( u - 1 , t + s + 2 * r * h , v + r * r * h ) ;
}
}
}
int main(){
cin >> n >> m ;
for(int i = 1 ; i <= m ; i ++ ) {
mins[i] = mins[i-1] + 2 * i * i ;
minv[i] = minv[i-1] + i * i * i ;
}
R[m+1] = H[m+1] = inf ;
dfs(m , 0 , 0 ) ;
if( ans == inf ) puts("0");
else
cout << ans << endl ;
return 0 ;
}