算法题:鸡蛋掉落(leetcode 887题)

16 篇文章 0 订阅

题目:

鸡蛋掉落

你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N  共有 N 层楼的建筑。

每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。

你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。

每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。

你的目标是确切地知道 F 的值是多少。

无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?

示例 1:

输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。

示例 2:

输入:K = 2, N = 6
输出:3

示例 3:

输入:K = 3, N = 14
输出:4

这个题是对动态规划的运用,关于动态规划,自己写了一点简单的理解:

https://www.cnblogs.com/JLY001/p/7249093.html

解法1:我们首先按照正常思路,来分析动态规划的状态转移方程:
1. 首先给出状态转移方程:

dp[k][n]代表k个鸡蛋,n层楼的找到F层的最少操作次数。

dp[k][n] = min(1 + max(dp[k-1][i-1] , dp[k][n-i]))  其中:(i in [1, n])   

动态规划状态方程分析:
(1)先考虑原问题:100层楼,2个鸡蛋的情况。 
(2)设dp[n]表示从第n层丢下鸡蛋,没有摔碎的最少操作次数。先给出dp方程式为:

dp[n] = min(1 + max(i-1, dp[n-i]))   其中:(i in [1, n])   //dp[n]通过遍历之前的值得到。
   解释: 
   假设:第一个鸡蛋从第i层扔下来。那么有两个情况。 
   A: 鸡蛋碎了,第二个鸡蛋只能从第 1 层,依次向上试,共有操作i - 1次。 
   B: 鸡蛋没碎,两个鸡蛋还都健在,楼上还有 n - i层,此时的问题,就转换成本问题的,子问题了dp[n-i]。 
   C: 所以 max(i-1, dp[n-i]) 表示两种情况最差的一种,也就是操作次数最多的哪一种。 
   D: 1 + max(i-1, dp[n-i]),前面那个 1就是本次操作。 
   E: 最后,很显然,我们不知道 最优的 i 层 在哪里对吧?所以通过 遍历从1到n层中 选出来。也就是上面的状态转换方程了。 
(3)现在看看本题 K个鸡蛋,N层楼,的情况。 
   A: 设 dp[k] [n]为,k 个鸡蛋  n 层楼,找到 F的最少操作次数。 
   B: 当第一个鸡蛋从第 i 层丢下:
   C: 鸡蛋碎了,那么现在剩下 k - 1 个鸡蛋,此时说明 F 在楼下(i 层的下面),接下来还要进行操作 dp[k-1] [i-1]次(子问题); 
   D: 鸡蛋没碎,说明此时的 F 在楼上(i 层的上面),接下来还要操作dp[k] [n-i]次(子问题哦)。 
所以得出dp方程:

dp[k][n] = min(1 + max(dp[k-1][i-1], dp[k][n-i]))  其中:(i in [1, n])

 

2. 初始条件:

我们已知状态转移方程,但方程中目前还都是未知数,所以要有一些初始已知条件,才能求出dp[k][n]。
(1)条件1:如果只有一个鸡蛋,那么多少层楼就要扔多少次鸡蛋,老老实实从下往上扔。
        for (int j = 1;j <= N;j++)
            dp[1][j] = j;
(2)条件2:如果只有1层时,那么无论多少个鸡蛋都只要扔一次:
        for (int i = 1;i <= K;i++)
            dp[i][1] = 1;

注意,我们要用的是动态规划的查表法,当然也可以用递归,如果用递归,我们就不需要用dp数组保存计算结果,以上初始条件也就是递归函数的结束条件。

而这里我们用查表法,就是把已知的数据先存下来,未知的dp[i][j]就能根据这些已知dp数值的推算出来,最后得出整个数据表。

于是用C++代码将以上思路实现如下:

class Solution {
public:
	int superEggDrop(int K, int N) {
		//将二维数组的长,宽分别设为K+1 N+1 ,dp[K][N]代表K个鸡蛋,N层的最少操作次数
		//形如dp(num,n) 用括号初始化,第一个数为初始化元素的个数,第二个为初始化值
		vector<vector<int>> dp(K + 1, vector<int>(N + 1, 0));
		//如果只有一层楼,那么无论多少个鸡蛋都只要扔一次
		for (int i = 1;i <= K;i++)
			dp[i][1] = 1;
		//如果只有一个鸡蛋,那么多少层楼就要扔多少次鸡蛋,老老实实从下往上扔
		for (int j = 1;j <= N;j++)
			dp[1][j] = j;
		
		//给所有的dp数组赋值(从2楼2个鸡蛋开始),并找出最小的
		for (int i = 2;i <= K;i++) {
			for (int j = 2;j <= N;j++) {
				int minVal = INT_MAX;
				for (int t = 1;t <= j;t++) 
					minVal= min(minVal, 1 + max(dp[i - 1][t - 1], dp[i][j - t]));

				dp[i][j] = minVal;

			}
		}
		return dp[K][N];
	}
};

