2021.11.25LeetCode每日一题——可怜的小猪

目录

可怜的小猪

描述

示例 1

示例 2

示例 3

提示

方法一:动态规划

方法二:数学


可怜的小猪

描述

有 buckets 桶液体,其中 正好 有一桶含有毒药,其余装的都是水。它们从外观看起来都一样。

为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断。

不幸的是,你只有 minutesToTest 分钟时间来确定哪桶液体是有毒的。

喂猪的规则如下:

  • 选择若干活猪进行喂养
  • 可以允许小猪同时饮用任意数量的桶中的水,并且该过程不需要时间。
  • 小猪喝完水后,必须有 minutesToDie 分钟的冷却时间。在这段时间里,你只能观察,而不允许继续喂猪。
  • 过了 minutesToDie 分钟后,所有喝到毒药的猪都会死去,其他所有猪都会活下来。
  • 重复这一过程,直到时间用完。

给你桶的数目 buckets ,minutesToDie 和 minutesToTest ,返回在规定时间内判断哪个桶有毒所需的 最小 猪数。

示例 1

输入:buckets = 1000, minutesToDie = 15, minutesToTest = 60
输出:5

示例 2

输入:buckets = 4, minutesToDie = 15, minutesToTest = 15
输出:2

示例 3

输入:buckets = 4, minutesToDie = 15, minutesToTest = 30
输出:2

提示

  • 1 <= buckets <= 1000
  • 1 <= minutesToDie <= minutesToTest <= 100

方法一:动态规划

根据 minutesToDie 和 minutesToTest,可以计算得到最大测试轮数:

iterations=\lfloor \frac{minutesToTest}{minutesToDie}\rfloor

问题的等价描述是:在 buckets 桶液体中恰好有一桶有毒,至少需要多少只小猪才能在 iterations 轮测试中确定有毒的是哪一桶。

这个问题很难直接计算,可以从另一个角度考虑:用 f(i,j) 表示 i 只小猪测试 j 轮最多可以在多少桶液体中确定有毒的是哪一桶。在确定最大测试轮数为 iterations 的情况下,需要计算使得 f(i,iterations)≥buckets 成立的最小的 i。

如果测试轮数是 0 或者小猪数量是 0,则不能进行测试,如果有超过 1 桶液体则无法确定有毒的是哪一桶,此时最多只能在 1 桶液体中确定有毒的是这唯一的一桶。因此对任意 i 都有 f(i,0)=1,对任意 j 都有 f(0,j)=1。

当 i 和 j 都大于 0 时,可以通过递推的方法计算 f(i,j) 的值。

当剩下 i 只小猪和 j 轮测试时,如果在一轮测试之后有 k 只小猪存活,则剩下 k 只小猪和 j−1 轮测试。由于 k 只小猪和 j−1 轮测试可以判断的最大桶数是 f(k,j−1),i 只小猪中有 k 只小猪存活的组合数是 C(i,k),因此剩下 k 只小猪和 j−1 轮测试时,可以判断的最大桶数是 f(k,j-1)\times C(i,k)。由于 0≤k≤i,因此 f(i,j) 的计算如下:

f(i,j)=\sum_{k=0}^i f(k,j-1)\times C(i,k)

其中 C(i,k) 表示从 i 个不同元素中取出 k 个元素的组合,i 和 k 满足 0≤k≤i。特别地,C(0,0)=1。

当 i≥1 时,组合数的计算如下:

  • C(i, 0) = C(i, i) = 1;
  • 当 0 < j < i 时,C(i, j) = C(i - 1, j - 1) + C(i - 1, j)

当小猪数量等于 buckets−1 时,可以将 buckets−1 桶液体分别给每只小猪喝一桶,剩下一桶液体没有小猪喝,如果有一只小猪死了则这只小猪喝的一桶液体有毒,如果小猪都存活则剩下一桶没有小猪喝的液体有毒,此时可以在一轮内确定有毒的是哪一桶。因此最多需要的小猪数量是 buckets−1,i 的取值范围是 0≤i<buckets。

由于最大测试轮数 iterations 可以根据 minutesToDie 和 minutesToTest 计算得到,因此最大测试轮数可以看成已知,任何情况下的测试轮数都不能超过最大测试轮数,j 的取值范围是 0≤j≤iterations。

为了计算 f 的值,一种做法是预先计算组合数,然后计算 f 的值,但是题目的数据规模是 buckets≤1000,如果预先计算所有 0≤j≤i≤buckets 的组合数则可能导致结果溢出。为了避免溢出,可以在计算 f 的同时计算组合数。

具体做法是,对于 1≤i<buckets 的每个 i,首先计算满足 0≤j≤i 的所有组合数 C(i, j),然后计算所有满足 1≤j≤iterations 的 f(i, j)。计算过程中,找到使得 f(i,iterations)≥buckets 成立的最小的 i 并返回,该返回值即为至少需要的小猪数量。

特别地,当 buckets=1 时,不需要进行测试即可知道这唯一的一桶液体一定有毒,此时不需要任何小猪,返回 0。

