https://leetcode.com/problems/poor-pigs/
(本文配了对题目解答的严格数学证明,不仅证明了至少所需要小猪数量的上界,也证明了下界。绝大部分网上的文章只是说明了一定数量的小猪可以找到毒水,但没有证明为什么少于那么多小猪就找不到毒水。题目问的是最少需要的小猪数量是多少,所以解的正确性和最优性都必须要证明。本文对最优性也给予了严格证明。)
给定一列桶,其中有且仅有一只桶里面是毒药,其余都是普通的水。如果一个小猪喝了毒水,过一段fixed的时间会毒死(在这段时间里不能喝别的水,只能观察)。假设小猪每次可以同时喝很多桶水,并且每次小猪们如果喝水也都是同时喝。给定一段时间,问多少只小猪可以保证在这段时间里验证出哪只桶有毒。
这道题实际上是一道编码理论的题。我们先证明一个引理:
设有一列数字 0 , 1 , 2 , 3 , . . . , n − 1 0,1,2,3,...,n-1 0,1,2,3,...,n−1,一共 n n n个数字;假设有一个人甲,心中想了一个在这个范围内的数字,另一个人乙想猜出甲心中想的是什么数字,但乙只能问形如 k k k选 1 1 1的选择题,并且有且仅有一个选项是对的。问乙至少需要问多少次才能保证必然能猜出甲心中想的数字。
假设乙至少要问 c c c次就能猜出。我们先考虑 c c c的上界。将 0 , 1 , 2 , . . . , n − 1 0,1,2,...,n-1 0,1,2,...,n−1这 n n n个数字转换为 k k k进制整数,那么最大的数 n − 1 n-1 n−1有 ⌊ log k ( n − 1 ) ⌋ + 1 \lfloor\log_k(n-1)\rfloor+1 ⌊logk(n−1)⌋+1位(这里多少位的意思是,写成最高位非 0 0 0的 k k k进制数后,有多少位)。
我们先证明这个小结论。设 m m m在 k k k进制下有 x x x位,那么必然有 k x − 1 ≤ m ≤ k x − 1 k^{x-1}\le m\le k^x-1 kx−1≤m≤kx−1(因为 x x x位 k k k进制的最小数是一个 1 1 1后面跟 x − 1 x-1 x−1个 0 0 0,也就是 1000... = k x − 1 1000...=k^{x-1} 1000...=kx−1,最大数是 x x x个 1 1 1,也就是 111... = k x − 1 111...=k^x-1 111...=kx−1),所以有 log k ( m + 1 ) ≤ x ≤ log k m + 1 \log_k(m+1)\le x\le \log_k m+1 logk(m+1)≤x≤logkm+1由于 x x x是整数,所以 x ≤ ⌊ log k m ⌋ + 1 x\le \lfloor\log_k m\rfloor + 1 x≤⌊logkm⌋+1,而又因为 x x x是存在且唯一的,所以 x = ⌊ log k m ⌋ + 1 x= \lfloor\log_k m\rfloor + 1 x=⌊logkm⌋+1(这里的逻辑是,既然 x x x是满足 k x − 1 ≤ m ≤ k x − 1 k^{x-1}\le m\le k^x-1 kx−1≤m≤kx−1的唯一整数,又有这个不等式 log k ( m + 1 ) ≤ x ≤ log k m + 1 \log_k(m+1)\le x\le \log_k m+1 logk(m+1)≤x≤logkm+1,而 ⌊ log k m ⌋ + 1 \lfloor\log_k m\rfloor + 1 ⌊logkm⌋+1这个数代入 x x x是满足不等式的,所以 x x x就等于它),证毕。
接下来就可以求 c c c到底是多少了:
步骤1:证明 c ≤ ⌊ log k ( n − 1 ) ⌋ + 1 c\le \lfloor\log_k(n-1)\rfloor+1 c≤⌊logk(n−1)⌋+1。只需证明存在这样一种问法,每次问 k k k选 1 1 1的选择题,经过不等号右边这么多次,一定能问出甲心里想的数字即可。问题设计如下:
第一个问题,甲心中所想之数在 k k k进制下右起第 1 1 1位数是 0 , 1 , . . . , k − 1 0,1,...,k-1 0,1,...,k−1中的哪一个;
第二个问题,甲心中所想之数在 k k k进制下右起第 2 2 2位数是 0 , 1 , . . . , k − 1 0,1,...,k-1 0,1,...,k−1中的哪一个;
第三个问题,甲心中所想之数在 k k k进制下右起第 3 3 3位数是 0 , 1 , . . . , k − 1 0,1,...,k-1 0,1,...,k−1中的哪一个;
…
第 ⌊ log k ( n − 1 ) ⌋ + 1 \lfloor\log_k(n-1)\rfloor+1 ⌊logk(n−1)⌋+1个问题,甲心中所想之数在 k k k进制下右起第 ⌊ log k ( n − 1 ) ⌋ + 1 \lfloor\log_k(n-1)\rfloor+1 ⌊logk(n−1)⌋+1位数是 0 , 1 , . . . , k − 1 0,1,...,k-1 0,1,...,k−1中的哪一个;
一共 ⌊ log k ( n − 1 ) ⌋ + 1 \lfloor\log_k(n-1)\rfloor+1 ⌊logk(n−1)⌋+1个问题,每个问题都是 k k k选 1 1 1的选择题,并且答案一定存在选项里。很显然这么多问题全得到答案后,乙就知道了甲想的是什么数了。
步骤2:证明 c ≥ ⌊ log k ( n − 1 ) ⌋ + 1 c\ge \lfloor\log_k(n-1)\rfloor+1 c≥⌊logk(n−1)⌋+1。反证法。如果存在少于 ⌊ log k ( n − 1 ) ⌋ + 1 \lfloor\log_k(n-1)\rfloor+1 ⌊logk(n−1)⌋+1个数个问题,比如只问 l l l个问题,也能问出甲心中所想的数,那么我们按照每个问题,对 0 ∼ n − 1 0\sim n-1 0∼n−1中的数编码,对于每个数右起第一位数,我们让它等于第一个问题答案的选项编号(从 0 0 0开始)。例如,如果在第一个问题下,数字 i i i对应的正确选项是第 j j j项,那么我们就让 i i i的右起第一位数是 j j j。以此类推。这样, 0 ∼ n − 1 0\sim n-1 0∼n−1中的每个数都映射到了一个 l l l进制数。由于 ⌊ log k ( n − 1 ) ⌋ \lfloor\log_k(n-1)\rfloor ⌊logk(n−1)⌋位 l l l进制下最大的数仍然是小于 n n n的,根据抽屉原理,这就意味着这 n n n个数必然存在两个数,它们被映射到了同一个 l l l进制数。假设甲心里想的就是那两个数其中一个,那么乙在问完之后是无法区分是那两个数的哪一个的(因为这两个数在乙的问题下,每道题答案都一模一样,乙无法判断是哪个),这就矛盾了。所以乙如果只问少于 ⌊ log k ( n − 1 ) ⌋ + 1 \lfloor\log_k(n-1)\rfloor+1 ⌊logk(n−1)⌋+1个问题,是不能保证问出结果的。
综上,所以有: c = ⌊ log k ( n − 1 ) ⌋ + 1 c=\lfloor\log_k(n-1)\rfloor+1 c=⌊logk(n−1)⌋+1整个证明过程实际上就是在考虑这样一个问题:如何编码,才能保证解码的时候可以原样还原。
接下来我们就可以解决这道题了。我们把每个桶映射到 0 ∼ n − 1 0\sim n - 1 0∼n−1这些数上去。一只小猪去喝水,它最多可以喝 m i n u t e s T o T e s t m i n u t e s T o D i e \frac{minutesToTest} {minutesToDie} minutesToDieminutesToTest这么多次。令 m i n u t e s T o T e s t m i n u t e s T o D i e + 1 = k \frac{minutesToTest} {minutesToDie}+1=k minutesToDieminutesToTest+1=k,每次喝水的时候就相当于在给每个桶编码。具体可以这样做,先让第一只小猪去喝所有 k k k进制下个位数为 0 0 0的桶,第二只小猪去喝所有 k k k进制下十位数为 0 0 0的桶,这样以此类推。过了第一段时间,如果某只小猪死了,我们就知道了有毒的桶在 k k k进制下的某位数是 0 0 0了,如果某只没死,就让它接着喝本位数为 1 1 1的桶。一共最多需要喝 k − 1 k-1 k−1次(之所以是 k − 1 k-1 k−1,是因为如果小猪喝了 k − 1 k-1 k−1次还没死,说明本位数就是 k − 1 k-1 k−1,并不需要多喝一次才能判断),我们就能知道有毒水的每位数字是几了。所以所需要的小猪数量就是 n − 1 n-1 n−1在 k k k进制下的位数。
用上面的引理来理解就是,每只小猪相当于一个问题,而 m i n u t e s T o T e s t m i n u t e s T o D i e + 1 \frac{minutesToTest} {minutesToDie}+1 minutesToDieminutesToTest+1相当于选项数。至少要问多少问题才能得到答案。
代码如下:
public class Solution {
public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
int k = minutesToTest / minutesToDie + 1;
int n = buckets - 1;
int count = 0;
while (n != 0) {
n /= k;
count++;
}
return count;
}
}
问题还没有结束。上面只是证明了有那么多只小猪就能保证得到答案,但如何才能证明最少就需要这么多小猪呢?我们只是得到了上界,还没有完全证明出这个上界就等于下界。其实下界可以由那个引理得到。证明如下:
每只小猪喝水,一共喝了 k − 1 k-1 k−1轮,比如说,第一只小猪,假设每次喝的桶的集合是 A 1 , A 2 , . . . , A k − 1 A_1,A_2,...,A_{k-1} A1,A2,...,Ak−1这相当于在问一个 k k k选题,即有毒的水在 A 1 , A 2 , . . . , A k − 1 , C A_1,A_2,...,A_{k-1},C A1,A2,...,Ak−1,C(其中 C C C是其余集合并集的补集)之一吗?每只小猪事实上都相当于在问形如这样的选择题。由前面的引理,要问出结果,至少需要 ⌊ log k ( n − 1 ) ⌋ + 1 \lfloor\log_k(n-1)\rfloor+1 ⌊logk(n−1)⌋+1这么多次问题才行,这就是所需要的小猪的数量。证明完毕。