在leetcode刷动态规划
问题过程中,鸡蛋掉落问题是比较经典的,特别是笔试面试喜欢出的问题。腾讯,Vivo等大厂都出现过,在这里通过自己学习,以及借鉴大佬的思路,对这道题进行整理。
其它算法问题刷题总结可以参考:基础算法分类总结(持续更新中)。
遇到这个问题时,很多同学就觉得使用二分查找,不断缩减查找的区间。但直接使用二分查找去计算层数时,鸡蛋是不够的。题目中描述说,每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋
。可以分析发现:
一颗蛋在任一楼层扔下,分两种情况:
因此可以使用动态规划来解决这道问题。
鸡蛋掉落的问题可以理解为动态规划中典型的01背包问题。背包:鸡蛋数(k个); 物品:操作数(n个);价值:确定楼层
。01背包问题可以参考:动态规划之背包问题——01背包。
有人问了,为什么不是鸡蛋作为物品,操作数作为背包?背包问题往往物品与价值有正相关关系。鸡蛋有k个,但实际不一定全都用上,限制一定的最小操作数,鸡蛋增加,确定楼层(价值)不一定增加。而取一定的鸡蛋,操作数每增加1,确定楼层(价值)就会一定增加。
因此可以定义:dp[i][count]代表k个鸡蛋扔count层最多能测试层数
。
状态转移方程:当前价值 = 鸡蛋碎了的价值 + 鸡蛋没碎的价值 + 确定当前层的价值1,dp[i][count] = dp[i-1][count-1]+dp[i][count-1]+1;
这样二维数组的01背包解法的如下(二维01背包的遍历顺序都可以):
class Solution {
public int superEggDrop(int k, int n) {
// 动态规划01背包,dp[i][count]代表k个鸡蛋扔count层最多能测试层数
// 当前价值 = 鸡蛋碎了的价值 + 鸡蛋没碎的价值 + 确定当前层的价值1
// dp[i][count] = dp[i-1][count-1]+dp[i][count-1]+1;
// 背包:鸡蛋数(k个); 物品:操作数(n个);价值:确定楼层
if (n == 1) return 1;
int[][] dp = new int[k + 1][n + 1];
int count = 0;
for (; dp[k][count] < n;) { // 遍历物品,这里直到价值为n就结束
count++;
for (int i = k; i >= 1; i--) {
dp[i][count] = dp[i-1][count-1] + dp[i][count-1] + 1;
}
}
return count;
}
}
对应的就是01背包动态规划的一维解法。
因为状态 i 只与状态 i-1 有关,int[] dp = new int[k + 1];
一维数组的遍历顺序是先遍历物品,再遍历背包,且逆序。
class Solution {
public int superEggDrop(int k, int n) {
if (n == 1) return 1;
// 一维数组
int[] dp = new int[k + 1];
int count = 0;
for (; dp[k] < n;) { // 遍历物品,这里直到价值为n就结束
count++;
for (int i = k; i >= 1; i--) {
dp[i] = dp[i-1] + dp[i] + 1;
}
}
return count;
}
}
这里可以对遍历做一下处理:
while (dp[k] < n) {
count++;
for (int i = k; i >= 1; i--) {
dp[i] += dp[i-1] + 1;
}
}
如果01背包不好理解,可以换一种理解方法,但整体思想是一致的。
状态定义:dp[i][j] 表示在i步用j个鸡蛋可以测出最大层数
状态转移:
dp[i - 1][j]
(上层);dp[i - 1][j - 1]
(下层);1
(当前层);则最大层数 = 上层 + 下层 + 当前层:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + 1;
class Solution {
public int superEggDrop(int k, int n) {
// dp[i][j] 表示在i步用j个鸡蛋可以测出最大层数
if (n == 1) return 1;
int[][] dp = new int[n + 1][k + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
// 鸡蛋没碎,数量不变,用了一步,dp[i - 1][j](上层)
// 鸡蛋碎了,数量减一,用了一步,dp[i - 1][j - 1](下层)
// 1(当前层)
// 最大层数 = 上层 + 下层 + 当前层
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + 1;
if (dp[i][j] >= n) return i;
}
}
return n;
}
}
同样的因为本次状态至于鸡蛋有关,可以优化二维数组为一维数组。
class Solution {
public int superEggDrop(int k, int n) {
// dp[j] 表示用j个鸡蛋可以测出最大层数
if (n == 1) return 1;
// 一维数组
int[] dp = new int[k + 1];
for (int i = 1; i <= n; i++) {
for (int j = k; j > 0; j --) { // 转一维需要逆序
// 鸡蛋没碎,数量不变,用了一步,dp[j](上层)
// 鸡蛋碎了,数量减一,用了一步,dp[j - 1](下层)
// 1(当前层)
// 最大层数 = 上层 + 下层 + 当前层
dp[j] += dp[j - 1] + 1;
if (dp[j] >= n) return i;
}
}
return n;
}
}
总结:
初看到这道题很容易被二分查找缩小区间给误导,这道题用动态规划解决需要自己分析其中的状态转换的过程,列出状态转移方程。
参考:
记录Leetcode 鸡蛋掉落 的思路
鹰蛋问题(leetcode 887 鸡蛋掉落)
官方题解:鸡蛋掉落