算法分析与设计第九次作业( leetcode 中 Super Egg Drop 题解 )

题目描述

在这里插入图片描述

问题分析

这是一个很有趣的题目,是一个很贴近生活的一个小实验 ---- 把鸡蛋从楼上扔下来,如果楼层太高自然鸡蛋会碎,高度比较低就不会摔碎,这里有一座N层的大楼,给你K个鸡蛋,并测出最高从什么地方扔下鸡蛋不会摔碎,如果把这个高度记作F,那么如果从F和F楼以下的地方扔鸡蛋不会摔碎鸡蛋,而从F楼以上的楼层扔下鸡蛋就会摔碎鸡蛋,然后问你在这个求解的过程最少要扔几次鸡蛋。

解题思路

要测出楼层F,最简单的办法自然是从底楼开始扔鸡蛋,如果鸡蛋没有碎就到更高一层的楼房扔鸡蛋,直到遇到某个楼层X,把这个鸡蛋摔碎了,那么F=X-1,但是这样的话求解会非常的慢,如果考虑到F=N的话,这个求解的过程就需要扔N次鸡蛋,这显然不是这个问题的答案,因为这种方法每次操作只能排除一层楼,但是如果从第i层扔鸡蛋就可以一次排除N-i或者i-1层楼,这样就能更快找到答案,这个求解次数就很可能小于N(除非只给了你一个鸡蛋,K=1,那么只能从底层一层一层向上测,否则万一图中摔碎鸡蛋就没法继续测了)。

从上面的分析我们得知,比较好的方法是:

  • 当K>1时,遍历所有的楼层i,i∈(1,n),从中间某个楼层i扔下一个鸡蛋,找出某个楼层使得测出F的步数最少(取这i种扔鸡蛋方法求解问题所需要步数的最小值),记步数为S,初始化S=∞,然后:
    • 如果层数i=1,只要扔一次鸡蛋,鸡蛋碎了F=0,否则F=1,然后结束操作(跳过后面的操作);
    • 如果鸡蛋没有碎,就仍然使用这K个鸡蛋来从i+1到N中找出F值,这等价于子问题使用K个鸡蛋从1到N-i层中找出F值最少需要仍几次鸡蛋,因为需要测量的就是i+1到N这一共N-i-1层是否会在某一层摔碎鸡蛋,1到i层都不需要测量了,因为肯定不会摔碎(i层都摔不碎,往下更加摔不碎),使用K个鸡蛋从1到N-i层中找出F值最少需要仍几次鸡蛋 同样需要测出N-i-1层楼中,在哪一层会摔碎鸡蛋,所以会使用的步数相同;然后我们记这个子问题的答案是F1;
    • 如果鸡蛋碎了,就少了一个鸡蛋,需要使用剩下的K-1个鸡蛋从1到i-1中找出F值,这就是子问题使用K-1个鸡蛋从1到i-1层中找出F值最少需要仍几次鸡蛋,因为在第i层摔碎了,高于i层肯定是会摔碎的,所以只要在1到i-1层里面找;然后我们记这个子问题的答案是F2;
    • 由于需要保证找出F,所以应该要取上面两种情况种最差的,这样才能保证在给定步数找出F(不是取最好的,因为如果恰好发生了更差的那种情况,那么就无法在给定步数找到F了),于是F=max{F1,F2};
    • 如果上述F比当前记录的最好方法步数S还要少,就将S替换为F的值;
  • 当K=1时,只能从底层一层一层向上测,问题求解的过程最少要扔N次鸡蛋,N是层数;

根据上面的描述可以实现一个递归函数,使用K(鸡蛋数),N(楼层数),求解出最少的扔鸡蛋次数,但是如果我们仔细分析我们会发现,这个递归算法会有很多重复子问题的计算:比如给出K=3,N=10的一个问题,按照如下两种方法求解会重复求解相同的子问题:

  1. 选择首先从6层扔鸡蛋,鸡蛋碎了,接下来求K=2,N=5的子问题,选择从第3层扔鸡蛋,鸡蛋碎了,接下来需要求解K=1,N=2的子问题;
  2. 选择首先从7层扔鸡蛋,鸡蛋碎了,接下来求K=2,N=6的子问题,选择从第3层扔鸡蛋,鸡蛋碎了,接下来需要求解K=1,N=2的子问题;
  3. 上面两种方法都上面两种方法都求解了k=1,n=2的子问题,其实在上面的递归算法中会有大量这样的重复子问题求解,所以考虑使用记忆化方法求解,即使用一个数组ans[n][k]来存储之前已经求结过的K=k,N=n的子问题,下次再需要求解时直接返回结果;

