zoukankan      html  css  js  c++  java
  • 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
    2'
     *
     * 给你 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;
        }
    };
    联系方式:emhhbmdfbGlhbmcxOTkxQDEyNi5jb20=
  • 相关阅读:
    lamp----6 实现虚拟主机ssl安全
    lamp-----5 apache虚拟主机实现,发布多个独立站点
    lamp----4 虚拟目录
    lamp----3 访问控制
    Apache配置反向代理、负载均衡和集群(mod_proxy方式)
    lamp-------3 userdir发布用户站点
    lamp------2 发布站点
    [转]KDE/QT与GNOME/GTK比较
    为什么会有文字聊天
    [转]gdb结合coredump定位崩溃进程
  • 原文地址:https://www.cnblogs.com/zl1991/p/14735806.html
Copyright © 2011-2022 走看看