双蛋问题,递归和动态规划

问题引入
鸡蛋掉落问题: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. 第1枚鸡蛋从楼层 i i i扔下去后碎掉了,说明寻找的楼层 f 位于 [ 0 , i ) [0,i) [0,i)的范围内;
  2. 第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+1inminmax(λ(k1,i1),λ(k,ni))

无论是哪一种情况,最终的结果要么是鸡蛋数减少,要么是楼层数减少;那么我们可以递归地推导下去,直到一个基础情况:

  1. k = 1 k=1 k=1,即只有一个鸡蛋,那么无论有多少楼层,扔鸡蛋次数都是 1 1 1;
  2. n = 1 n=1 n=1,即只有一个楼层,那么无论有多少鸡蛋,扔鸡蛋次数也是 1 1 1

根据上面的两种基本情况,我们可以列出如下的表格:
Alt
在构建动态规划表格时,如果我们采用从底往上的推导策略的话,会不会出现没有计算过状态?例如,求解

λ ( 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+1i5minmax(λ(1,i1),λ(2,5i))

会不会 λ ( 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 λ(k1,i),0in, 即 k − 1 k-1 k1枚鸡蛋从 1 − n 1-n 1n楼层的最少次数,和 λ ( k , n − i ) \lambda(k, n-i) λ(k,ni),即 k k k枚鸡蛋 1 ∼ n − 1 1\sim n-1 1n1楼层的最少次数。因而,在遍历到 ( k , n ) (k, n) (k,n)之前,应该遍历完 k − 1 k-1 k1 枚鸡蛋和 ( n − 1 ) (n-1) (n1)个楼层所有的状态,即遍历的顺序应为

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] [1n] 的楼层,来寻找最小值,这是导致 O ( k n 2 ) O(kn^2) O(kn2) 的时间复杂度的所在,这个过程是否可以优化,即不用遍历每一层,就能找到 [ 1 − n ] [ 1 - n] [1n] 选择中的最小值呢?我们将状态转移方程改造成如下的形式:
λ ( 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+1inminmax(λ(k1,i1),λ(k,ni))
我们观察到, λ ( k − 1 , i − 1 ) \lambda(k-1, i - 1) λ(k1,i1) 随着 i i i 的增加而单调不减的, λ ( k , n − i ) \lambda(k, n-i) λ(k,ni) 随着 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) λ(k1,i1)λ(k,ni);随着 i i i 逐渐增加, λ ( k − 1 , i − 1 ) \lambda(k-1, i-1) λ(k1,i1) $ 逐渐增加, λ ( k , n − i ) \lambda(k, n-i) λ(k,ni) 逐渐减少,即寻找“山谷”。

这里的二分法其实还蛮难理解的,和我们平时见到的二分法感觉不同。

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];
	}
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值