鸡蛋楼层问题

探讨使用最少尝试次数确定N层楼中最安全落蛋楼层的算法,涉及动态规划及优化技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、N层楼2个鸡蛋问题

题目描述:有两个软硬程度一样但未知的鸡蛋,它们有可能都在一楼就摔碎,也可能从一百层楼摔下来没事。有座100层的建筑,要你用这个鸡蛋通过最少的次数确定哪一层是鸡蛋可以安全落下的最高位置。可以摔碎两个鸡蛋。

 

分析:看到这个题目,最保险的方法就是从第一层往上一层一层试验,但这样就需要一个鸡蛋就可以了。我们现在有两个鸡蛋,完全可以用更快的方法。

进一步可以试验的方法是二分查找,例如:第一个鸡蛋从50层扔下,如果碎了,第二个鸡蛋就从1到49一层一层试验;如果没碎,将区间缩小为50~100,将第一个鸡蛋从75层扔下,如果碎了,第二个鸡蛋从51到74一层一层试验...。但是这个方法有一个缺点那就是考虑这样一种情况:正好49就是要求的最高安全楼层,这时你将第一个鸡蛋从50层摔下去,碎了。第二个鸡蛋就要从1到49层逐层检验,效果很差。

 

我们换个角度思考,从楼层的角度出发。尝试用动态规划的方法,找到构成这个最优问题的最优子问题。用dp[N]表示当楼层一共有N层时,找到安全位置的最小次数。当楼层为N时,假设我们一开始将第1个鸡蛋从第i层开始扔(显然1<=i<=N),如果第一个鸡蛋碎了,那么第二个鸡蛋必须从第1层到i-1层逐层检验,这样的话最坏情况下要测试1+i-1=i次。假设第一个鸡蛋没碎,那么问题就转化为找出楼层从i~N的安全位置,F(N)问题转化为F(N-i)的问题,此时最坏情况下要测试1+F(N-i)次。

 

所以状态转移方程为:F(N) = min(max(1, 1+F(N-1)), max(2, 1+F(N-2), ... ,

max(i, 1+F(N-i)),..., max(N-1, 1+F(1))));

其中F(1)=1

 

例题:

某幢大楼有100层。你手里有两颗一模一样的玻璃珠。当你拿着玻璃珠在某一层往下扔的时候,一定会有两个结果,玻璃珠碎了或者没碎。这幢大楼有个临界楼层。低于它的楼层,往下扔玻璃珠,玻璃珠不会碎,等于或高于它的楼层,扔下玻璃珠,玻璃珠一定会碎。玻璃珠碎了就不能再扔。现在让你设计一种方式,使得在该方式下,最坏的情况扔的次数比其他任何方式最坏的次数都少。也就是设计一种最有效的方式。

 

代码:

#include<iostream>
#include<vector>
#include<cmath>
 
usingnamespace std;
 
intcalFloor(int n) {
    //总楼层为n时
    vector<int> dp(n + 1, 0);
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = i;
        for (int j = 1; j < i; j++) {
            int tmp = max(j, 1 + dp[i - j]);
            if (tmp < dp[i]) {
                dp[i] = tmp;
            }
        }
    }
    return dp[n];
}
 
int main() {
    int n; //要求的总楼层
    while (cin >> n) {
        cout << calFloor(n) <<endl;
    }
    return 0;
}

实际上,两个鸡蛋的情况用数学方程就可以解决,前提是你知道该怎么扔:

先假设最少判断次数为x,则第一个鸡蛋第一次从第x层扔(不管碎没碎,还有x-1次尝试机会)。如果碎了,则第二个鸡蛋在1~x-1层中线性搜索,最多x-1次;如果没碎,则第一个鸡蛋第二次从x+(x-1)层扔(现在还剩x-2次尝试机会)。如果这次碎了,则第二个鸡蛋在x+1~x+(x-1)-1层中线性搜索,最多x-2次;如果还没碎第一个鸡蛋再从x+(x-1)+(x-2)层扔,依此类推。x次尝试所能确定的最高楼层数为x+(x-1)+(x-2)+...+1=x(x+1)/2。

  比如100层的楼,只要让x(x+1)/2>=100,得x>=14,最少判断14次。具体地说,100层的楼,第一次从14层开始扔。碎了好说,从第1层开始试。不碎的话还有13次机会,再从14+13=27层开始扔。依此类推,各次尝试的楼层依次为:

  14,  27 = 14 + 13,  39 = 27 + 12 , ...  , 99 = 95 + 4,  100

 

二、N层楼M个鸡蛋问题

分析:推广到n层楼m个鸡蛋。假设f(n, m)表示n层楼、m个鸡蛋时找到最高楼层的最少尝试次数。当第一个鸡蛋从第i层扔下,如果碎了,还剩m-1个鸡蛋,问题转化为f(i-1, m-1)。如果没碎,上面还有n-i层,问题转化为f(n-i, m)。

