扔猫出窗户

151

假设你和一只猫在高楼里。这只猫可以从低楼层的窗户掉下来而不死,但如果从高楼层扔下去就会死亡。如何通过最少次数的尝试来确定猫能够生存的最大楼层数?

显然,如果你只有一只猫,那么你只能线性地搜索。先把猫从一楼扔下去。如果它活了下来,再从二楼扔下去。最终,在从第f层扔下后,猫将死亡。你会知道第f-1层是最大安全楼层。

但是如果你有不止一只猫呢?现在你可以尝试某种对数搜索。假设建筑有100层,你有两只相同的猫。如果你从50楼扔出第一只猫,它死了,那么你只需要线性地搜索50层。如果你选择更低的楼层作为第一次尝试,情况会更好。假设你每次尝试20层,第一个致命楼层是#50。那么,在它死于60楼之前,你的第一只猫将从20楼和40楼飞到并幸存下来。你只需要逐个检查41到49楼。这总共需要12次尝试,比二分法的50次要好得多。

一般来说,在有两只猫的n层建筑中,最佳策略及其最坏情况复杂度是什么?对于n层楼和m只猫呢?

假设所有的猫都是相同的:它们从给定窗户掉下来后都会幸存或死亡。此外,每次尝试都是相互独立的:如果一只猫在掉落时幸存了下来,它不会受到任何伤害。

这不是作业,虽然我可能曾经为学校的任务解决过这个问题。这只是一个今天突然想起的奇思妙想的问题,我不记得解决方案了。如果有人知道这个问题或解决算法的名称,可以额外加分。


125
我反对以所描述的方式使用猫。我们能否改为用狗? - Thilo
53
这件事情并不简单。已经有研究针对猫在高楼中意外掉落(而非被人抛掷)的情况进行了探究。研究发现,猫的生存范围存在一定的区间,在这个区间内它们会死亡,而在比这个范围更高的区间内它们则能够存活下来。这可能与猫在空中时身体的紧张状态有关。 - Andrew Shepherd
6
我曾在某处读到,15英尺或以上的高度,猫有更大的生存机会。如果我们正在讨论甩掉前女友或唠叨的妻子,这个问题可能更合适。 - Anthony Forloney
35
如果你有两只猫,你可以等几个月然后运用二分查找。或者再等几个月,进行“同时搜索”,请助手们同时从每层扔猫--现存的猫的数量是你可以从哪一层开始扔的最高楼层数,当然了。 - mjfgates
10
针对兔子这个话题,将“months”改为“weeks”。 - mjfgates
显示剩余31条评论
8个回答

93

16
作为一个热爱猫咪的人,我想指出这项研究是基于动物医院的抛掷事件报告。在此次调查中,没有其他猫咪受伤或受到影响。 - Thilo
19
这个回答恰好符合问题的程度。 - Mark Ransom
2
这只是展示了它不是一个简单的活着=1,死亡=0的结果,而是更多的活着=1.0,死亡=0.0以及中间所有的概率。它也是一条曲线,而不是一条直线,需要去探索。 - tadman
74
那份报告的问题在于存在选择偏差——没有人会带一只死猫去看兽医。 - Niki Yoshiuchi
1
可能是SO上唯一与编程无关的回答。 - nawfal
显示剩余3条评论

70

您可以轻松地为n层楼和m只猫的一般情况编写一个小DP(动态规划)。

主要公式:a[n][m] = min(max(a[k - 1][m - 1], a[n - k][m]) + 1) : for each k in 1..n,应该是不言自明的:

  • 如果第一只猫从第k层掉下并死亡,则我们现在有k-1层楼需要检查(所有在k以下的楼层)和m-1只猫(a[k-1][m-1])。
  • 如果猫幸存,则还剩下n-k层楼(所有在k以上的楼层)和m只猫。
  • 应选择两者中的最坏情况,因此使用max
  • +1来自于我们刚刚使用了一次尝试(无论猫是否幸存)。
  • 我们尝试每个可能的楼层以找到最佳结果,因此使用min(f(k)):for k in 1..n

它与Gaurav Saxena链接中(100,2)的Google结果相符。

int n = 100; // number of floors
int m = 20; // number of cats
int INFINITY = 1000000;

