LeetCode Entry

458. Poor Pigs

29.10.2023 hard 2023 kotlin

Minimum pigs to find a poison in buckets in k rounds

458. Poor Pigs hard blog post substack

image.png image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/385

Problem TLDR

Minimum pigs to find a poison in buckets in k rounds

Intuition

The first idea is, with the number of pigs increasing, the possibility to successfully test in the given time grows from impossible to possible. This gives us the idea to use a Binary Search.

However, now we must solve another problem: given the pigs and rounds, how many buckets we can test?

One simple insight is: let’s assign unique pigs pattern to each of the bucket.

We can brute-force this problem and use memorization. Consider each pig, it can avoid participation, and can participate in all the rounds:

    val dp = mutableMapOf<Int, Int>()
    fun numPatterns(pigs: Int): Int {
      fun dfs(curr: Int): Int = if (curr == 0) 1 else dp.getOrPut(curr) {
        val take = dfs(curr - 1)
        if (take >= buckets) take else take + take * minutesToTest / minutesToDie
      }
      return dfs(pigs)
    }

This number grows quickly, so we trim it by the buckets number maximum.

Another way to solve this, is to observe those unique patterns.

If we have just one round, 3 pigs, there are total 8 patterns:

    // pigs = 3 rounds = 1
    //   123
    // 0 000 no pig drinks
    // 1 001 pig #3 drinks
    // 2 010 pig #2 drinks
    // 3 011 pigs #2 and #3 drinks
    // 4 100 pig #1 drinks
    // 5 101 pigs #1 and #3 drinks
    // 6 110 pigs #1 and #2 drinks
    // 7 111 all pig drinks

or,

    //
    // 0 1 2 3 4 5 6 7
    //         1 1 1 1 <-- pig #1
    //     2 2     2 2 <-- pig #2
    //   3   3   3   3 <-- pig #3

Now, if one bucket is a poison, we immediately know which one of those 8 buckets by its unique pattern.

Ok, so 3 pigs for 1 round enables to test 8 or 2^3 buckets. It is evident, that for 1 round the number of possible buckets is 2^pigs

How this changes with the growth of rounds? Let’s observe another example, 3 pigs, 2 rounds:

    //
    // 3 pigs, 2 rounds:
    //   123
    // 0 000
    // 1 001
    // 2 002
    // 3 010
    // 4 011
    // 5 012
    // 6 020
    // 7 021
    // 8 022
    // 9 100
    //10 101
    //11 102
    //12 110
    //13 111
    //14 112
    //15 120
    //16 121
    //17 122
    //18 200
    //19 201
    //20 202
    //21 210
    //22 211
    //23 212
    //24 220
    //25 221
    //26 222

or,

    // 0 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
    // - round 1 -
    //                   1  1  1  1  1  1  1  1  1
    //       2 2 2                2  2  2                    2  2  2
    //   3     3     3      3        3        3        3        3        3
    // - round 2 -
    //                                              1  1  1  1  1  1  1  1  1
    //             2 2 2                   2  2  2                    2  2  2
    //     3     3     3       3         3       3        3        3        3

Each pigs pattern consists of the 3 pigs, and each pig defined as round 1 or round 2.

This results in 27 unique patterns, or buckets being able to test, or 3^3. Let’s extrapolate this formula: buckets = (1 + rounds) ^ pigs

Approach

For better Binary Search, use:

  • inclusive lo and hi
  • check the last condition lo == hi
  • always move lo or hi
  • always compute the result independently min = min(min, mid)

Complexity

  • Time complexity: \(O(log^2(buckets))\), one log for the Binary Search, another is for canTest function

  • Space complexity: \(O(1)\)

Code

DFS + memo

  fun poorPigs(buckets: Int, minutesToDie: Int, minutesToTest: Int): Int {
    val dp = mutableMapOf<Int, Int>()
    fun numPatterns(pigs: Int): Int {
      fun dfs(curr: Int): Int = if (curr == 0) 1 else dp.getOrPut(curr) {
        val take = dfs(curr - 1)
        if (take >= buckets) take else take + take * minutesToTest / minutesToDie
      }
      return dfs(pigs)
    }
    var lo = 0
    var hi = buckets
    var min = hi
    while (lo <= hi) {
      val mid = lo + (hi - lo) / 2
      if (numPatterns(mid) >= buckets) {
        min = min(min, mid)
        hi = mid - 1
      } else lo = mid + 1
    }
    return min
  }

The more clever version:


    fun poorPigs(buckets: Int, minutesToDie: Int, minutesToTest: Int): Int {
      fun canTest(pigs: Int): Boolean {
        var p = 0
        var bs = 1
        while (p++ < pigs) {
          bs *= 1 + minutesToTest / minutesToDie
          if (bs >= buckets) return true
        }
        return bs >= buckets
      }
      var lo = 0
      var hi = buckets
      var min = hi
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (canTest(mid)) {
          min = min(min, mid)
          hi = mid - 1
        } else lo = mid + 1
      }
      return min
    }