入门级动态规划:2018年第九届蓝桥杯省赛B组第四题—测试次数( 摔手机 )

目录

下面列出用动态规划如何解决此问题

 

原题描述:

        x星球的居民脾气不太好,但好在他们生气的时候唯一的异常举动是:摔手机。
各大厂商也就纷纷推出各种耐摔型手机。x星球的质监局规定了手机必须经过耐摔测试,并且评定出一个耐摔指数来,之后才允许上市流通。
        x星球有很多高耸入云的高塔,刚好可以用来做耐摔测试。塔的每一层高度都是一样的,与地球上稍有不同的是,他们的第一层不是地面,而是相当于我们的2楼。
        如果手机从第7层扔下去没摔坏,但第8层摔坏了,则手机耐摔指数=7。特别地,如果手机从第1层扔下去就坏了,则耐摔指数=0。如果到了塔的最高层第n层扔没摔坏,则耐摔指数=n

        为了减少测试次数,从每个厂家抽样3部手机参加测试。
        某次测试的塔高为1000层,如果我们总是采用最佳策略,在最坏的运气下最多需要测试多少次才能确定手机的耐摔指数呢?
        请填写这个最多测试次数。

        注意:需要填写的是一个整数,不要填写任何多余内容

        先把答案写在前面 19


当初没有注意到题目中的关键,只提供3部手机供你测试摔手机。直接一个二分法lg1000/lg2<10,交了个10,于是华丽丽的错掉了,二分法虽然可以在最短次数测到最坏可能性,但是致命的是它每次直接从中间高度摔手机,手机很有可能摔坏,如果想要用二分法摔出耐率指数是0的手机,那么足足需要摔坏10部手机!

        还有要说明的是,用二分法摔手机必须要搜索匹配到所有0~1000的值,因为耐率指数可以是0,选择中间值排除另一边或是选取另一边的过程中,对于楼层一来说是不起作用的,因为它需要特别照顾,专门摔它才能知道耐率指数是0还是1。这个陷阱对于1000这个高度没什么,但如果是512层,二分法摔手机就得是10次了( lg513/lg2 )。

        那如果只给了一部手机,怎么测出耐率指数呢?只好从1层开始摔,摔坏了就是(当前层数-1)的耐率指数。假如是两部手机呢?我一开始的想法是对1000进行分块,先用第一部手机摔出目标层数在哪一个小块里,然后再用第二部手机从那个小块最低层一层一层往上摔,直到确认为止。按照这个想法1000层需要分成 a*b*c 三个手机分别负责一个变量,最后就能确认目标层数。

        为了给自己的歪理加个公式增强点可信度,我想到了高中的基本不等式……3√(a*b*c)<=(a+b+c)/3 ,设a*b*c是1000,这么一算a+b+c最小值不就是30吗,a=b=c=10的时候成立。这么看来最难摔到的层数应该就是1000层了,第一部手机摔9次才能确定目标层数在901~1000( 摔101、201…901 ),然后再摔上9次才能确定目标层数在991~1000( 摔911、921…991 ),再摔上9次才能确定是1000( 摔992、993…1000 )确定出目标层数1000。可能有疑问为什么这里不考虑1了呢,因为我们采用的是自底向上排查,虽然1~10的范围必须要摔上10次才能确定每个值,但是1~10的范围只需要摔101和11就能确定下来( 两次 ),所以我们不考虑它会额外产生次数。因此这种方法摔手机27次能用三部手机摔出目标楼层!

        是不是有点太多了?!!

        ****显然是的,虽说分块分的很合理看上去无懈可击,但是这是建立在分同等长度块的前提下的。找到每个楼层的次数严重不均匀,因为是自底向上的摔,位于前面的楼层可以在短短几次就能确定范围,排在后面的楼层却需要许多次才能确定下范围,这无疑是不公平的,只有位于后面的楼层才会用最大次数摔后确定下来。

        那么为了均衡,把前面的块范围加大,后面块的范围缩小。这样前面的块很快能确定,但是它内部需要很多次才能确定到底是哪一个,而后面的块很多次才能确定但内部很小,因而很快可以确定是哪一个!完美的解决方法……

        如果是20层楼,我们用 2 部手机手机摔。那么最后一个确定的块范围定为1,那么5+5+4+3+2+1 这么划分是最合理的,在每一个块的最后一层扔第一部手机( 这是为了照顾第一层 ),确认下来目标楼层在哪个块之后在块内部用第二部手机扔。第一个块扔一次可以确定,但是内部需要4次;第二个块需要2次确定范围,内部需要4次确定;最后一个块需要摔上5次才能确定,但是内部摔上1次就可以。所以最后的结果应该是6次。这种分法恰好解决了必须要摔1楼的坑!最后一个块必摔一次,前面的块摔长度减一次。

        那当我们有三部手机呢,更深一层的分块,也就是说把全部楼层划分后用两部摔出每个块的内部……继续根据每个块的内部次数分配楼层!!这么来手算就太费劲了,现在看来显然是个动态规划问题了,分解成用 cnt 部手机可以在 ind 楼层最少摔多少次可以确定全部楼层?在一部手机的时候肯定是从1~1000了,一层一层确认。

