887 super-egg-drop
题目:
你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?
思路1:动态规划
1)如果不考虑有几个鸡蛋,最优解法是二分搜索,相当于猜数字游戏
2)如果鸡蛋只有一个,那肯定是从N层往下尝试,到k层碎掉,F=k+1(根据题意x<F,则鸡蛋碎掉,在F-1层会碎),最差的情况则是F=0,那么就要尝试N次(到第一层还是没碎则F=0)
3)如果只有一层,则只要尝试一次(在第1层碎了,F=0;第一层没碎,F=1)
4)普通情况,我们用dp(k, n)表示有k个鸡蛋,n层楼,需要移动的次数。我们选择在test层进行测试,如果鸡蛋碎了,那么F<test,只需要尝试小于test的层,即dp(k-1, test-1);如果没碎,那么F>test,只要尝试高于test的层,即dp(k, n-test)。那么到底该选择哪一个呢?选择接下来会带来更差情况的那种可能,即两个值中的较大值
5)我们选择尝试每一层,选取最小值
class Solution {
public:
int superEggDrop(int K, int N) {
// [鸡蛋数目][楼层数目]
vector<vector<int>> dp(K+1, vector<int>(N+1, 0));
// base
for(int n=1;n<=N;n++)
{
dp[1][n] = n;
}
for(int k=1;k<=K;k++)
{
dp[k][1] = 1;
}
// state transfer
for(int k=2;k<=K;k++)// k个鸡蛋
{
for(int n=2;n<=N;n++) // n层楼
{
// 碎了:dp[k-1][test-1]
// 没碎:dp[k][n-test]
dp[k][n] = max(dp[k-1][0], dp[k][n-1]); //test = 1
for(int test = 2;test<=n;test++)
dp[k][n] = min(dp[k][n],
max(dp[k-1][test-1], // 碎了
dp[k][n-test])); // 没碎
dp[k][n]+=1;
}
}
return dp[K][N];
}
};
思路2
上面的方法会超时,3个for循环的时间复杂度为O(KN^2),需要进行优化。
在循环内查找最小值为线性查找,可以优化为二分查找。
dp[k-1][test-1]:k和n是固定的,随着test的增大而增大
dp[k][n-test]:k和n是固定的,随着test的增大而减小
则相当于求一个线性递增函数和一个线性递减函数的相交值(因为test是离散的,那么久转化为了求离相交值最近的两个test值,这两个值中更小的那个test)(详见leetcode官方题解)
class Solution {
public:
int superEggDrop(int K, int N) {
// [鸡蛋数目][楼层数目]
vector<vector<int>> dp(K+1, vector<int>(N+1, 0));
// base
for(int n=1;n<=N;n++)
{
dp[1][n] = n;
}
// state transfer
for(int k=2;k<=K;k++)// k个鸡蛋
{
for(int n=1;n<=N;n++) // n层楼
{
// 碎了:dp[k-1][test-1]
// 没碎:dp[k][n-test]
int l = 1, r = n, mid;
while(l+1<r)
{
mid = (l+r) >> 1;
if(dp[k-1][mid-1] < dp[k][n-mid]) l = mid+1;
else r = mid;
}
dp[k][n] = 1 + min(max(dp[k - 1][l - 1], dp[k][n - l]),
max(dp[k - 1][r - 1], dp[k][n - r]));
}
}
return dp[K][N];
}
};
思路3
除了二分查找,可以分析到
dp[k-1][test-1]:k和n是固定的,随着test的增大而增大;且与n无关(黑色线)
dp[k][n-test]:k和n是固定的,随着test的增大而减小;随着n的增大而增大(红色线)
所以交点一定是递增的
class Solution {
public:
int superEggDrop(int K, int N) {
// [鸡蛋数目][楼层数目]
vector<vector<int>> dp(K+1, vector<int>(N+1, 0));
// base
for(int n=1;n<=N;n++)
{
dp[1][n] = n;
}
// state transfer
for(int k=2;k<=K;k++)// k个鸡蛋
{
int test = 1;
for(int n=1;n<=N;n++) // n层楼
{
// 碎了:dp[k-1][test-1]
// 没碎:dp[k][n-test]
while (test < n &&
max(dp[k - 1][test - 1], dp[k][n - test]) > max(dp[k - 1][test], dp[k][n - test - 1]))
{
test++;
}
dp[k][n] = 1 + max(dp[k - 1][test - 1], dp[k][n - test]);
}
}
return dp[K][N];
}
};
思路4
逆向思考,这个思路真的很赞,然而想不到(参见官方题解)
假设我们可以移动T次,有K个鸡蛋。随机选择一层进行测试,如果碎了,那么我么测试下面还有多少层,即f(T-1, K-1);如果没有碎,那么可以测试当前层上面还有几层,即f(T-1, K),加上当前这一层,即为移动T次,有K个鸡蛋能够测试到的所有层。
f(T, K) = f(T-1, K) + f(T-1, K-1) + 1
=> f(K) = f(K) + f(K-1) + 1
class Solution {
public:
int superEggDrop(int K, int N) {
vector<int> f(K + 1, 1);
f[0] = 0;
int test = 1;
while (f[K] < N) {
for (int i = K; i >= 1; i--) // test次可以到达的最高层数,因为依赖于前一项的上一个状态,所以需要从后往前计算
f[i] = f[i] + f[i - 1] + 1;
test++;
}
return test;
}
};