然而当我使用上面改进后方法,仍然超时,所以还有改进空间,我们首先看看上面算法的复杂度:

  • 上面算法其实就是求解一个二维数组ans[N][K],然后最终的答案将存储在ans[N-1][K-1]中
  • 而每一次求解一个ans[i][j]所做的操作是遍历i种扔鸡蛋的方法(扔鸡蛋楼层从1到i),然后取其中最好的作为ans[i][j]的值,即ans[i][j]=min{max{ans[i-1][j]+1,ans[0][j-1]+1},max{ans[i-2][j]+1,ans[1][j-1]+1},…,max{ans[i-k][j]+1,ans[k-1][j-1]+1},…,max{ans[1][j]+1,ans[i-2][j-1]+1},max{ans[0][j]+1,ans[i-1][j-1]+1}};
    上面内层的max{ans[i-k][j]+1,ans[k-1][j-1]+1}指的是,当我们求解N=i,K=j的子问题的时候,选择从第k层扔下鸡蛋,此时求解代价是从两个子问题种选出代价高的,即ans[i-k][j]和ans[k-1][j-1]中较大者,然后加上1(本层扔鸡蛋操作),作为这种方法的求解代价,因为这样能够保证在最坏的情况下也能够用ans对应步数完成求解F;
    然后外层的max{…}指的是,当我们求解N=i,K=j的子问题的时候,综合所有从第k层扔下鸡蛋的方法(k∈(1.i)),选出最小的ans作为这个子问题的解,因为这种扔鸡蛋能够保证在最小的步数完成求解F;
    因为i的平均取值是(1+N)/2,所以这个求解ans[i][j]子问题的复杂度是O(n),因为它从平均(1+N)/2个数中选出最小的一个;
  • 所以综合上面的两点,整个问题求解复杂度是 O ( ( N ∗ K ) ∗ N ) = O ( K ∗ N 2 ) O((N*K)*N)=O(K*N^2) O((NK)N)=O(KN2)

所以我们需要比 O ( K ∗ N 2 ) O(K*N^2) O(KN2)更好的算法,首先我认为这个二维数组ans[N][K]还是要填满的,因为K*N规模的子问题还是要求解的,所以可以考虑优化每一个ans[i][j]的求解,这个“取最小值操作”用优于O(n)的方法实现;
我们分别分析求解ans[i][j]的过程:
选择从第k层扔下鸡蛋,然后得到两个子问题:
求解ans[i-k][j]ans[k-1][j-1]
我们发现 i,j 不变时随着k增大,ans[i-k][j]减少而ans[k-1][j-1]增大,因为前者的楼层减少,所以自然需要更少步数,后者楼层数增多,步数增多;
如果绘出下面的图,就能更直观地分析问题:
在这里插入图片描述
我们对ans[i][j]的求解就是遍历所有k,然后对与每个k,求出max{ans[i-k][j],ans[k-1][j-1]},也就是找出上面图中所有蓝色的点(它是ans[1][j]或者ans[i-2][j-1]中较大的);
然后从所有蓝色点中取最低点加1作为ans[i][j]的值;
所以现在问题就变成了找出上面红色曲线的最低的(但是注意横坐标取值是整数,所以实际上是在一系列离散点中求解最低点),有了上面的图我们可以很简单的得到答案(下面是粗略步骤,更精准的步骤见算法实现部分):
找出两条曲线的交点,这就是ans[i][j]的解:
由于ans[i-k][j]+1和ans[k-1][j-1]两个序列都是有序的,所以我们可以使用二分法:
每次找区间中点作为k比较ans[i-k][j]+1和ans[k-1][j-1],
如果ans[i-k][j]更大,则这个区间中点位于两线交点左侧,将区间起始点设为当前的区间中点+1;
如果ans[k-1][j-1],则这个区间中点位于两线交点右侧,将区间终点设为当前的区间中点-1;
然后重复二分操作直到区间起点等于终点,这个起点的ans值就是ans[i][j]的解;
因为这样一个二分操作找出ans[i][j]的复杂度是O(log(n)),所以优化之后的算法复杂度是 O ( K ∗ N ∗ l o g ( N ) ) O(K*N*log(N)) O(KNlog(N)),最终这样一个算法AC了。