下面用一个例子说明 f 的计算。假设有 3 只小猪和 4 轮测试,f(3, 4) = 125,即最多可以在 125 桶液体中确定有毒的是哪一桶。

将 125 桶液体排成 5×5×5 的正方体,每桶液体都可以用唯一的坐标 (x, y, z) 表示,其中 x、y、z 都是整数且取值范围都是 [0, 4]。

排成棱长为 55 的正方体是因为 44 轮测试对应 55 种状态,前 44 种状态分别是在 44 轮当中的某一轮喝,最后一种状态是不喝。

在第 i 轮测试中,第 0 只小猪喝 x = i 平面内的全部液体,第 1 只小猪喝 y = i 平面内的全部液体,第 2 只小猪喝 z = i 平面内的全部液体,其中 0≤i<4。

考虑第 0 轮之后存活的小猪数量。

  • 第 0 轮之后没有小猪存活。有毒的液体位于 (0, 0, 0),有毒的液体的可能位置有 f(0,3)×C(3,0)=1 个。

  • 第 0 轮之后有 1 只小猪存活。假设存活的是第 0 只小猪,则有毒的液体的坐标 (x, y, z)满足x!=0、y = 0 且 z = 0,此时 x 有 4 种取值,因此有毒的液体的可能位置有 f(1, 3) = 4个。

    • 由于有 1 只小猪存活的组合数是 C(3, 1) = 3,因此有毒的液体的所有可能位置有 f(1,3)×C(3,1)=12 个

  • 第 0 轮之后有 2 只小猪存活。假设存活的是第 0 只小猪和第 1 只小猪,则有毒的液体的坐标 (x, y, z) 满足 x != 0、y != 0 且 z = 0,此时 x 和 y 各有 4 种取值,因此有毒的液体的可能位置有 f(2, 3) = 16个。

    • 由于有 2 只小猪存活的组合数是 C(3, 2) = 3,因此有毒的液体的所有可能位置有 f(2,3)×C(3,2)=48 个。

  • 第 0 轮之后有 3 只小猪存活。有毒的液体的坐标 (x, y, z) 满足 x != 0、y != 0 且 z != 0,此时 x、y 和 z 各有 4 种取值,因此有毒的液体的可能位置有 f(3,3)×C(3,3)=64

因此 f(3, 4) = 1 + 12 + 48 + 64 = 125。

class Solution {
    public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
        if (buckets == 1) {
            return 0;
        }
        int[][] combinations = new int[buckets + 1][buckets + 1];
        combinations[0][0] = 1;
        int iterations = minutesToTest / minutesToDie;
        int[][] f = new int[buckets][iterations + 1];
        for (int i = 0; i < buckets; i++) {
            f[i][0] = 1;
        }
        for (int j = 0; j <= iterations; j++) {
            f[0][j] = 1;
        }
        for (int i = 1; i < buckets; i++) {
            combinations[i][0] = 1;
            combinations[i][i] = 1;
            for (int j = 1; j < i; j++) {
                combinations[i][j] = combinations[i - 1][j - 1] + combinations[i - 1][j];
            }
            for (int j = 1; j <= iterations; j++) {
                for (int k = 0; k <= i; k++) {
                    f[i][j] += f[k][j - 1] * combinations[i][i - k];
                }
            }
            if (f[i][iterations] >= buckets) {
                return i;
            }
        }
        return 0;
    }
}

方法二:数学

方法一的动态规划需要计算 f 的每个状态,也可以直接推导 f 的每个元素的表达式。

当最大测试轮数是 1 时,i 只小猪可以判断的最大桶数是 f(i, 1)。根据递推关系,有:

f(i,1)=\sum_{k=0}^if(k,0)\times C(i,k)=\sum_{k=0}^iC(i,k)=2^i

当最大测试轮数是 2 时,i 只小猪可以判断的最大桶数是 f(i, 2)。根据递推关系,有:

f(i,2)=\sum_{k=0}^if(k,1)\times C(i,k)=\sum_{k=0}^i2^k\times C(i,k)=3^i

推广到一般情况,当最大测试轮数是 j 时,i 只小猪可以判断的最大桶数是 f(i, j)。根据递推关系,有:

f(i,j)=\sum_{k=0}^if(k,j-1)\times C(i,k)=\sum_{k=0}^ij^k\times C(i,k)=(j+1)^i

上述结论可以通过二项式定理证明。

当最大测试轮数为 iterations 时,需要找到使得 (iterations+1)^i\ge buckets 成立的最小的 i,即为至少需要的小猪数量。令 states=iterations+1,则至少需要的小猪数量是 \lfloor \log_{states}buckets\rfloor

class Solution {
    public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
        int states = minutesToTest / minutesToDie + 1;
        int pigs = (int) Math.ceil(Math.log(buckets) / Math.log(states));
        return pigs;
    }
}

おすすめ

転載: blog.csdn.net/weixin_39478524/article/details/121536538