连接:http://poj.org/problem?id=1190
题意:自己读还是说一下吧,一个体积为Nπ的M层蛋糕,问你最小表面积是多少(不算底面积),蛋糕要求就是每层都是圆柱体,而且从下往上高和半径依次递减(其实就是蛋糕店那种多层蛋糕。
思路:作为一道经典的深搜剪枝题,同时也是北大在深搜重点讲的题,这个题的价值不言而喻。
首先,作为搜索题,在写代码之前,都要想四个问题:
1.深度优先搜索,枚举什么?(至少你要知道,你的结果是由什么因素组成的,也就是说,搜的东西是什么,在代码里,就是你dfs函数后面那个括号里都得写什么)
一个很显而易见的东西,搜索出每一个圆柱的半径和高,就可以得到体积;也就是说,搜索就是枚举每一种半径和高的组合
2.如何确定搜索范围?(虽然搜索算法的时间复杂度很难精确描述,但至少还是要知道你搜的范围有多大)
搜索上下界也好想,现在只要知道是底层最大/小高度和最大/小半径就可以
3.搜索顺序,哪些地方体现搜索顺序?(这个很好理解,也是区分深搜和广搜最明显的地方)
自底向上,从大到小,一次枚举
4.如何剪枝?(在大多数时候,往往123很好想,第四点往往是此题最不好想到的地方)
这个东西比较神奇,放在代码里讲更好
可以看一看这个博客里的东西:https://www.cnblogs.com/fenghaoran/p/6391016.html
接下来我要做一件以前没做过的事情:在博客里讲怎么写代码:
由于自己的一些经历:发现,看别人代码,并理解其思想,是一件很重要的事;
另外,北大的思路是会讲完思路给你看代码的,下面讲讲关于怎么看懂一份代码;
最后,就是我发现,我的代码也没有北大的代码更优秀,而且北大的代码受众面更广。
北大的代码会给一些重要的注释,这也算是比较良心,更多时候,代码注释很少(看下你队友的代码吧),这时候阅读代码就需要更多的思考;
#include <iostream> #include <vector> #include <cstring> #include <cmath> using namespace std; int N,M; int minArea = 1 << 30; //最优表面积 int area = 0; //正在搭建中的蛋糕的表面积 int minV[30]; // minV[n]表示n层蛋糕最少的体积 int minA[30]; // minA[n]表示n层蛋糕的最少侧表面积 int MaxVforNRH(int n,int r,int h) { //求在n层蛋糕,底层最大半径r,最高高度h的情况下,能凑出来的最大体积 int v = 0; for( int i = 0; i < n ; ++ i ) v += (r - i ) *(r-i) * (h-i); return v; } void Dfs(int v, int n,int r,int h) //要用n层去凑体积v,最底层半径不能超过r,高度不能超过h //求出最小表面积放入 minArea { if( n == 0 ) { if( v ) return; else { minArea = min(minArea,area); return; } } if( v <= 0) return ; if( minV[n] > v ) //剪枝3 return ; if( area + minA[n] >= minArea) //剪枝1 return ; if( h < n || r < n ) //剪枝2 return ; if( MaxVforNRH(n,r,h) < v ) //剪枝4 //这个剪枝最强!没有的话,5秒都超时,有的话,10ms过! return; //for( int rr = n; rr <= r; ++ rr ) { 这种写法比从大到小慢5倍 for( int rr = r; rr >=n; -- rr ) { if( n == M ) //底面积 area = rr * rr; for( int hh = h; hh >= n ; --hh ) { area += 2 * rr * hh; Dfs(v-rr*rr*hh,n-1,rr-1,hh-1); area -= 2 * rr * hh; } } } int main() { cin >> N >> M ;//M层蛋糕,体积N minV[0] = 0; minA[0] = 0; for( int i = 1; i<= M; ++ i) { minV[i] = minV[i-1] + i * i * i; //第i层半径至少i,高度至少i minA[i] = minA[i-1] + 2 * i * i; } if( minV[M] > N ) cout << 0 << endl; else { int maxH = (N - minV[M-1])/(M*M) + 1;//底层最大高度 //最底层体积不超过 (N-minV[M-1]),且半径至少M int maxR = sqrt(double(N-minV[M-1])/M) + 1;//底层高度至少M area = 0; minArea = 1 << 30; Dfs( N,M,maxR,maxH); if( minArea == 1 << 30) cout << 0 << endl; else cout << minArea << endl; } return 0; }
看代码的步骤:
先看主函数外:定义了什么不变量?定义了什么变量数组?他们的意思是什么?
然后看主函数。输入自然不用说,然后看都初始化了什么东西。拿这个题来说,57-60行用一个for将一个最小体积和最小表面积算了出来:第i层最小就是半径是i,高i。通过这两个东西,可以算出搜索的下界。超出了上界和下界的情况,都直接舍去;最直接的体现就是61 62行,小于下限的体积舍去;之后的64 65行就非常简单了,通过下界计算出了搜索的上界,这也就是搜索四问之第二问在代码里的体现;初始化了当前表面积和最小表面积(也就是答案)需要解释的就是为什么+1(可以简单理解成向上取整,不然会丢情况)。接下来就是dfs函数,通过它更新最小表面积,得出结果;最后的输出也不用多说。
另外,如果有些人的代码,喜爱将所有东西一股脑塞进main里的话,在读代码的时候,要注意分块;可以帮他重构一下代码,一些语句写成函数,提高可读性;
接下来,就要看各个函数的功能了:先是dfs函数。
先看参数:在mian函数中,dfs传了N,M还有搜索上界;
dfs中,四个参数分别叫v,n,r,h
也就是每次dfs,要做的就是在n层时凑出体积v,而且不超过搜索上界rh;
再看dfs函数内部:一般,搜索函数放在最前面的东西,就是剪枝。
剪枝中,大多数可行性剪枝很好想:23到29便是一个最常见的可行性剪枝:当整个蛋糕搜完的时候,如果还需要凑出体积,自然时不行的;如果不需要凑体积,也就是说,这是一个可行的方案,让它和当前最小表面积比较,取出最小的;
第二个剪枝也比较好想(可行性剪枝):没有搜完蛋糕的时候,v小于等于0 这自然是不科学的
第三个剪枝也是可行性剪枝,意思是要凑出的体积小于上面层数体积下界,这也是不行的,对应课件中的剪枝3
第四个剪枝,叫做最优性剪枝,可以理解按照这样操作,不可能得到一个更优的解。也就是说,当前得到的表面积加上上面层数表面积下界依然大于当前表面积最小值,这个结果自然是错误的,对应剪枝1;
第五个剪枝,也是可行性剪枝,当前层数超过了搜索的高度或半径上界,对应剪枝2;
第六个剪枝,也是效率最高的剪枝,调用了一个函数,这个函数的功能很好理解,计算出上面所有层数能得到的最大体积,如果这个都比v小,那么也就不用搜了。这一条强就强在,能在多步之前,就预测出第三个剪枝是否会发生,将效率大大提高,对应剪枝4。
至此,这个题目最优美的地方也就结束了。感觉像是变种的股价函数过程:当前状态+预测最优状态与当前最优的比较 ,体现出了剪枝的美。
接下来的搜索函数平白无奇,两层for遍历所有的hr的可能;
唯一的小高潮就是从大到小搜更加的快,这倒是一个比较神奇的地方,至今没有参透其中的奥妙