二进制表示法和二分搜索算法实现百水查毒需求

HYF Lv3

最近看到这么一个面试问题:“有 100 瓶水,其中 1 瓶有剧毒,如何最快找到它”。那么在我们专业领域中,应该如何去解决呢?

首先,在我看来这个问题是比较不严谨的,我们不妨给他加点条件:
1.喝下剧毒后不会立刻死亡,而是会在一定时间后升天(给予实验足够多的操作时间)。
2.剧毒可以在被无限稀释后依然致死。
3.我们有若干个大杯子和若干只食水量无限且健康无传染病的小白鼠
4.大杯子和小白鼠是需要一定的金币去兑换。与金币的比例皆为 1:1。
本文来研究一下如何使用最少的金币,完成这个需求。

利用二进制表示特性

世界上只有 10 种人: 懂二进制的 不懂二进制的

当我们使用二进制表示数字时,每个数字位都代表一个权值。在这个问题中,我们有 100 瓶水,需要找出哪瓶水有毒。我们可以用二进制表示来编码瓶子的编号,从而实现高效的解决方案。
让我们从最简单的情况开始,假设我们只有 10 瓶水(编号 1 到 10 )。10 (瓶水)为 2^3(8) 和 2^4(16) 之间,所以需要使用 4 位二进制表示。同时我们需要使用 4 个大杯子以及 4 只小白鼠。编号从”0001” 到 “1010”(1 到 10 的二进制表示)。例如第 5 瓶水,它的编号是”0101”。

我们需要使用 4 个编号分别为 “1”, “2”, “3”, “4” 的大杯子,每个大杯子分别负责 4 位二进制的第 1、2、3、4 位为 1 的情况从最低位(个位)到最高位(千位)。

随后我们需要根据瓶子的编号,依次在大杯子中倒入瓶内的水。例如,第 2 瓶水的编号为 “0010”,那么这瓶水需要在编号为 “2” 的大杯子中倒入一些水,第 7 瓶水为 “0111”,那么则需要在编号为 “1”、编号为 “2” 以及编号为 “3” 的大杯子中分别倒入一些水。

在做完该操作后各个大杯子将有以下编号的瓶子中的水:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
编号为 "1" 的大杯子中有:瓶子编号为 1(0001)、3(0011)、5(0101)、7(0111) 的水
编号为 "2" 的大杯子中有:瓶子编号为 2(0010)、3(0011)、6(0110)、7(0111) 的水
编号为 "3" 的大杯子中有:瓶子编号为 4(0100)、5(0101)、6(0110)、7(0111) 的水
编号为 "4" 的大杯子中有:瓶子编号为 8(1000) 的水

(各编号及其4位二进制:
编号 1:0001
编号 2:0010
编号 3:0011
编号 4:0100
编号 5:0101
编号 6:0110
编号 7:0111
编号 8:1000)

接下来,我们将这 4 个大杯子分配给 4 只小白鼠,每个小白鼠负责饮用所分配的大杯子中的水,依然是从最低位(个位)到最高位(千位)。

通过观察每只小白鼠的死活情况,我们可以确定哪些位置上有毒。在这个例子中,假设升天的是喝了编号为 “1”、编号为 “2” 以及编号为 “3” 的三只老鼠,且升天为 1 存活为 0,那么(转化为二进制表示)就是 “0111”,转化为十进制就是 7,表示瓶子编号为 7 的有毒。

那么回到原问题,当有 100 瓶水并且其中一瓶有毒时,我们可以将每瓶水的编号表示为一个 7 位的二进制数(因为 2^7 = 128 > 100,所以 7 位足够),且我们需要 7 个大杯子以及 7 只小白鼠。我们将这些编号按照从 1 到 100 的顺序分配给瓶子并将其转换成二进制。随后与上方一致,根据瓶子的编号,依次在大杯子中倒入瓶内的水并分配给每只小白鼠饮用。最后观察 7 只小白鼠的死活情况。如果第 1 只、第 3 只以及第 7 只小白鼠都死了,那么我们就知道有毒的水瓶编号的二进制表示是 “1000101”,转化为十进制就是 69,也就是第 69 瓶水为剧毒。

该方法需要 floor(log2(N)) + 1 个大杯子以及同等数量小白鼠。(log2 表示以2为底的对数,floor 表示向下取整操作)。在该需求中,共需要 7 + 7 = 14 枚金币。