int[][] a = new int[n + 1][m + 1];
for (int i = 1; i <= n; ++i) {
    // no cats - no game
    a[i][0] = INFINITY;
}

for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= m; ++j) {
        // i floors, j cats
        a[i][j] = INFINITY;

        for (int k = 1; k <= i; ++k) {
            // try throw first cat from k-th floor
            int result = Math.max(a[k - 1][j - 1], a[i - k][j]) + 1;
            a[i][j] = Math.min(a[i][j], result);
        }
    }
}

System.out.println(a[n][m]);

如果您将最佳的k保存在另一个数组中,就可以轻松找到策略(如何抛出第一只猫)。

还有一种更快的解决方案,不涉及O(n^3)的计算,但我已经有点困了。

编辑
哦对了,我记得我以前在哪里看过这个问题


嗯,+ 1不需要放在min()里面吗?正如你自己所说,无论尝试是否成功,它仍然是一次尝试。 - j_random_hacker
@j_random_hacker 这会改变什么吗?将 +1 移到 min 外面。或者将它移动到 max 里面 :) - Nikita Rybak
@Nikita:非常抱歉,我不知怎么地误读了你写的内容——根据我的理解,你所说的完全正确!+1。 - j_random_hacker
请注意,这与 Google Code Jam 的“鸡蛋掉落问题”完全相同。下面的 O(n^3) 解决方案不够好,因为大型问题集使用 N = 2000000000。http://code.google.com/codejam/contest/dashboard?c=32003#s=p2 - ripper234
1
请查看这个新问题,了解O(n)算法。Google Code Jam的最佳答案是O(n),但我还不理解它。https://dev59.com/xG445IYBdhLWcg3wytEU - ripper234
我不理解在投掷那些猫时使用的策略,有人能解释一下吗?“最坏情况”是指如果扔出去,猫会死在最后一层吗? - Pavel

10
假设你和一只猫在高楼里。这只猫可以从低楼层的窗户掉下来而不会死,但如果从高楼层扔下去就会死。如何用最少的尝试次数找出这只猫能够生存的最长掉落距离?
解决这个问题的最佳策略是,利用物理定律调查并确定你的假设在第一时间内是否成立的概率。
如果你这样做了,你会发现猫的生存机会实际上随着到地面的距离越高而增加。当然,前提是你要把它从越来越高的建筑物上扔下去,比如双峰塔,而不是越来越高的山上,比如珠穆朗玛峰。
编辑: 实际上,你会看到一个未完成的骆驼分布。 首先,猫死亡的概率很低(非常低的高度),然后变得更高(低高度),然后再次变低(更高高度),然后再次变高(非常高的高度)。
作为海拔高度函数的猫死亡概率图看起来像这样: (在3处结束,因为骆驼分布未完成)

alt text

更新: 一只猫的终端速度为100公里/小时(60英里/小时)[= 27.7米/秒 = 25.4码/秒]。 人类的终端速度为210公里/小时(130英里/小时)[= 75米/秒 = 68.58码/秒]。
终端速度来源: http://en.wikipedia.org/wiki/Cat_righting_reflex 制作人员: 谷歌
需要稍后验证: http://en.wikipedia.org/wiki/Terminal_velocity http://www.grc.nasa.gov/WWW/K-12/airplane/termv.html

2
这个正确吗?一旦达到极限速度,机会就不会改变 - 而我印象中猫可以在极限速度下存活下来。 - ZoFreX
4
他们可以,只要不超过终端速度,这时候是最致命的。但如果从比如十万英里高处扔下猫,猫更有可能在死亡后因真空而燃烧,而不是坠落并存活。 - David Thornley
1
那个图表里是兔耳朵吗? - ninjalj
1
@ZoFreX:角动量。猫总是能落在脚底下,这是由于猫的身体设计和转身技巧所产生的角动量。但这仍然意味着它需要时间来转身。有更多的时间(==>高度越高),猫就越有可能落在脚底下(==>生存机会大大增加,而不是落在头上)。但你说得对,在达到终端速度后,概率保持不变。我想一个猫从终端速度坠落很有可能幸存下来,至少我的猫从浴室窗户跳出去(约20米),毫发无损。 - Stefan Steiger

