题目:
鸡蛋掉落
你将获得 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/