因此,状态转移方程为:

f(n, m) = min(max(1+f(0,m-1), 1+f(n-1, m)), max(1+f(1,m-1), 1+f(n-2, m)), ..., max(1+f(i-1, m-1),1+f(n-i, m)), ..., max(1+f(n-2, m-1), 1+f(1, m)), max(1+f(n-1, m-1), 1+f(0, m)));

初始化F(1,m) = 1 表示当只有一层楼m个鸡蛋时,只需要测试1次。 

对于f(i, 0)和f(0, i)都初始化为0

 

例题:球掉落问题

题目描述:

有一幢N(N>0)层的楼,当从高于某一楼层往下扔玻璃球时,玻璃球会被摔坏,反之,玻璃球保持完好。现在用m(m>0)颗完全一样的玻璃球,每次从某一楼层往下扔一颗球,来找到这个刚好能使玻璃球摔坏的临界楼层。规定扔碎的玻璃球不可用于下一次试验,求一定能确定这个临界楼层的最少试验次数。主函数已经完成,请完成calcThrowNumber(int, int)函数

#include<stdio.h>
 
intcalcThrowNumber(int numOfFloors, int numOfBalls) {
    //implement your code here
}
 
int main() {
    int numOfFloors, numOfBalls;
    while (scanf("%d%d",&numOfFloors, &numOfBalls) != EOF) {
        printf("%d\n",calcThrowNumber(numOfFloors, numOfBalls));
    }
}

输入:

输入数据为一行,包括两个复数N和m(0<N<=10000,0<m<=10000)

输出:

对于每组测试用例,要求输出最少试验次数。

样例输入:

100 3

样例输出:

9

 

代码:

#include<stdio.h>
#include<algorithm>
#include<vector>
 
usingnamespace std;
 
intcalcThrowNumber(int numOfFloors, int numOfBalls) {
    if (numOfBalls < 1 || numOfFloors <1) {
        return 0;
    }
    //dp[egg][num]形式
    vector<vector<int> >dp(numOfBalls + 1, vector<int>(numOfFloors + 1, 0));
 
    //初始化
    for (int i = 1; i <= numOfBalls; i++) {
        for (int j = 1; j <= numOfFloors;j++) {
            dp[i][j] = j;   //对于第j层,最坏的情况就是从1到j层逐层验证,就是j次。
        }
    }
 
    for (int i = 2; i <= numOfBalls; i++){  //鸡蛋个数遍历
        for (int j = 1; j <= numOfFloors;j++) {  //总楼层遍历
            for (int k = 1; k < j; k++){  //对第一次扔的楼层进行遍历 1~j
                dp[i][j] = min(dp[i][j], 1 +max(dp[i - 1][k - 1], dp[i][j - k]));
            }
        }
    }
    return dp[numOfBalls][numOfFloors];
 
}
 
int main() {
    int numOfFloors, numOfBalls;
    while (scanf("%d%d",&numOfFloors, &numOfBalls) != EOF) {
        printf("%d\n",calcThrowNumber(numOfFloors, numOfBalls));
    }
    return 0;
}

 

注意:这种解法的时间复杂度太大,并没有AC,只过了88%的测试用例。


优化版本的代码

#include <bits/stdc++.h>
using namespace std;

vector<vector<int>> dp(10000 + 1, vector<int>(10000 + 1, -1));

void init()
{
    for(int i = 1; i < dp.size(); i++)
    {
        dp[i][1] = i;
    }

    for(int i = 1; i < dp[0].size(); i++)
    {
        dp[1][i] = 1;
    }
}

int calcThrowNumber(int numOfFloors, int numOfBalls) {
    if(dp[numOfFloors][numOfBalls] != -1)
        return dp[numOfFloors][numOfBalls];

    for(int floor = 2; floor <= numOfFloors; floor++)
    {
        for(int balls = 2; balls <= numOfBalls; balls++)
        {
            int step = min(
                    calcThrowNumber(floor - 1, balls - 1), 
                    calcThrowNumber(floor - 1, balls)
                    ); 

            for(int n = 2;  n < floor; n++)
            {
                step = min(
                        max(
                            calcThrowNumber(n - 1, balls - 1), 
                            calcThrowNumber(floor - n, balls)
                            ), 
                        step
                        );
            }

            dp[floor][balls] = step + 1;
        }
    }
    return dp[numOfFloors][numOfBalls];
}

int main() {
    init();
    int numOfFloors, numOfBalls;

    while (scanf("%d %d", &numOfFloors, &numOfBalls) != EOF) {
        printf("%d\n",calcThrowNumber(numOfFloors, numOfBalls));
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值