8
我最初在Steven Skiena的《算法设计手册》(练习8.15)中看到了这个问题。它跟随一个动态规划章节,但是您不需要了解动态规划就可以证明策略精确边界。首先是问题陈述,然后是下面的解决方案。
鸡蛋从足够高的楼层掉落会摔碎。给定一个n层楼房,必须有一个楼层f,使得从f层掉落的鸡蛋会摔碎,但从f-1层掉落的鸡蛋不会摔碎。(如果鸡蛋从任何一层摔碎,我们将说f=1。如果从任何一层幸存下来,我们将说f=n+1。)
您要寻找关键楼层f。您唯一能进行的操作是从某个楼层放下一个鸡蛋并观察发生了什么。您从k个鸡蛋开始,并希望尽可能少地放鸡蛋。摔碎的鸡蛋不能重新使用(完好的鸡蛋可以)。E(k,n)是始终足够的最小鸡蛋投放次数。
1. 证明E(1,n)=n。 2. 证明E(k,n)=Θ(n**(1/k))。 3. 找到E(k,n)的递归。动态规划查找E(k,n)的运行时间是多少?
只有一个鸡蛋时,从第一层开始依次从每个楼层放下鸡蛋,将在最多n步操作内找到关键楼层。
没有更快的算法。在任何算法的任何时候,让g是从中看到鸡蛋不破碎的最高楼层。算法必须在任何更高的楼层h>g+1之前测试g+1层,否则,如果鸡蛋从h楼层摔碎,则它无法区分f=g+1和f=h。
2个鸡蛋时,让我们考虑k=2鸡蛋情况,当n=r**2是一个完全平方数。这是需要O(根号n)时间的策略。首先,以r层的增量放下第一个鸡蛋。当第一个鸡蛋摔碎时,在ar层时,我们知道关键楼层f必须是(a-1)r
当n不是完全平方数时,取r=ceil(sqrt(n))∈Θ(sqrt(n))。该算法仍然是Θ(sqrt(n))。

证明任何算法至少需要sqrt(n)时间。 假设存在更快的算法。考虑它第一次扔鸡蛋时所在的楼层序列(只要不碎)。由于扔的楼层数小于sqrt(n),必须有至少n/sqrt(n)的区间是sqrt(n)。当f在这个区间内时,算法将不得不用第二个鸡蛋来破解,这必须通过记住1-鸡蛋的情况逐层进行。矛盾。

k个鸡蛋

对于k个鸡蛋,可以轻松地将2个鸡蛋的算法扩展到k个鸡蛋。每次以恒定间隔丢出鸡蛋,应该将其视为n的k次方根的幂。例如,对于n = 1000和k = 3,使用第一个鸡蛋搜索100层,第二个鸡蛋搜索10层,最后一个鸡蛋搜索1层。

同样,我们可以通过从k = 2的证明归纳出,没有算法比Θ(n ** (1/k))更快。

精确解决方案

通过优化第一个鸡蛋(g楼)的投放位置,我们推导出递归,假设我们知道更小参数的最优解。如果鸡蛋碎了,我们需要用k-1个鸡蛋探索下面的g-1层。如果鸡蛋没有碎,我们需要用k个鸡蛋探索上面的n-g层。恶魔为我们选择最坏情况。因此,对于k>1,递归如下。

E(k,n) = min(max(E(k,n-g), E(k-1,g))) minimised over g in 1..n

如果我有k个鸡蛋,为什么最坏情况下的运行时间不是O(k*n**(1/k))呢?因为在最坏情况下,我必须恰好通过n**(1/k) k次。 - Rakete1111

2
这是否假定您正在使用“同一只猫”?
你可以用数学方法来解决,但是数学的好处就在于...在正确的假设下,0可以等于1(对于大的0值)。
从实际角度来看,你可以获得“相似的猫”,但你无法获得“同一只猫”。
你可以尝试通过经验确定答案,但我认为会有足够的统计差异,使答案在统计意义上没有意义。
你可以尝试使用“同一只猫”,但这不起作用,因为第一次掉落后,它就不再是同一只猫了。(类似于永远不能两次踏进同一条河流)
或者,你可以汇总猫的健康状况,以极近的时间间隔进行采样,并找到猫“大多活着”的高度(与《公主新娘》中的“大多死亡”相反)。猫平均能存活(直到最后一个时间间隔)。
我想我已经偏离了最初的意图,但如果你选择经验路线,我建议从尽可能高的高度开始,并在高度降低时继续扔猫,直到它们在统计上存活。然后再对存活的猫进行重新测试,以确保结果正确。