可能有读者会想,实际生活哪有那么麻烦。找 100 只小白鼠拿个杯子(不污染水)挨个喂(不用一只的原因是存在毒发时间),比你这快多了。虽然这样快一些,但需要 101 枚金币。(如果领导给你 101 枚金币,你只花了 14 枚,剩下的 87 枚呢?嘿嘿……

使用二进制表示法的效率并不佳,因为需要对水瓶进行重复操作。且十分浪费水资源,每个水瓶转换的二进制含有 1 的都需要倒出水。假设我们每次只需要倒出 1 滴水,我们来计算一下总共需要倒出多少滴水。

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
/**
* @author -侑枫
* @date 2023/8/19 22:35:19
*/
public class CountOnesInBinary {
public static int countOnes(int num) {
int count = 0;
while (num > 0) {
// 通过与运算检查二进制的最低位是否为 1
if ((num & 1) == 1) {
count++;
}
// 将整数右移一位,相当于将二进制位向右移动一位
num >>= 1;
}
return count;
}

public static void main(String[] args) {
// 总共的二进制中 1 的个数
int totalOnes = 0;
for (int i = 1; i <= 100; i++) {
totalOnes += countOnes(i);
}
System.out.println("从 1 到 100 的二进制中总共有 " + totalOnes + " 个 1。");
}
}

执行结果如下:

1
2
3
从 1 到 100 的二进制中总共有 319 个 1。

Process finished with exit code 0

使用了 319 滴水。
那么有没有更快一些,且使用经济差不多的方法呢?有的,比如:
赌徒:喝下去死了就是毒药,没死就是水
企业家:开实习证明让大学生来喝
厨师:水烧滚后不会变色
化学家:做实验提纯
律师:倒掉一瓶弄死一只,有人喝到就告他诽谤
咳咳~~ 开个玩笑,当然是:

使用二分搜索算法

二分搜索法(Binary Search)是一种在有序数组中查找特定元素的高效算法。它通过将数组分成两半并比较中间元素与目标元素的大小关系,然后根据比较结果来确定目标元素在数组的哪一半,从而缩小搜索范围。这个过程不断地将搜索范围缩小一半,直到找到目标元素或者确定目标元素不存在于数组中。

那么该算法在我们的需求中要怎么去使用呢?在操作前我们需要将这 100 杯水编号为 1 到 100。

步骤 1:需要使用第一个大杯子,将中间的瓶子(第 50 瓶水)倒入一滴水,并给一只小白鼠喝。如果这杯水是有毒的,那么问题就解决了。如果小白鼠没有升天,则进行下一步骤。

此刻拥有资源(大杯子*1 小白鼠*1)
此刻已使用资源(大杯子*1 小白鼠*1)

步骤2:如果这杯水不是有毒的,根据二进制搜索的原理,你知道有毒水杯在剩余的99杯水中。将其分成平均两份,编号 1 到编号 49 各倒入一滴到大杯子中给小白鼠引用,再取一个新的大杯子在其内倒入编号 51 至 100 的水瓶中的水给另一只小白鼠引用。若第一只小白鼠升天,则编号 50 到编号 100 的水瓶无毒,反之则编号 1 到编号 50 无毒。处理丢弃升天的小白鼠以及毒的大杯子。

此刻拥有资源(大杯子*1 小白鼠*1)
此刻已使用资源(大杯子*2 小白鼠*2)

步骤 3:接下来,重复步骤 1 的动作。假设第 1 到 49 中有毒,将中间的瓶子(第 25 瓶水)倒入一滴水,并给一只小白鼠喝。如果这杯水是有毒的,那么问题就解决了。如果小白鼠没有升天,则进行下一步骤。

此刻拥有资源(大杯子*1 小白鼠*1)
此刻已使用资源(大杯子*2 小白鼠*2)

步骤 4:接下来,重复步骤 2 的动作。将其分成平均两份,编号 1 到编号 24 各倒入一滴到大杯子中给小白鼠引用,再取一个新的大杯子在其内倒入编号 26 至 49 的水瓶中的水给另一只小白鼠引用。观察小白鼠存活情况,并处理丢弃升天的小白鼠以及毒的大杯子。

此刻拥有资源(大杯子*1 小白鼠*1)
此刻已使用资源(大杯子*3 小白鼠*3)

……

接下来,不断重复以上操作,通过不断将搜索范围缩小一半,你可以在最坏的情况下最多进行 7 次测试就找到有毒的水杯。这种方法比二进制表示法要更高效,节省了从水瓶倒水的次数,且通过二分查找将搜索范围迅速缩小,从而节省了时间和资源。

那么最终,在最好的情况下,我们只需要使用 1 个大杯子以及同等数量的小白鼠,而最坏的情况,我们将使用到最多 7 个大杯子以及同等数量的小白鼠。即最少 2 枚金币,最多 14 枚金币的情况下完成这个需求。相比二进制表示法一定为 7 的情况,存在一定的优化。且对水资源的浪费一定存在缓解。

那么相比二进制表示法,该方法能节省多少水资源呢?我们通过代码来计算一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int initialValue = 100;
int result = 0;

while (initialValue > 2) {
result += initialValue;
initialValue /= 2;
}

result += 2;

System.out.println("最多需要倒出:" + result+" 滴水");
}

执行结果:

1
2
3
最多需要倒出:198 滴水

Process finished with exit code 0

相比上文的 319 滴水,是不是节省了不少水资源呢?

这位打工仔,你也不想你的老板发现你找一瓶剧毒倒了那么多水吧?
使用二分搜索算法我们能在节省浪费水,又节省金币的情况下,更完美地完成”百水查毒”的需求。

想要通过动画了解二分搜索算法的执行,可以访问以下链接:搜索排序列表

最后附上本文所写源代码:二进制表示法和二分搜索算法实现百水查毒需求

  • 标题: 二进制表示法和二分搜索算法实现百水查毒需求
  • 作者: HYF
  • 创建于 : 2023-08-19 23:26:32
  • 更新于 : 2024-04-18 00:22:34
  • 链接: https://youfeng.ink/binary-377778ffcd26/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
二进制表示法和二分搜索算法实现百水查毒需求