Leetcode 887. 鸡蛋掉落 动态规划

题目是这样:你面前有一栋从 1 到NN层的楼,然后给你K个鸡蛋(K至少为 1)。现在确定这栋楼存在楼层0 <= F <= N,在这层楼将鸡蛋扔下去,鸡蛋恰好没摔碎(高于F的楼层都会碎,低于F的楼层都不会碎)。现在问你,最坏情况下,你至少要扔几次鸡蛋,才能确定这个楼层F呢?

PS:F 可以为 0,比如说鸡蛋在 1 层都能摔碎,那么 F = 0。

也就是让你找摔不碎鸡蛋的最高楼层F,但什么叫「最坏情况」下「至少」要扔几次呢?我们分别举个例子就明白了。

比方说现在先不管鸡蛋个数的限制,有 7 层楼,你怎么去找鸡蛋恰好摔碎的那层楼?

最原始的方式就是线性扫描:我先在 1 楼扔一下,没碎,我再去 2 楼扔一下,没碎,我再去 3 楼……

以这种策略,最坏情况应该就是我试到第 7 层鸡蛋也没碎(F = 7),也就是我扔了 7 次鸡蛋。

现在你应该理解什么叫做「最坏情况」下了,鸡蛋破碎一定发生在搜索区间穷尽时,不会说你在第 1 层摔一下鸡蛋就碎了,这是你运气好,不是最坏情况。

现在再来理解一下什么叫「至少」要扔几次。依然不考虑鸡蛋个数限制,同样是 7 层楼,我们可以优化策略。

最好的策略是使用二分查找思路,我先去第(1 + 7) / 2 = 4层扔一下:

如果碎了说明F小于 4,我就去第(1 + 3) / 2 = 2层试……

如果没碎说明F大于等于 4,我就去第(5 + 7) / 2 = 6层试……

以这种策略,最坏情况应该是试到第 7 层鸡蛋还没碎(F = 7),或者鸡蛋一直碎到第 1 层(F = 0)。然而无论那种最坏情况,只需要试log7向上取整等于 3 次,比刚才的 7 次要少,这就是所谓的至少要扔几次。

PS:这有点像 Big O 表示法计算算法的复杂度。

实际上,如果不限制鸡蛋个数的话,二分思路显然可以得到最少尝试的次数,但问题是,现在给你了鸡蛋个数的限制K,直接使用二分思路就不行了

比如说只给你 1 个鸡蛋,7 层楼,你敢用二分吗?你直接去第 4 层扔一下,如果鸡蛋没碎还好,但如果碎了你就没有鸡蛋继续测试了,无法确定鸡蛋恰好摔不碎的楼层F了。这种情况下只能用线性扫描的方法,算法返回结果应该是 7。

有的读者也许会有这种想法:二分查找排除楼层的速度无疑是最快的,那干脆先用二分查找,等到只剩 1 个鸡蛋的时候再执行线性扫描,这样得到的结果是不是就是最少的扔鸡蛋次数呢?

很遗憾,并不是,比如说把楼层变高一些,100 层,给你 2 个鸡蛋,你在 50 层扔一下,碎了,那就只能线性扫描 1~49 层了,最坏情况下要扔 50 次。

如果不要「二分」,变成「五分」「十分」都会大幅减少最坏情况下的尝试次数。比方说第一个鸡蛋每隔十层楼扔,在哪里碎了第二个鸡蛋一个个线性扫描,总共不会超过 20 次。

最优解其实是 14 次。最优策略非常多,而且并没有什么规律可言。

/*
 * @lc app=leetcode.cn id=887 lang=cpp
 *
 * [887] 鸡蛋掉落
 *
 * https://leetcode-cn.com/problems/super-egg-drop/description/
 *
 * algorithms
 * Hard (28.88%)
 * Likes:    622
 * Dislikes: 0
 * Total Accepted:    43K
 * Total Submissions: 148.9K
 * Testcase Example:  '1\n2'
 *
 * 给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
 * 
 * 已知存在楼层 f ,满足 0  ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
 * 
 * 每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1
 * )。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
 * 
 * 请你计算并返回要确定 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
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 1 
 * 
 * 
 */

1、基础思路

labuladong

我们在第i层楼扔了鸡蛋之后,可能出现两种情况:鸡蛋碎了,鸡蛋没碎。注意,这时候状态转移就来了

如果鸡蛋碎了,那么鸡蛋的个数K应该减一,搜索的楼层区间应该从[1..N]变为[1..i-1]i-1层楼;

如果鸡蛋没碎,那么鸡蛋的个数K不变,搜索的楼层区间应该从 [1..N]变为[i+1..N]N-i层楼。

递归的 base case 很容易理解:当楼层数N等于 0 时,显然不需要扔鸡蛋;当鸡蛋数K为 1 时,显然只能线性扫描所有楼层:

n=0,return0;k=1,return n;

可以正常解决,但是会超时。也是所有优化思路的基础

class Solution {
public:
    int superEggDrop(int k, int n) {
        vector<vector<int>> dp(k+1,vector<int>(n+1,-1));
        return find(k,n,dp);
    }
    int find(int k,int n, vector<vector<int>> &dp){
        if(k==1) return n;
        if(n==0) return 0;
        if(dp[k][n]!=-1) return dp[k][n];
        int res=INT_MAX;
        for(int i=1;i<=n;++i){
            res=min(res,max(find(k-1,i-1,dp),find(k,n-i,dp))+1);
        }
        dp[k][n]=res;
        return res;
    }
};

 2、优化思路-二分查找

labuladong

注意dp(K - 1, i - 1)dp(K, N - i)这两个函数,其中i是从 1 到N单增的,如果我们固定KN把这两个函数看做关于i的函数,前者随着i的增加应该也是单调递增的,而后者随着i的增加应该是单调递减的

注意dp(K - 1, i - 1)dp(K, N - i)这两个函数,其中i是从 1 到N单增的,如果我们固定KN把这两个函数看做关于i的函数,前者随着i的增加应该也是单调递增的,而后者随着i的增加应该是单调递减的

 

 

 这时候求二者的较大值,再求这些最大值之中的最小值,其实就是求这两条直线交点,也就是红色折线的最低点嘛

 也就是每次不遍历,而是使用二分查找来找到交点值

这个思路可以通过case

class Solution {
public:
    int superEggDrop(int k, int n) {
        vector<vector<int>> dp(k+1,vector<int>(n+1,-1));
        return find(k,n,dp);
    }
    int find(int k,int n, vector<vector<int>> &dp){
        if(k==1) return n;
        if(n==0) return 0;
        if(dp[k][n]!=-1) return dp[k][n];
        int res=INT_MAX;
        int lo=1,hi=n;
        while(lo<=hi){
            int mid=(lo+hi)/2;
            int broken=find(k-1,mid-1,dp);
            int no_broken=find(k,n-mid,dp);
            if(broken<no_broken){
                lo=mid+1;
                res=min(res,no_broken+1);
            }
            else{
                hi=mid-1;
                res=min(res,broken+1);
            }
        }
        dp[k][n]=res;
        return res;
    }
};

 

posted @ 2021-05-06 16:38  鸭子船长  阅读(169)  评论(0编辑  收藏  举报