分享一道最近刷到的面试题:
有一种玻璃杯质量确定但未知,需要检测。
有一栋100层的大楼,该种玻璃杯从某一层楼扔下,刚好会碎。
现给你两个杯子,问怎样检测出这个杯子的质量,即找到在哪一层楼刚好会碎?
看到这道题的时候我自然地想,从一楼开始扔起...这还不简单?然而之前面试挂掉的经验让我知道,这时候面试官一定会问:
“你再多想想?有没有更快的方法?”
首先要分析一下题目,和面试官争取一波思考的时间:
1、两个杯子质量一样;
2、如果杯子在第N楼扔下去没有碎,那在小于N楼扔下去也不会碎;
3、如果杯子在第N楼扔下去碎了,那就得往小于N楼继续检验(但杯子只有两个);
4、如果杯子扔下去没碎,还能反复利用。
总的来说就是,通过有限的两个杯子,快速找到在第N-1楼扔下去不碎而第N楼扔下去会碎的这个楼层N。
方案一:二分法
这不就是查找吗,不过得保证在杯子用完之前能找到这个楼层,作为一个朴实的算法菜鸟,我首先就想到了——二分法:
先从50楼扔下去一个杯子,没碎的话就往上走,从75楼扔下去,没碎就继续,直到找到刚刚好碎掉的楼层。然而事情不会这么顺利,在这种方案下我们只能往上走。一旦第一个杯子碎了,就得从最低楼层往上找了,比如说:我从50楼扔下去,没碎,再从75楼扔下去,碎了,这时候只能从51层开始往上尝试,如果刚好结果是74楼的话,总共得扔1+24=25次。所以最坏的情况也出来了:从50楼扔下去,碎了,然后乖乖从1楼开始往上试,如果刚好结果是49楼的话,总共得扔1+49=50次。
面试官:“还行,次数少了一半了,你再多想想?”
方案二:分段查找
如果二分不行,那就把区间分得比二分再细一些,也就是分段地扔,比如说把100楼分成10段,从10楼开始往上,逐十楼地扔。
这样最好的情况是:从10楼扔下去刚好碎了,从1楼开始试,假如恰好在9楼碎了,那总共得扔1+9=10次。
最差的情况是:从10楼开始扔,没碎,继续走到20楼开始扔,也没碎,一直到了100楼扔下去碎了,然后从91楼开始往上尝试,假如恰好在99楼碎了,总共得扔10+9=19次。
面试官:“可以,更快了,但我不想多跑几次,能不能告诉我最少扔几次就能确定出楼层?”
方案三:解方程
这么说有理论上的最少尝试次数?其实从前面两种方案能够看出我们试错的规律:假如我只需要扔x次,那必须得从第x楼开始扔,这是因为碎了的话,我还可以尝试前面的x-1楼,确保能够找到目标楼层。那根据碎和不碎就有两种情况:
- 如果第一个杯子从x楼扔下去碎了,第二次就从x+(x-1)楼开始尝试,如果还没碎,那第三次就到x+(x-1)+(x-2)楼尝试,就依此类推......
- 如果中途碎了,我们有x-m次尝试机会回去尝试,能够以少于x-m次机会找到目标楼层。
假如经过x次尝试能够到达的最高楼层就是x+(x-1)+(x-2)+ ... +1=x(x+1)/2,如果最高楼层是100,那求解x(x+1)/2>=100,得到x>=14,即按上述方案至多尝试14次就能确保找到目标楼层,所以最少尝试次数就是14次。
相信此时面试官已经满意了,但是我不满意了。
“万一下次它问我3个杯子怎么办?再多想想?”
方案四:动态规划推广
当我们只有一个杯子的时候,其实能做的事情只有从低楼层开始往上尝试。有了两个以上的杯子就有了多次试错的机会,减少尝试的次数。如果只要找到最优解,那可以通过动态规划,将N个杯子的问题拆成N-1个杯子的子问题和1个用于试错的杯子。
将问题标记为W(n,k),其中 n 代表可用于测试的杯子数,k 代表被测试的楼层数。
对于问题 W(2,100), 我们可以如此考虑:
-
将第 1 个杯子,在第 i 楼扔下( i 可以为 1~k 的任意值),如果碎了,则我们需要用第 2 个杯子,解决从第 1 楼到第 i-1 楼的 子问题 W(1,i-1);
-
如果这个杯子没碎,则我们需要用这两个杯子,解决从 i+1 楼到第 100 楼的子问题 W(2,100-i)。
解决上述的两个子问题后,我们可以取两者中尝试次数最多的方案(排除掉因为欧气快速找到楼层的情况,确保能够找到目标楼层),与在第 i 楼尝试的这次相加,即得到了在第 i 层扔下杯子时 W(2,100)这个问题的最少尝试次数。
而在所有杯子从100楼中任意楼层扔下去,产生的100个测试方案,它们的尝试次数恒大于等于一个理论上最少的尝试次数T,这个T就是本问题的答案。
递推公式:
W(n, k) = 1 + min{max(W(n -1, x -1), W(n, k - x))}, x in {2, 3, ……,k}
同理,我们可以拓展N个杯子、M个楼层的问题!
我:“我还能想,别拦着我!”