这里写自定义目录标题
鸡蛋掉落/高楼砸鸡蛋(动态规划)
引子:
两个软硬程度一样但未知的鸡蛋,它们有可能都在一楼就摔碎,也可能从一百层楼摔下来没事,每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。有座100层的建筑,要你用这两个鸡蛋通过最少的次数确定哪一层是鸡蛋可以安全落下的最高位置。
思路:
看到这个题目,那么最保险的方法就是一层一层试验,但这样只需要一个鸡蛋就可以了。我们现在有两个鸡蛋,完全可以用有更快的方法。
第一个鸡蛋,可能的落下位置
[
1
,
n
]
[1,n]
[1,n],第一个鸡蛋从第
i
i
i层扔下,有两种情况:
①碎了,第二个鸡蛋,需要从第一层开始试验,有
i
−
1
i-1
i−1次机会
②没碎,两个鸡蛋,还有
n
−
i
n-i
n−i层。变成
f
(
n
−
i
)
f(n-i)
f(n−i)子问题。 所以,当第一个鸡蛋,由第
i
i
i个位置落下的时候,要尝试的次数为
1
+
m
a
x
(
i
−
1
,
f
(
n
−
i
)
)
1+ max(i - 1, f(n - i))
1+max(i−1,f(n−i)) 因为我们要考虑到最坏的情况,那么对于每一个
i
i
i,要求尝试次数最少。状态转移方程如下:
f ( n ) = 1 + m i n ( max 1 ≤ x ≤ n ( i − 1 , f ( n − i ) ) ) f(n) = 1+min (\max\limits_{1≤x≤n}(i - 1, f(n - i)) ) f(n)=1+min(1≤x≤nmax(i−1,f(n−i)))
那如果我们规定鸡蛋的数量呢
题目:
你将获得
K
K
K 个鸡蛋,并可以使用一栋从
1
1
1 到
N
N
N 共有
N
N
N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。你知道存在楼层
F
F
F ,满足
0
<
=
F
<
=
N
0 <= F <= N
0<=F<=N 任何从高于
F
F
F的楼层落下的鸡蛋都会碎,从
F
F
F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层
X
X
X 扔下(满足
1
<
=
X
<
=
N
1 <= X <= N
1<=X<=N)。
你的目标是确切地知道
F
F
F 的值是多少。
无论
F
F
F 的初始值如何,你确定 F 的值的最小移动次数是多少?
题解:
这个题加入了鸡蛋数量这个限制条件,所以我们的转移方程的维度也要变成二维即
(
K
,
N
)
(K,N)
(K,N),其中
K
K
K 为鸡蛋数,
N
N
N 为楼层数(这里的楼层数并不是表示楼的高度,而是表示可能有解的范围),表示有K个鸡蛋和N层楼的时候,确定鸡蛋会碎的最小楼层所需要的最小移动次数。
但鸡蛋并不需要一定都被摔碎,比如有1层楼但我们有2个鸡蛋的时候,我们只要在第一层扔下鸡蛋,如果摔碎,那么
F
=
0
F=0
F=0,如果没碎,那么
F
=
1
F=1
F=1。我们只用了1颗鸡蛋便解决了这个问题。现在我们把它推广:
当我们从第
X
X
X 楼扔鸡蛋的时候
①如果鸡蛋碎了,那么可以确定可以摔碎的楼层
F
F
F在
[
0
,
X
−
1
]
[0,X-1]
[0,X−1]中,同时我们也损失了一颗宝贵的鸡蛋,这样问题便缩小为一个
(
K
−
1
,
X
−
1
)
(K-1,X-1)
(K−1,X−1)的子问题。
②如果鸡蛋没碎,那么同理我们可以确定可以摔碎的楼层
F
F
F在
X
X
X层往上,即
[
X
+
1
,
N
]
[X+1,N]
[X+1,N],问题便转化为
(
K
,
N
−
X
)
(K,N-X)
(K,N−X)的子问题。这里的
N
−
X
N-X
N−X并不是表示第0层到第
N
−
X
N-X
N−X层,由于我们已经确定目标楼层范围为
[
X
+
1
,
N
]
[X+1,N]
[X+1,N],则
[
0
,
X
]
[0,X]
[0,X]层我们便不在考虑,可以等价为第0层到第
N
−
X
N-X
N−X层。
对此我们令
d
p
(
K
,
N
)
dp(K,N)
dp(K,N)为
K
K
K颗鸡蛋,
N
N
N层楼时,所能确定
F
F
F的最小步数,转移方程为:
d p ( K , N ) = 1 + min 1 ≤ x ≤ N ( m a x ( d p ( K − 1 , X − 1 ) , d p ( K , N − X ) ) ) dp(K,N)=1+\min \limits_{1≤x≤N}(max(dp(K−1,X−1),dp(K,N−X))) dp(K,N)=1+1≤x≤Nmin(max(dp(K−1,X−1),dp(K,N−X)))
因为题目中让我们求最坏情况下的最小移动次数,而我们并不知道 F F F的值具体是多少,对于每一次在 X X X层扔鸡蛋都会有俩种情况,碎或者没碎,因此我们必须取鸡蛋碎了之后接下来需要的步数和鸡蛋没碎之后接下来需要的步数二者的最大值;而对于最小移动次数,在 ( K , N ) (K,N) (K,N)的情况下,我们可以在 [ 1 , N ] [1,N] [1,N]楼层中尝试,每次尝试都会有上述的两种情况。那么便要取所有两种情况中最大值的最小值。
如果我们直接暴力求解,便会有
K
∗
N
K*N
K∗N种状态,对于每种状态循环楼层
X
X
X需要
O
(
N
)
O(N)
O(N)时间,那么时间复杂度达到了
O
(
K
N
2
)
O(KN^2)
O(KN2),这如何帮助我们来优化这个问题呢?
我们观察到
d
p
(
K
,
N
)
dp(K,N)
dp(K,N)我们在鸡蛋数量
K
K
K固定的情况下,是随着
N
N
N的增加单调不减的,那么上述两项
P
(
X
)
=
d
p
(
K
−
1
,
X
−
1
)
,
Q
(
X
)
=
d
p
(
K
,
N
−
X
)
P(X)=dp(K−1,X−1),Q(X)=dp(K,N−X)
P(X)=dp(K−1,X−1),Q(X)=dp(K,N−X)中,
P
(
X
)
P(X)
P(X)是随着
X
X
X单调递增的,
Q
(
X
)
Q(X)
Q(X)是随着
X
X
X单调递减的。
这如何帮助我们来优化这个问题呢?我们可以将这两个函数放在直角坐标系中:
不难看出,红色部分便是在
[
1
,
N
]
[1,N]
[1,N]区间中所有
m
a
x
(
d
p
(
K
−
1
,
X
−
1
)
,
d
p
(
K
,
N
−
X
)
)
max(dp(K−1,X−1),dp(K,N−X))
max(dp(K−1,X−1),dp(K,N−X))
的值,如果这两个函数是连续的,、那么我们只需要找出这两个函数的交点,在交点处就能保证这两个函数的最大值最小;但函数是离散的,它们只能在整数取值时有意义,如果交点
X
2
X_2
X2不是整数,那么我们就要找出距离
X
2
X_2
X2点最近的两个点
X
0
X_0
X0和
X
1
X_1
X1(不难想到它们相差1),取
Q
(
X
0
)
Q(X_0)
Q(X0)与
P
(
X
1
)
P(X_1)
P(X1)中的最小值便是
min
1
<
x
<
N
(
m
a
x
(
d
p
(
K
−
1
,
X
−
1
)
,
d
p
(
K
,
N
−
X
)
)
)
\min \limits_{1<x<N}(max(dp(K−1,X−1),dp(K,N−X)))
1<x<Nmin(max(dp(K−1,X−1),dp(K,N−X)))的值;如果恰好交点
X
2
X_2
X2是整数,那只需要取
P
(
X
2
)
P(X_2)
P(X2)即可。
对于单调函数的搜索不难想到使用二分法查找
X
0
X_0
X0再去找到
X
1
X_1
X1即可,对于二分法便不做赘述。
这样一来,对于给定的状态
(
K
,
N
)
(K, N)
(K,N),我们只需要
O
(
l
o
g
N
O(logN
O(logN) 的时间,通过二分查找就能得到最优的那个
X
X
X,因此时间复杂度从
O
(
K
N
2
)
O(KN^2)
O(KN2) 降低至
O
(
K
N
l
o
g
N
)
O(KNlogN)
O(KNlogN)。
public int superEggDrop(int K, int N) {
int dp[][] = new int [K+1][N+1];
if(K==1) return N;
if(N==1) return 1;
for(int i=1;i<=N;i++) {
dp[1][i]=i;//一个鸡蛋只能从1层开始扔
}
for(int i=1;i<=K;i++) {
dp[i][1]=1;//一层楼的话只要在第一层扔就可以了
}
for(int i=2;i<=K;i++) {
for(int j=2;j<=N;j++) {
int flag=1;
int l=1,r=j;
int mid=(r+l)>>1;
while(l<=r) {
if(dp[i-1][mid-1]==dp[i][j-mid]) {
flag=mid;
break;
}else if(dp[i-1][mid-1]<dp[i][j-mid]) {
flag=mid;
l=mid+1;
}else {
r=mid-1;
}
mid=(r+l)>>1;
}
int ans1=Math.max(dp[i-1][flag-1], dp[i][j-flag]);
int ans2=Math.max(dp[i-1][flag], dp[i][j-flag-1]);
dp[i][j]=1+Math.min(ans1, ans2);
}
}
return dp[K][N];
复杂度分析
时间复杂度: O ( K N l o g N ) O(KNlogN) O(KNlogN)。我们需要计算 O ( K ∗ N ) O(K∗N) O(K∗N) 个状态,每个状态计算时需要 O ( l o g N ) O(logN) O(logN) 的时间进行二分搜索。
空间复杂度: O ( K N ) O(KN) O(KN)。我们需要 O ( K N ) O(KN) O(KN) 的空间存储每个状态的解。
来源:LeetCode 887
https://leetcode-cn.com/problems/super-egg-drop