经典的动态规划问题,题设是这样的:
如果你有2颗鸡蛋,和一栋36层高的楼,现在你想知道在哪一层楼之下,鸡蛋不会被摔碎,应该如何用最少的测试次数对于任何答案楼层都能够使问题得到解决。
1、傻子都能想到的答案(零分):折半二分查找
2、将36分为6个部分(五分):1-6、7-12、13-18、19-24、25-30、31-36,第一个第一次鸡蛋扔下楼层序列为6、12。。。等,每次将范围缩小6,最坏最多需要11(12?)次;
3、
我们可以将这样的问题简记为W(n,k),其中n代表可用于测试的鸡蛋数,k代表被测试的楼层数。对于问题W(2,36)我们可以如此考虑,将第1颗鸡蛋,在第i层扔下(i可以为1~k的任意值),如果碎了,则我们需要用第2颗鸡蛋,解决从第1层到第i-1层楼的子问题W(1,i-1),如果这颗鸡蛋没碎,则我们需要用这两颗鸡蛋,解决从i+1层到第36层的子问题W(2,36-i),解决这两个问题,可以分别得到一个尝试次数p,q,我们取这两个次数中的较大者(假设是p),与第1次在i层执行测试的这1次相加,则p+1就是第一次将鸡蛋仍在i层来解决W(2,36)所需的最少测试次数次数ti。对于36层楼的问题,第一次,我们可以把鸡蛋仍在36层中的任何一层,所以可以得到36中解决方案的测试次数T{t1,t2,t3,……,t36},在这些结果中,我们选取最小的ti,使得对于集合T中任意的值tj(1<=j<=36,j!=i),都有ti<=tj,则ti就是这个问题的答案。用公式来描述就是W(n, k) = 1 + min{max(W(n -1, x -1), W(n, k - x))}, x in {2, 3, ……,k},其中x是第一次的测试的楼层位置。
其中W(1,k) = k(相当于1颗鸡蛋测试k层楼问题),W(0,k) = 0,W(n, 0) = 0
所以在计算W(2,36)之前,我们需先计算出所有W(1,0),……,W(1,36),W(2,0),……,W(2,35)这些的值,可以用递推的方法实现,代码如下:
1) 最优子结构
当我们从一个楼层x扔下鸡蛋时,有可能出现两种情况(1)鸡蛋破(2)鸡蛋不破。
1)鸡蛋破,那么我们只需要用剩下的鸡蛋测试 x层以下的楼层; 所以问题简化为x-1层和n-1个鸡蛋
2)如果鸡蛋没有破,那么我们只需要检查比x较高的楼层; 所以问题简化为 k-x 和n个鸡蛋。
最优子结构可以表示为:
1 |
k ==> 楼层数 |
2 |
n ==> 鸡蛋数 |
3 |
eggDrop(n, k) ==>最少需要的测试次数(考虑所有情况) |
4 |
eggDrop(n, k) = 1 + min{max(eggDrop(n - 1, x - 1), eggDrop(n, k - x)): |
5 |
x 属于 {1, 2, ..., k}} |
下面用递归的方法解决这个问题:
unsigned int DroppingEggsPuzzle(unsigned int eggs, unsigned int floors)
{
unsigned int i, j, k, t, max;
unsigned int temp[eggs + 1][floors + 1];
for(i = 0; i < floors + 1; ++i)
{
temp[0][i] = 0;
temp[1][i] = i;
}
for(i = 2; i < eggs + 1; ++i)
{
temp[i][0] = 0;
temp[i][1] = 1;
}
for(i = 2; i < eggs + 1; ++i)
{
for(j = 2; j < floors + 1; ++j)
{
for(k = 1, max = UINT_MAX; k < j; ++k)
{
t = temp[i][j - k] > temp[i - 1][k -1] ? temp[i][j - k] : temp[i - 1][k -1];
if(max > t)
{
max = t;
}
}
temp[i][j] = max + 1;
}
}
return temp[eggs][floors];
}
该算法的空间复杂度是O(nk),时间复杂度是O(nk^2),对于规模较大的问题,无论是空间还是时间复杂度都很可观。
这个算法可以计算出W(2,36)问题的最少测试次数是8,但是却不能给出用2颗鸡蛋解决36层楼问题的具体方案
其实有公式可以算出具体方案:设第一次扔鸡蛋的楼层为X,第二次为X-1+X,。。。
保证X+X-1+X-2+......+1>=K,K为楼层,这里给的是36,求得X = 8。
这里就给出一个测试方案:
该方案可以保证,无论满足条件的楼层是多少,都可以在最多8次测试之后找到答案,例如目标楼层为28时,该方案的测试顺序为8,15,21,26,30,27,28,总共测试7次,有兴趣的读者可以尝试一下其他情况。
该方案解决W(2,36)问题比较优雅,但是却暗藏一个很大的玄机,那就是一般我们见到的这个问题的题面,往往是W(2,15),W(2,36),不知道读者考虑过没有,为什么非让我们计算2颗鸡蛋测试36层楼的情况,而不是35层或者37层?下面是用之前的算法解决W(4,50)问题的递推结果表格(其中,行代表楼层数1~50,列代表鸡蛋数1~4),我们会发现,W(2,36)=8,W(2,37) = 9,那么是不是用2颗鸡蛋测试8次,最多只能解决36层楼问题,对于37层就无能为力了呢?
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
1 | 2 | 2 | 3 | 3 | 3 | 4 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | 5 | 6 | 6 | 6 | 6 | 6 | 6 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 8 | 9 | 9 | 9 | 9 | 9 | 9 | 9 | 9 | 9 | 10 | 10 | 10 | 10 | 10 |
1 | 2 | 2 | 3 | 3 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 7 |
1 | 2 | 2 | 3 | 3 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
这里引出了一个问题:n个鸡蛋,测试m次(简记为D(n,m)),最大可以解决几层楼的问题,通过对递推结果表格的观察,我们可以得到如下结论
对于第二点,以D(4,4)为例,我们第1次在8楼扔下鸡蛋,如果碎了,则第二次在4楼扔下鸡蛋,否则在12楼扔下鸡蛋,对于在4楼扔下鸡蛋的情况,之后可以分别在2楼或者6楼扔下鸡蛋,如此进行,就可以找到答案楼层,方法与二分查找一样。例如答案楼层是5的情况,测试序列为8,4,6,5。
对于第三点,如果有5个鸡蛋让你测试3次,即使三次测试鸡蛋都碎了,剩下的2个鸡蛋也派不上用场,所以D(5,3) = D(3,3)
发现这些关系之后,我们似乎找到解决n个鸡蛋测试m次最大能够解决楼层数的方法。对于D(n,m){n < m}而言,对于其能够测试的最大楼层数k,我们可以构造这样的场景,将第一颗鸡蛋仍在楼层i,使得第i + 1层到第k层是D(n,m-1)可以解决的最大楼层数,第1层到第i - 1层是D(n-1,m-1)可以解决的最大楼层数,由此得到递推关系D(n,m) = D(n -1,m-1) + 1 + D(n,m-1),然后对D(n,m-1),D(n-1,m-1)再按照上述公式分解,直到得出刚才所列的三种可计算情况(n = 1,或者m <= n)为止,再进行回溯累加,就可以得到D(n,m)的值,代码如下:
根据此算法,我们可以得出D(2,5)=15,D(2,8)=36,也就是说,2个鸡蛋测试5次最多可以解决15层楼的问题,测试8次最多可以解决36层楼的问题。可见,出这个题的人并不是随便找两个楼层数陪咱们玩玩,而是对此问题认真研读后的结果。有了此利器之后,我们解决扔鸡蛋问题的的方法将得到大幅简化,对于n个鸡蛋解决k层楼的问题我们只需找到这样的值m,使得D(n,m-1)
该算法的时间和空间复杂度不太好分析,但都要好于传统的DP算法,有兴趣的读者可以推敲一下,在我的机器上测试10个鸡蛋,5000层楼的情况,第二个方法比第一个要快10万倍!注意到算法2也是一个动态规划问题,所以可以用一个n*m的矩阵来保存计算过程中的中间结果,算法的效率还可以得到很大提升!
不管是算法1,还是算法2,都没有给出用n个鸡蛋如何通过m次测试,解决k层楼的问题,对此我根据算法2给出一个思路。对于满足条件D(n,m-1)
这其中每个单独的1,都代表一次独立测试,这些1后面中的中括号代表其是第几次独立测试,与其从公式中分离出来的时机相关,最早分离出来的1,其值就是[1],第二次分离出来的1,其值就是[2],这些1的目的就是把k层楼分解为若干个可直接计算的子部分。我们取出两者不同的部分D(1,3)+1[2]+D(1,2)+1[3]+D(2,2)+1[1],这部分表示通过增加了一次测试,我们所获得的额外的探测能力,通过改造这部,使得这部分的和等于k-D(n,m-1),然后将改装部分与两者的相同部分结合,形成新的结果,这些结果从前到后,对应着楼层从下到上的测试方案
上例中我们知道D(3,4)=14, D(3,5)=25,对于14 < k <= 25,我们用k减去14得到需要构造的值,尽量保留右侧的算式,只改变最左侧的算式,例如对于k = 15,不同部分可以用1替换,对于k = 16可以用D(1,1)+1替换,对于k = 18可以用D(2,2)+1替换,对于k = 21可以用D(1,2)+1+D(2,2)+1替换。以21为例,我们将改造结果和D(3,4),D(3,5)的相同部分结合,形成
D(1,2)+1[2]+D(2,2)+1[1]+D(1,2)+1[3]+D(2,2)+1[2]+D(3,3)