序言:第二章的题目实在是花费了我很多时间去想,甚至有些题目我并不能仅依靠自己的能力,还要上网去查明某些算法的实现,才能解出这道题。虽然本章作业的五道题目都不容易,但其思想没有脱离本章内容——分治法和递归,因此我们一一解读一下部分题目,并总结一下它们的解题思想。
1.派
题目内容:
我的生日要到了!根据习俗,我需要将一些派分给大家。我有N个不同口味、不同大小的派。有F个朋友会来参加我的派对,每个人会拿到一块派(必须一个派的一块,不能由几个派的小块拼成;可以是一整个派)。
我的朋友们都特别小气,如果有人拿到更大的一块,就会开始抱怨。因此所有人拿到的派是同样大小的(但不需要是同样形状的),虽然这样有些派会被浪费,但总比搞砸整个派对好。当然,我也要给自己留一块,而这一块也要和其他人的同样大小。
请问我们每个人拿到的派最大是多少?每个派都是一个高为1,半径不等的圆柱体。
输入格式:
第一行包含两个正整数N和F,1 ≤ N, F ≤ 10 000,表示派的数量和朋友的数量。 第二行包含N个1到10000之间的整数,表示每个派的半径。
输出格式:
输出每个人能得到的最大的派的体积,精确到小数点后三位。
输入样例:
3 3
4 3 3
输出样例:
在这里给出相应的输出。例如:
25.133
我的解题思路:
(按例先放参考代码)
1 #include <iostream> 2 #include <cmath> 3 using namespace std; 4 const double PI = acos(-1.0); //定义常量PI,这里用到了cmath头函数提供的求PI的反cos函数 5 6 bool divide(double array[], double currentDivide, int N, int people) 7 { //cuttentDivide为当前划分的量,大小为中间量(divideMid) 8 int ableNum = 0; //ableNum表示按当前的分法,每个派能分几份 9 for (int i = 0; i < N; i++) 10 { 11 ableNum += int(array[i] / currentDivide); //记录当前这个派能够分几份,然后再讲每个派的情况累加起来 12 } 13 if (ableNum >= people) return true; //如果分出来的份数大于或等于总人数,那么此情况可成立(但不是最佳分法) 14 else return false; //这种情况不可行,要缩小currentDivide以分更多的派给朋友和我 15 } 16 17 int main() 18 { 19 int N, F; 20 cin>>N>>F; 21 int radius[10005]; 22 double v[10005]; 23 double vmax = 0; 24 int people = F + 1; 25 for (int i = 0; i < N; i++) //输入每个派的半径,同时算出每个派的体积,并用vmax记录最大的体积 26 { 27 cin>>radius[i]; 28 v[i] = radius[i] * radius[i] * PI; 29 if (vmax < v[i]) 30 vmax = v[i]; 31 } 32 double divideMin = 0, divideMax = vmax; //定义划分的最小情况和最大情况 33 double divideMid; //定义划分中间量 34 while (divideMax - divideMin > 1e-7) //循环终止条件:最大值小于最小值 35 { 36 divideMid = (divideMin + divideMax) / 2; 37 if (divide(v, divideMid, N, people)) 38 { 39 divideMin = divideMid; //如果当前分法成立,则尝试分更大的派给大家 40 } 41 else 42 { 43 divideMax = divideMid; //如果当前分法根本无法满足我们的要求,那么我们分小一点保证能完成基本任务 44 } 45 } 46 double finalDivide; 47 finalDivide = divideMid; 48 printf("%.3lf", finalDivide); //涉及到输出特定位小数,还是用printf安全! 49 return 0; 50 }
这一道题目非常有意思,所以先放到第一个讲。
先解决输入问题:首先题目就有一点要我们注意:既然N是派的个数,F为来访朋友的个数,而“我”本人也是需要吃派的,所以实际分派人数为N+1。而题目要求的输出项为每人分到的派的体积,所以我在定义每个派的半径数组的同时,还算出了每一派的体积数组,还用一个vmax来记录最大的那个派的体积。
再看一下题目要求的分派方法:所有人拿到的派是同样大小的(但不需要是同样形状的),虽然这样有些派会被浪费,但总比搞砸整个派对好。当然,我也要给自己留一块,而这一块也要和其他人的同样大小。
这就意味着,每个人分到的派,都是从某一个给定的派中划分出来的(题目不允许拼派),所以会有以下情况:
1.某一个派因为太小了,但它刚好是每个人能分到的大小,所以把它直接给了一个人。
2.某一个派因为稍稍比算出来的每个人分到的派大,所以要浪费掉该派的剩下的部分。
3.某一个派刚好是算出来的每个人分到的派的两倍体积!(只能刚好平分,不能一边大,一边小,不然违背题目要求,同时题目不允许拼派,所以也不能分一半大、一半小)
根据二分法的思想,我定义了三个变量分别表示划分派的最小量、最大量、中间量,最小值初值为0,最大值初值为刚刚的得到的最大体积,这样的话,中间值就是最大派平分的情况,那么下面开始本题核心算法:判断到底该缩小中间量还是增大中间量的算法(函数名为divide的函数)
1 bool divide(double array[], double currentDivide, int N, int people) 2 { //cuttentDivide为当前划分的量,大小为中间量(divideMid) 3 int ableNum = 0; //ableNum表示按当前的分法,每个派能分几份 4 for (int i = 0; i < N; i++) 5 { 6 ableNum += int(array[i] / currentDivide); //记录当前这个派能够分几份,然后再讲每个派的情况累加起来 7 } 8 if (ableNum >= people) return true; //如果分出来的份数大于或等于总人数,那么此情况可成立(但不是最佳分法) 9 else return false; //这种情况不可行,要缩小currentDivide以分更多的派给朋友和我 10 }
为什么要用到判断是要缩小中间量还是增大中间量?
因为中间量被传到divide函数中,变成了当前这种分派方法的划分派大小值(currentDivide)。
如果,按照当前的划分大小来划分每一个派,结果所有派分好了,不够总人数大,不能保证朋友们和我都吃到派,这种方法太奢侈了!要求缩小每个人分到的派的大小,以保证我们的基本任务(每个人都吃到大小相等的派)!所以,继续尝试分下去!
如果,按照当前的划分大小来划分每一个派,结果所有派分好了,够总人数,这样完成了我们的基本任务,但肯定会有浪费的派,为了响应国家节约粮食的号召,我们不能满足于大家吃很少很少的派,然后笑嘻嘻地丢掉了浪费掉的派!所以我们增大当前分法的划分大小,试一下,让大家都吃更多的派同时保证每个人吃到同样大小的派的基本任务!所以,继续尝试分下去!
因为循环条件是最大值大于最小值,当我们不断修改最大值和最小值后,最后我们得到了最佳的划分大小并终止循环,就能得到最佳分法啦!
不得不说用二分法解决这道题真的很有意思,我们既要保证每个人吃一样大的派,同时还要尽量减少浪费,因此要控制好循环的终止条件,并设置好每种情况要如何修改最大值和最小值(中间值取决于最大值和最小值)。
2.二分法求函数的零点
题目内容:
有函数:f(x)=x^5−15x^4+85x^3−225x^2+274^x−121 已知f(1.5)>0,f(2.4)<0 且方程f(x)=0 在区间[1.5,2.4] 有且只有一个根,请用二分法求出该根。
我的解题思路:
(按例先放参考代码)
1 #include <iostream> 2 #include <math.h> 3 using namespace std; 4 5 double expression(double x) //用于表示题目的函数 6 { 7 double fx; 8 double x2 = pow(x, 2); 9 double x3 = pow(x, 3); 10 double x4 = pow(x, 4); 11 double x5 = pow(x, 5); 12 fx = x5 - 15 * x4 + 85 * x3 - 225 * x2 + 274 * x - 121; 13 return fx; 14 } 15 16 int main() 17 { 18 //设定初始区间,并设置其的作用域为main函数内 19 double left = 1.5; 20 double right = 2.4; 21 while(left + 1e-7 < right) 22 { 23 double mid = (left + right) /2; //定义mid存储中间值 24 if(expression(mid) > 1e-7) //若mid大于0,则最小值变大 25 left = mid; 26 else right = mid; //若mid小于或等于0,则最大值缩小 27 } 28 if(fabs(expression(left)) < 1e-7) 29 cout<<left; 30 return 0; 31 }
这道题的要求简单粗暴,就是要让你解出能让函数为0的x值,同时题目给定了一个定义域,我们只要针对这个定义域使用二分法来不断缩短其范围即可。
那么,我们在定义好定义域的最大、最小值后,开始判断如何更改中间值:
1.如果中间值对应的fx大于0,那么证明中间值还有潜力变大,所以将最小值变大(将其等于当前的中间值)。
2.如果中间值对应的fx小于0,那么证明中间值要缩小了,否则得不到能让fx为0的x值,所以将最大值变小(将其等于当前的中间值)。
通过不断循环,我们不断压缩题目给定的定义域范围,最后我们能够得出一个确切的值使得fx为0啦。
这道题的要求简单,其思想也很简单——用二分法压缩定义域范围求出题目要求的值。虽然看起来很简单,但我一开始做题的时候用了递归,又因为没设置好结束条件,导致程序陷入死循环。书上提供的二分法,是用递归思想没错,但不代表二分法就要用递归,这两道题的while就是最好的证明,因此二分法的思想要掌握好:分情况来改变中间值,设置好循环终结条件后,得出的结果就是我们想要的结果啦。