算法步骤
Int Find(K,N):
使用K个鸡蛋,找出N层楼的F值,返回F:
	如果ans[N][K] != 0,说明这个子问题已经求解过,return ans[N][K];
	如果只有一个鸡蛋, ans[N][K]=N,return ans[N][K],因为只能从底层往上慢慢测试;
	如果N==0,ans[N][K]=0,return ans[N][K],因为不用测,F就是0;

	设置区间起点start为1,终点end为n,开始循环二分该区间,直到start==end才退出该循环:
		mid=(start+end)/2;
		如果find(K-1,mid-1)大于find(K,N-mid)
			end=mid-1;
		如果find(K-1,mid-1)于find(K,N-mid)
			start=mid+1;
		如果find(K-1,mid-1)等于find(K,N-mid)
			ans[N][K]=find(K-1,mid-1)+1;
			返回ans[N][K],结束函数;
	
	如果start为1或者n
		ans[N][K] 设为 find(K-1, start-1)和find(K, N-start)中较大的再加上1,返回这个ans[N][K],函数结束;
	否则
		在start的周围(左右两侧)找出符合条件的答案赋值给ans[N][K],返回这个ans[N][K],函数结束;
		(上面这个寻找方法是,对start-1,start,start+1分别求出对应的两个子问题中较大者,然后在求出的三个ans中取最小的作为ans[N][K]的答案)
复杂度分析

前面解题思路中已详细分析过,填写二维数组ans[n][k]的每一个值需要O(log(N))的复杂度,所以整个问题求解复杂度为O(KNlog(N))。

代码实现&结果分析

代码实现:

class Solution {
public:
    int find (int k, int n, int** ans) {
        if ( ans[n][k] != 0 ) {
            return ans[n][k];
        }
        if ( k == 1 ) {
            ans[n][k] = n;
            return ans[n][k];
        }
        if ( n == 0 ) {
            ans[n][k] = 0;
            return ans[n][k];
        }

        int begin = 1;
        int end = n;
        while ( begin < end ) {
            int mid = (begin+end)/2;
            int t1 = find(k-1, mid-1, ans);
            int t2 = find(k, n-mid, ans);
            if ( t1 > t2 ) {
                end = mid-1;
            } else if ( t1 < t2 ) {
                begin = mid+1;
            } else {
                ans[n][k] = find(k-1, mid-1, ans)+1;
                return ans[n][k];
            }
        }
        int a = find(k-1, begin-1, ans);
        int b = find(k, n-begin, ans);
        if ( begin == 1 || begin == n ) {
            ans[n][k] = a > b ? a+1 : b+1;
        } else if ( a > b ) {
            int c = find(k, n-begin+1, ans);
            ans[n][k] = a > c ? c+1 : a+1;
        } else {
            int d = find(k-1, begin, ans);
            ans[n][k] = b > d ? d+1 : b+1;
        }
        return ans[n][k];
    }

    int superEggDrop(int K, int N) {
        int** ans = new int* [N+1];
        for (int i = 0; i < N+1; ++i) {
            ans[i] = new int[K+1];
            for (int j = 0; j < K+1; ++j) {
                ans[i][j] = 0;
            }
        }
        return find(K, N, ans);
    }
};

提交结果:
在这里插入图片描述
runtime 32ms,自己算是满意了,总之这个题目暂时到这里,以后有时间再写一下其它方法(这个题目方法很多,还有很多更加优秀的方法甚至能够做到O(1)空间复杂度,后续有时间再行研究);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值