玻璃球如果从一定的高度掉到地上会摔碎。
假设这个恰巧摔碎的高度为 F F F层
任何从高于或等于 F F F的楼层落下的玻璃球都会碎,从比 F F F层低的楼层落下的玻璃球都不会碎。
给你 K K K个玻璃球,如何测试出恰好摔碎的楼层 F F F?
首先,先从两个小球的情况开始考虑,假设第一个球每次跨过 M M M层(如:从1层到 M M M+1层),假如玻璃球在恰巧 M M M+1碎了,则使用第二个小球便从1层到 M M M层逐层测试即可。
那么如果总楼层为100层,则最小测试次数 K K K的公式为:
K = [ 100 M + 1 ] + M K= \left[\frac{100}{M+1}\right]+M K=[M+1100]+M
用Python来计算一下 K K K的最小值:
for M in range(1,30):
print(M,': ', 100//(M+1)+M)
Python输出结果为:
1 : 51
2 : 35
3 : 28
4 : 24
5 : 21
6 : 20
7 : 19
8 : 19
9 : 19
10 : 19
11 : 19
12 : 19
13 : 20
14 : 20
15 : 21
16 : 21
17 : 22
18 : 23
19 : 24
20 : 24
21 : 25
22 : 26
23 : 27
24 : 28
25 : 28
26 : 29
27 : 30
28 : 31
29 : 32
可以看出在步长在7-12之间时,测试次数 K K K达到最小值19。
然而经过简单分析便能得知固定步长得出的结果一定不是最小的。
举个简单的例子:假如有两个玻璃球使用9为步长来计算21层楼需要测试的最小值,那么第一个球将会从第10层扔下,假如没有碎再从第20层扔下,此时的最坏情况是第一个小球从第20层扔下时摔碎了,此时只能用第二个小球从第11层测试到第19层,测试次数为11次。然而想要测试次数更小的话,便可以将第一个球将会从第10层扔下没碎后,再将小球从16层扔下,此时最坏情况是小球碎了,那么第二个小球便需要从第11层测试到第15层,此时测试只需要7次。
由此可见固定步长的方法是不能将测试次数将为最小的,不过也提醒我们需要根据具体的情况确定具体的策略。
有了上面一种测试方法的经验,现在的改进思路便是最大程度利用小球可测试的层数。
假设还是使用两个玻璃球进行测试,这一次不再规定楼层高度,而是思考在规定次数内如何测试最多的楼层,这样得到最大可测楼层 N N N后,只需要找到满足到 N ⩾ M N\geqslant M N⩾M( M M M为总层数)时 N N N的最小值所对应的测试次数即可。因此第一个小球第一次释放的层数应该依据剩下的一个球在剩余次数里最多能测试多少层来确定。
假如规定次数为5次,那么第一个小球从哪里落下合适呢?答案应该是第5层。原因是:如果第一个小球从第5层仍落时恰好摔碎,剩下的一个小球恰好可以使用余下的4次机会逐一测试第1层到第4层,同样的思路假如小球从第5层掉落没有摔碎,那么下一次小球下落的楼层应为第9层,因为小球在第9层摔碎,剩下的一个小球也可以在剩下的3次机会内,测试第6层到第8层。以此类推,第一个小球可以跨过的层数便为4层,3层,2层,和1层,而第一个小球释放层数应为:第5层,第9层,第13层,第14层。假如第一个小球在第14层下落也没碎,那么便可以用剩下的最后一次机会测试第15层,因此最多可测试层数便为15层。
用测试次数T计算最大可测试层数N的公式便是:
N = [ T ( T − 1 ) 2 ] + 1 N=\left[\frac{T(T-1)}{2}\right]+1 N=[2T(T−1)]+1
(注:这个公式其实就是等差数列求和公式)
有了两个玻璃球的基础,再思考三个玻璃球的情况时,确定第一个球跨过楼层的个数应为剩下两个球在剩余次数中最多可测试的层数。
例如一共有6次测试次数,第一个球释放楼层数应是剩下两个球在剩余次数中最多可测试的层数+1,也就是: [ 6 ( 6 − 1 ) 2 ] + 1 \left[\frac{6(6-1)}{2}\right]+1 [26(6−1)]+1
而第一个小球第二次下落层数便是: [ 6 ( 6 − 1 ) 2 ] + 1 + [ 5 ( 5 − 1 ) 2 ] + 1 \left[\frac{6(6-1)}{2}\right]+1+\left[\frac{5(5-1)}{2}\right]+1 [26(6−1)]+1+[25(5−1)]+1
以此类推三个玻璃球的最大可测试层数N公式便为: N = ∑ i = 1 T ( [ i ( i − 1 ) 2 ] + 1 ) N=\sum_{i=1}^T (\left[\frac{i(i-1)}{2}\right]+1) N=i=1∑T([2i(i−1)]+1)
首先,先对两个球和三个球的情况进行初步尝试:
两个玻璃球:
for k in range(1,10):
print(k)
floor=0
for i in range(1,k):
floor+=i*(i+1)/2+1
print(floor)
输出结果:
1
0
2
2.0
3
6.0
4
13.0
5
24.0
6
40.0
7
62.0
8
91.0
9
128.0
三个玻璃球:
for k in range(1,10):
print(k)
floor_4 = 0
for p in range(1,k):
floor_3 = 0
for i in range(1,p):
floor_3 += i*(i+1)/2+1
floor_4 += floor_3
print(floor_4)
输出结果:
1
0
2
0
3
2.0
4
8.0
5
21.0
6
45.0
7
85.0
8
147.0
9
238.0
其中k为测试次数,floor为最大可测试层数。
从输出结果看,可以看出最明显的问题便是:最开始的输出为0。也就是说4个小球测试1次最大可测试楼层为0。然而实际上结果应当为1。
出现这个问题的原因是:
for p in range(1,k):
for i in range(1,p):
当for循环时,i最多只能取到p-1,因此当p=1时,i的范围是1-0,for循环便不能进行下去了。
考虑解决 K K K个小球问题需要一层一层迭代,而这个出现的问题又比较棘手,下面便会介绍一种可行的实现方法。
这个方法是为了弄清上面那个由于“玻璃球个数大于测试次数”引起的问题时所制作的“玻璃球个数与测试次数对应最大可测楼层层数”表格(每个单元格的值根据之前介绍的公式很容易计算)。
玻璃球个数 | 测试1次 | 测试2次 | 测试3次 | 测试4次 | 测试5次 |
---|---|---|---|---|---|
1 | 1层 | 2层 | 3层 | 4层 | 5层 |
2 | 1层 | 3层 | 6层 | 10层 | 15层 |
3 | 1层 | 3层 | 7层 | 14层 | 25层 |
4 | 1层 | 3层 | 7层 | 15层 | 30层 |
与前面得出的公式原理相同,但从表格可以清楚的看出之前所使用的原理:第一个球释放的楼层数应是剩下 N N N-1个球在剩余次数中最多可测试的层数+1。 因此,表格中某一格的值=左面格子的值+左上方格子的值+1。
举个例子:3个玻璃球测试4次可测的最高楼层为:6层+7层+1层=14层。
这便为计算最大楼层提供了一个新的思路:利用球数和测试次数定位并使用上述规律逐行计算。
Python代码:
ball = int(input('玻璃球个数:'))
time = int(input('尝试次数:'))
line_1 = list(map(lambda x:x, range(1,time+2-ball)))
def line_2(line_1):
line = [1]
for i in range(1,len(line_1)+1):
x=line[i-1]+1+line_1[i-1]
line.append(x)
return line
for i in range(0,ball-1):
line_1 = line_2(line_1)
在上述代码可以输入玻璃球的个数和测试次数计算最大可测楼层。
如果是想输入玻璃球个数和总楼层数计最小测试次数,会有很多实现方法。
这里先提供一个解法(这个并不是最简单的转换方法,但是思路是最直接的):
ball = int(input('玻璃球个数:'))
floor = int(input('总楼层数:'))
time = 5
line_1 = list(map(lambda x:x, range(1,time+2-ball)))
cont=-1
def line_2(line_1):
line = [1]
for i in range(1,len(line_1)+1):
x=line[i-1]+1+line_1[i-1]
line.append(x)
return line
for i in range(0,ball-1):
line_1 = line_2(line_1)
while cont==-1:
if line_1[-1]<floor:
time+=5
line_1 = list(map(lambda x:x, range(1,time+2-ball)))
for i in range(0,ball-1):
line_1 = line_2(line_1)
else:
for i in range(0,len(line_1)):
if line_1[i]>=floor:
cont=i+1
print('最小测试次数:',cont)
break
当然这个转换的方法不是最有效率的,提供一个改进的思路:发现最大楼层依旧小于总楼层时,可以让time变大,并从目前得出的最大楼层继续计算。这样就会比重新计算到新的time效率更高。