做算法第二章作业的编程题有感

序言:第二章的题目实在是花费了我很多时间去想,甚至有些题目我并不能仅依靠自己的能力,还要上网去查明某些算法的实现,才能解出这道题。虽然本章作业的五道题目都不容易,但其思想没有脱离本章内容——分治法和递归,因此我们一一解读一下部分题目,并总结一下它们的解题思想。

 

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就是最好的证明,因此二分法的思想要掌握好:分情况来改变中间值,设置好循环终结条件后,得出的结果就是我们想要的结果啦。

 

 

 

 

posted @ 2019-10-06 17:37  算算时间复杂度  阅读(324)  评论(0编辑  收藏  举报