以上内容分析了是如何得到最小摔手机次数的,核心在于大于两部手机时需要分块后摔第2~n部手机,且块内层数必须大于等于1,这样最后一部手机才有利用价值。

下面列出用动态规划如何解决此问题

1.设ind为当前层数,cnt为当前拥有手机个数,dp[ind][cnt]为此时最少摔多少次。

2.cnt=1时,没有任何花里胡哨,从低往高一层一层摔。

dp[ind][1]=ind

3.cnt>=2时

先看一个小例子,ind=4,cnt=2时,如何计算?目标当然是得到dp[4][2]的值。

—>用表格列出得到过程,cnt=1的情况先写出。

  ind=1 ind=2 ind=3 ind=4
cnt=1 1 2 3 4
cnt=2        

 

 

 

 

ind=1,cnt=2时,dp[1][2]显然也是1,一层楼摔一次足矣。

ind=2,cnt=2时,2层楼,摔一次肯定不够,必须摔两次。

  ind=1 ind=2 ind=3 ind=4
cnt=1 1 2 3 4
cnt=2 1 2    

 

 

 

 

ind=3的时候呢??

首先可以确定的是,用2层楼确定的最小次数+1,多摔上一次肯定能确定出3层楼最少用多少次。也就是说dp[3][2]最大值就是dp[2][2]+1  ,只需要确定的能不能等于dp[2][2]……

****有三层楼,手中拿出一部手机,可以去1~3层摔对不对,一开始说了那么多怎么个分块法得到最小次数, cnt 又大于1,肯定是需要分块后摔手机的。分块最小是块中有一层,这样下一部手机才不会没有用处。

于是乎拿出一部手机摔的层数肯定是(1+1)~(3-1)层(2层)

在2层这部手机摔碎了,那么就需要用剩下的cnt-1部手机来确定1层(2-1层~1层,共一层)手机是否会摔碎;(dp[1][1]+1)

在2层这部手机没有摔碎,那么就需要用cnt部手机来确定3层(2+1层~最高层,共一层)手机是否会摔碎;(dp[1][2]+1)

+1的意义是在2层摔的这部手机,是摔了一次的。

摔碎和没摔碎这两种情况的最大值就是在当前楼层摔手机可以得到的最小摔手机次数。

现在我们看看分析得到的结果

(1)    dp[3][2]<=dp[2][2]+1

(2)    dp[3][2]>=Max( dp[3-k][2]+1 , dp[k-1][1]+1 )

k大于1,小于ind(此处k只能等于2)。

我们需要的当然是最小值,即==>     dp[3][2]=Max( dp[3-k][2]+1 , dp[k-1][1]+1  )    =  2

  ind=1 ind=2 ind=3 ind=4
cnt=1 1 2 3 4
cnt=2 1 2 2  

 

 

 

 

同理分析ind=4,cnt=2时。

最大值:dp[3][2]+1 = 3

最小值: k=2,Max( dp[2][2]+1 ,dp[1][1]+1 ) = 3

               k=3,Max( dp[1][2]+1 ,dp[2][1]+1 )  = 3

dp[4][2] = 3

  ind=1 ind=2 ind=3 ind=4
cnt=1 1 2 3 4
cnt=2 1 2 2 3

 

 

 

 

完成!四层楼用两部手机至少需要摔 dp[4][2] = 3 次。

总结以上,dp[ind][cnt]=Min( dp[k-1][cnt-1] ,dp[ind-k][cnt] ) +1     1 < k < ind

还有个毛病是:cnt=1、2的时候,k是不存在的,这时候采用最大值 dp[ind-1][cnt]+1 即可,初始化 dp[0][cnt]=0

综合起来采用:dp[ind][cnt] = Min( 1+dp[ind-1][cnt] , 1+Max( dp[k-1][cnt-1] , dp[ind-k][cnt] )     1 < k < ind

 

#include<stdio.h>
#define Max(a,b) (a>b?a:b)
#define Min(a,b) (a<b?a:b)
int dp[1005][50];
int main(int argc, char* argv[])
{
    int n,m;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++)
        dp[i][1]=i;
    for (int cnt=2;cnt<=m;cnt++)
    {
        for (int ind=1;ind<=n;ind++)
        {
            dp[ind][cnt]=1+dp[ind-1][cnt];
            for (int k=2;k<ind;k++)
                dp[ind][cnt]=Min(dp[ind][cnt],1+Max(dp[k-1][cnt-1],dp[ind-k][cnt]));
        }
    }
    printf("%d\n",dp[n][m]);
    return 0;
}

        附上代码,输入1000 3即可得到结果 19

END