0
O(m*(n^(1/m))) algorithm.

Let 'x' be the maximum number of attempts needed.  

m = 1 => linear => x=n

m = 2:  
Let the floors be split into 'k' partitions. The first cat is thrown at the end of each partition (max 'k' times). 
When it dies, the second cat is used to go up from the beginning of this partition.   
x = k + n/k.   
Minimize x by diff wrt k and setting = 0, to get k = n^(1/2) and x = 2 * n^(1/2).

m = 3:  
x = k + 2*(y^(1/2)), where y = n/k  
diff wrt x and set = 0, to get k = n^(1/3) and x = 3 * n^(1/3)

for general m:  
x = m * n^(1/m). 

0

我采用了稍微不同的方法来解决问题。

我首先通过以下方法计算使用 x 只猫和 y 次猜测可以覆盖的最大楼层数。

从第一层开始,不断增加猜测次数,同时跟踪检查过的楼层、它们被检查的猜测次数以及每个楼层剩余的猫的数量。
重复此过程最多 y 次。

这段代码非常低效,但对于少量的猫/楼层仍然有用。

Python 代码:

def next_step(x, guess):
  next_x = []
  for y in x:
    if y[0] == guess:
      if y[1] != 1:
        next_x.append((guess+1, y[1] - 1))
    next_x.append(y)
    if y[0] == guess:
      next_x.append((guess+1, y[1]))
  return next_x

x = [(1, TOTAL_NUM_CATS)]
current_floor = 1
while len(x) <= TOTAL_NUM_FLOORS:
  x = next_step(x, current_floor)
  current_floor += 1
  print len(x)

对于两只猫,可以在x次猜测中确定的最大楼层数为:
1、3、6、10、15、21、28...

对于三只猫:
1、3、7、14、25、41、63...

对于四只猫:
1、3、7、15、30、56、98...

经过广泛的研究(主要涉及将数字序列输入OEIS),我注意到的最大楼层遵循组合分段模式。

对于两只猫:
n < 2:2^n - 1
n >= 2:C(n, 1) + C(n, 2)

对于三只猫:
n < 3:2^n - 1
n >= 3:C(n, 1) + C(n, 2) + C(n, 3)

对于4只猫:
n < 4:2^n - 1
n >= 4:C(n, 1) + C(n, 2) + C(n, 3) + C(n, 4)

从这里开始,我采取了简单的方法,通过递增n直到达到所需楼层数。

Python代码:

def find_smallest(floors, eggs):
  maximum_floors = 0
  n = 0
  while maximum_floors < floors:
    maximum_floors = 0
    n += 1
    if n < eggs:
      maximum_floors = 2**n - 1
    else:
      count = 0
      for x in xrange(1, eggs+1):
        maximum_floors += combination(n, x)
  print n

这给出了 (100, 2) = 14 的正确解答。
对于任何希望检查不太平凡的内容,它给出了 (1 000 000, 5) = 43。

这运行时间为 O(n),其中 n 是问题的答案(猫越多越好)。
但是我相信某个数学水平更高的人可以简化分段公式以在 O(1) 中计算。


-1
所有这些关于猫的疯狂谈论...其实只是一个猜数字问题,需要最少的猜测次数(猫的数量)。也不应该人为地(并且不正确地)将无限定义为解决方案的一部分。变量应该被命名为上限或最大尝试次数等。 然而,问题定义(猫的事情)存在一些严重的问题,人们对动物虐待的潜在反应以及现实生活中提出的许多问题的各个方面,例如空气阻力、重力加速度和其他类似的问题参数。因此,也许应该用完全不同的方式提出这个问题。

说实话,这可能是一个伪装的现实问题。假设你有一个自动化测试,在版本1234失败了,但在版本42中工作正常。在1234版本中猫死了,但在42版本中还活着。是哪个版本导致了它的死亡?如果从42更新到43很快很容易,但检出和重建一个新版本很困难,那么这就开始像是猫问题了。 - mcdowella

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接