问题引入
鸡蛋掉落问题:leetcode 887: 鸡蛋掉落
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 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
问题分析
乍一看这道题,最少的扔鸡蛋次数。这个限定就比较模糊,什么是最少的扔鸡蛋次数。如果正好站在f楼层上,那么最少扔鸡蛋次数是不是1?不是1,即使站在 f 楼层上,扔下去的鸡蛋没碎,也只能说明待寻找的f楼层位于 [f + 1, N]之间,假定楼层总数是N。什么叫最少的扔鸡蛋次数呢?是指我们按照某种策略
i
i
i,在此策略下,由于楼层 f 随机性的影响,扔鸡蛋的次数是
λ
i
\lambda_i
λi, 最差的情况下一定能找到楼层 f 所需的次数是
max
λ
i
\max\lambda_i
maxλi;所有的策略中,
max
λ
i
\max\lambda_i
maxλi 的最小值,即:
min
max
λ
i
\min\max\lambda_i
minmaxλi
这样的策略该如何寻找呢。假设鸡蛋总数是
k
k
k,楼层总数是
n
n
n,找到楼层 f 所需的最少扔鸡蛋次数是
λ
(
k
,
n
)
\lambda(k, n)
λ(k,n)
第1枚鸡蛋所扔的楼层是
i
i
i, 则根据第1枚鸡蛋在楼层
i
i
i扔下去碎还是不碎的结果,对后续的扔鸡蛋过程会发生不同的影响:
- 第1枚鸡蛋从楼层 i i i扔下去后碎掉了,说明寻找的楼层 f 位于 [ 0 , i ) [0,i) [0,i)的范围内;
- 第1枚鸡蛋从楼层 i i i扔下去后没有碎,说明寻找的楼层 f 位于 [ i , n ] [i,n] [i,n]的范围内;
根据上面两种情况可以写出如下的方程:
λ ( k , n ) = 1 + min 1 ≤ i ≤ n max ( λ ( k − 1 , i − 1 ) , λ ( k , n − i ) ) \lambda(k,n)=1 + \min_{1\leq i\leq n}\max(\lambda(k-1, i-1), \lambda(k, n-i)) λ(k,n)=1+1≤i≤nminmax(λ(k−1,i−1),λ(k,n−i))
无论是哪一种情况,最终的结果要么是鸡蛋数减少,要么是楼层数减少;那么我们可以递归地推导下去,直到一个基础情况:
- k = 1 k=1 k=1,即只有一个鸡蛋,那么无论有多少楼层,扔鸡蛋次数都是 1 1 1;
- n = 1 n=1 n=1,即只有一个楼层,那么无论有多少鸡蛋,扔鸡蛋次数也是 1 1 1。
根据上面的两种基本情况,我们可以列出如下的表格:
在构建动态规划表格时,如果我们采用从底往上的推导策略的话,会不会出现没有计算过状态?例如,求解
λ ( 2 , 5 ) = 1 + min 1 ≤ i ≤ 5 max ( λ ( 1 , i − 1 ) , λ ( 2 , 5 − i ) ) \lambda(2, 5) =1 + \min_{1\leq i \leq 5}\max(\lambda(1,i-1), \lambda(2, 5-i)) λ(2,5)=1+1≤i≤5minmax(λ(1,i−1),λ(2,5−i))
会不会 λ ( 2 , 4 ) \lambda(2,4) λ(2,4)没有计算过?根据 λ ( k , n ) \lambda(k, n) λ(k,n)的计算公式,选择 i i i在遍历的时候,会用到以下的状态 λ ( k − 1 , i ) , 0 ≤ i ≤ n \lambda(k-1, i), 0\leq i\leq n λ(k−1,i),0≤i≤n, 即 k − 1 k-1 k−1枚鸡蛋从 1 − n 1-n 1−n楼层的最少次数,和 λ ( k , n − i ) \lambda(k, n-i) λ(k,n−i),即 k k k枚鸡蛋 1 ∼ n − 1 1\sim n-1 1∼n−1楼层的最少次数。因而,在遍历到 ( k , n ) (k, n) (k,n)之前,应该遍历完 k − 1 k-1 k−1 枚鸡蛋和 ( n − 1 ) (n-1) (n−1)个楼层所有的状态,即遍历的顺序应为
for i = 1 to k
for j = 1 to n
边界值处理: λ ( k , n ) \lambda(k,n) λ(k,n) 由于 i i i 的选择, 会出现一个临界条件,即 i = n i=n i=n 的情况。这时候需要 λ ( k , 0 ) \lambda(k,0) λ(k,0) 的值,没有楼层, λ ( k , 0 ) = 0 \lambda(k,0) = 0 λ(k,0)=0,这是从常识出发得出的结论。如果我们一开始就从顶楼扔,如果鸡蛋没碎,那么寻找的楼层 f 直接等于 n n n 了,子问题将不复存在。故不用考虑 λ ( k , 0 ) \lambda(k,0) λ(k,0)了,将 λ ( k , 0 ) \lambda(k,0) λ(k,0)设置成 0 0 0 是合理的。
上述动态规划的时间复杂度是
O
(
k
n
2
)
O(kn^2)
O(kn2),仍然是比较高的数量级别。如果楼层总数
n
n
n 很大,那么时间复杂度会非常高。如何来优化时间复杂度呢?观察动态规划的状态转移方程,这种解法中我们遍历了
[
1
−
n
]
[1 - n]
[1−n] 的楼层,来寻找最小值,这是导致
O
(
k
n
2
)
O(kn^2)
O(kn2) 的时间复杂度的所在,这个过程是否可以优化,即不用遍历每一层,就能找到
[
1
−
n
]
[ 1 - n]
[1−n] 选择中的最小值呢?我们将状态转移方程改造成如下的形式:
λ
(
k
,
n
)
=
1
+
min
1
≤
i
≤
n
max
(
λ
(
k
−
1
,
i
−
1
)
,
λ
(
k
,
n
−
i
)
)
\lambda(k,n)=1 + \min_{1\leq i\leq n}\max(\lambda(k-1, i - 1), \lambda(k, n-i))
λ(k,n)=1+1≤i≤nminmax(λ(k−1,i−1),λ(k,n−i))
我们观察到,
λ
(
k
−
1
,
i
−
1
)
\lambda(k-1, i - 1)
λ(k−1,i−1) 随着
i
i
i 的增加而单调不减的,
λ
(
k
,
n
−
i
)
\lambda(k, n-i)
λ(k,n−i) 随着
i
i
i 的增加单调不增;故min max问题的最优解一定发生在这二者相等的附近。当
i
=
1
i = 1
i=1 的时候,
λ
(
k
−
1
,
i
−
1
)
≤
λ
(
k
,
n
−
i
)
\lambda(k-1, i - 1) \leq \lambda(k,n-i)
λ(k−1,i−1)≤λ(k,n−i);随着
i
i
i 逐渐增加,
λ
(
k
−
1
,
i
−
1
)
\lambda(k-1, i-1)
λ(k−1,i−1) $ 逐渐增加,
λ
(
k
,
n
−
i
)
\lambda(k, n-i)
λ(k,n−i) 逐渐减少,即寻找“山谷”。
这里的二分法其实还蛮难理解的,和我们平时见到的二分法感觉不同。
class Solution {
public:
int superEggDrop(int k, int n) {
vector<vector<int>> dp(k+1, vector<int>(n+1, 0));
// init condition
for (int i = 1; i <=k; ++i) {
for (int j = 1; j <=n; ++j) {
dp[i][j] = j;
}
}
// dynamic programing
for (int i = 2; i <= k; ++i) {
for (int j = 2; j <= n; ++j) {
int left = 1, right = j;
while (left <= right) {
int mid = left + (right - left)/2;
int value1 = dp[i-1][mid-1], value2 = dp[i][j-mid];
dp[i][j] = min(dp[i][j], max(value1, value2) + 1);
if (value1 > value2) {
right = mid - 1;
}
else if (value1 < value2) {
left = mid + 1;
}
else {
break;
}
}
}
}
return dp[k][n];
}
};