另一个版本,递归实现(java):
不过递归的计算时间复杂度高,因为与递归版本的斐波那契数列一样,重复计算了很多遍底部节点的值,为了加快这个计算过程,建议使用上面的查表算法,就是拿空间换时间,把计算的中间结果都存储起来,后面直接查表即可。

class Solution {
    public int superEggDrop(int K, int N) {
        return Solution.recursive(K, N);
    }
    
    public static int recursive(int K, int N) {
        if (N == 0 || N == 1 || K == 1) {
            return N;
        }

        int minimun = N;
        for (int i = 1; i <= N; i++) {
            int tMin = Math.max(Solution.recursive(K - 1, i - 1), Solution.recursive(K, N - i));
            minimun = Math.min(minimun, 1 + tMin);
        }
        return minimun;
    }
}

这是我们的解法1的思路的两种实现,但是很不幸的是,这个思路时间复杂度太高了,都超时了。

解法2:

上面的方法的思路,都还是顺着题目的思路的进行的,其实我们可以换一个思路来想:

 设 dp[m][k] 为,k 个鸡蛋, m次操作(扔m次),可以判定的最大楼层数。现在的dp 方程如下:

 dp[m][k] = dp[m - 1][k] + dp[m - 1][k - 1] + 1

但是我们的状态转移方程并不是 dp[]m[]f(k,m)= max(f(k-1, m-1), f(k, m-1)) +1 而是f(k,m) = f(k-1,m-1) + f(k, m-1) + 1 +1即测试的X层本身。

为什么是+ 而不是取max,因为之前的思路是 K个鸡蛋测N层楼最坏情况下需要移动多少次, 与之相对的应该用 k个鸡蛋移动m次数最好情况下能测多少层。

   A: 如果当前鸡蛋 - 碎了, 此时,能判断出的楼层数,最少为 dp[m - 1][k - 1] 。
   B: 如果当前鸡蛋 - 没碎,此时,能判断出的楼层数,最多为 dp[m - 1][ k ] ,现在是不是还有 k个鸡蛋?而这k 个鸡蛋是不是 至少又可以向上 判断出 dp[m - 1][k - 1] 层,(因为之前已经算过了,只要加上就可以了。) 然后在加上当前这 1 层。所以总体就是上面的方程式了。 

有了这个方程式,我们能求M次次数和K个鸡蛋的情况下,最高能测多少层。

但题目求的是层数确定了,鸡蛋个数确定了,要求M的具体值。

其实一样的,比如确定鸡蛋个数K是3,楼层高度N是14。

假如只有一次尝试次数,3个鸡蛋,那么最高也就能测一层。达不到14的高度。

如果两次尝试次数,3个鸡蛋,那么record[2][3] = record[1][2] + record[1][3] + 1。

record[1][2]和record[1][3]代表一次尝试次数,那么最高只能测一层,所以上式结果是3。同样达不到14的高度。

如果三次尝试次数,3个鸡蛋,那么record[3][3] = record[2][2] + record[2][3] + 1=3+3+1=7。同样达不到14的高度。

如果四次尝试次数,3个鸡蛋,我们会发现record[4][3] = record[3][2] + record[3][3] + 1=6+7+1=14。刚好达到14的高度。

所以其实我们只需要不断尝试下去,最终尝试第M次的时候,发现record[M][K]>=N,那么就可以了。

 

具体写代码的时候,发现我们没办法提前确定M的次数,所以没办法定义一个M行K列的vector来存储数据。

但我们发现其实每次增大尝试次数的时候,都是基于上一次尝试的结果来求解。

所以我们可以只定义一个1行K列的vector,然后不断地更新这一行vector的数值,直到在某次更新之后vector[K]>=N。

int superEggDrop(int K, int N)
{
    vector<int>record(K+1,1);//包含0个鸡蛋的情况,所以需要申请K+1个空间。只尝试一次的时候,无论多少个鸡蛋,最高都只能测1层
    record[0]=0;//0个鸡蛋的情况特殊化处理,为0
    int move=2;//如果尝试2次
    while(record[K]<N)//当record[K]大于等于N的时候就退出循环
    {
        for(int i=K;i>=1;i--)//从vector的后面开始更新,这样不影响其他位置的vector元素的更新
            record[i]=record[i]+record[i-1]+1;
        move++;//move+1,再尝试一次
    }
    return move-1;//返回需要的尝试次数
}

参考博客:https://blog.csdn.net/XX_123_1_RJ/article/details/82284902

参考博客:https://www.cnblogs.com/chenjx85/p/10523857.html

参考题解:https://leetcode-cn.com/problems/two-sum/solution/ji-dan-diao-luo-xiang-jie-by-shellbye/

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值