当Rust遇上LeetCode #887. 鸡蛋掉落 [困难]

2020/2/20

题目描述

你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。

你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。

每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。

无论 F 的初始值如何,你确定 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


相关标签

数学
二分查找
动态规划


解题思路

思路:
首先,对本题的题目描述进行进一步解释。假设,你现在在一幢高度为N=4大楼中(处于的楼层为[1, 2, 3, 4])中的第2层,此时你扔下一枚鸡蛋,这会产生两种可能的结果:鸡蛋碎,或是不碎。如果鸡蛋碎了,那么说明在所有n>=2的楼层中扔下的鸡蛋都会碎;若果鸡蛋没碎,那么说明在所有n<2的楼层中,扔的鸡蛋都不会碎,并且你这一次尝试中扔出去的鸡蛋是可以被回收再利用的,也就是说鸡蛋数量k不会-1。我们要找到的就是这个分水岭楼层数F,即从F+1层开始扔的都会碎,1~F层扔的鸡蛋都不会碎。

然后就是如何理解所谓的 “ 无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?” 这句话。换一种说法,其实是在问我们在“最坏的情况下,用有限的鸡蛋最少能确定楼层F的最小尝试次数”。要理解这个问题,我们先来考虑一下在只有一个鸡蛋和有无限鸡蛋情况下不同的扔法。

  1. 如果只存在一个鸡蛋,那么我们只能老老实实从1楼开始一层一层的向上推进,鸡蛋第一次碎的楼层即我们所寻找的楼层数F,此时所谓的最坏情况,即为这层楼的最高层N是我们想要找到的F,搜寻次数 t = N;
  2. 如果存在无限多的鸡蛋,那么将楼层看做为一个性质有序的数列,我们可以采用二分搜索的方法来使用最小的次数找到目标楼层F。此时,搜寻次数为 |log2(N)|+1。

实际情况中,当1 < k < |log2(N)|+1 时,既不能全程采用二分搜索,也不需要从底层一层一层开始找,此时就需要采取混合策略来确定最小的尝试次数了,比如不断的采用二分,当发现鸡蛋数量不足时再逐楼层排查,最坏情况就可以理解为,当正好用完最后一个鸡蛋时,同时排查完楼层F的所有可能情况最小尝试次数对应的便是这些所有可能的最坏情况中,尝试次数最小的一次。此时,就可以用上动态规划来做决策了。

动态规划 (方法一)+ 二分查找

  • 算法:
    按照上述思路,我们已经整理出了第一类动态规划的大致思路。我们用 dp(k, n) 表示在k个鸡蛋和n层楼的条件下,最坏情况下的最小尝试次数 t 。
    可以得到如下递推关系式:
a dp(k, n) = Min{ Max( dp(k-1, i), dp(k, N-1-i) ) + 1, i in 1..N+1 }; 最小尝试次数为,当 i 属于 [1, N] 时,{dp(k-1, i), dp(k, N-1-i) ) + 1} 中的最小值
b dp(1, n) = n; 当 k = 1 时, 最小尝试次数即为楼层高度
c dp(k, n) = log2(n) + 1 当k >= log2(n)+1时,最小尝试次数为二分查找次数

~~~~再加上备忘录消除重叠子问题,本题最基本的解法已经成形了。

~~~~复杂度分析
~~~~函数本身的复杂度就是忽略递归部分的复杂度,这里 dp 函数中有一个 for 循环,所以函数本身的复杂度是 O(N)。
~~~~子问题个数也就是不同状态组合的总数,显然是两个状态的乘积,也就是 O(KN)。
~~~~所以算法的总时间复杂度是 O(K*N^2), 空间复杂度 O(KN)。

  • 二分查找时间优化
    在此之上,我们还可以采用二分查找来加快 a 情况中的最小值
    二分法求山顶值示意图(来源:labuladong)

    如图所示,我们可以通过选择在第 i 楼扔鸡蛋,将整栋大楼分为 [1, i - 1] 和 [ i + 1, N] 两部分。
    其中楼下的部分的最小尝试次数表示为dp(k-1, i-1),楼上部分表示为dp(k, N-i)。
    比较容易证明,前者为单调上升,而后者单调下降,最终我们想要找到的是max(dp(k-1, i-1), dp(k, N-i)) 的最小值,也就是max(dp(k-1, i-1), dp(k, N-i)) 的函数图像的最低点(山顶/底点)。
    这一点可以通过求dp(k-1, i-1)与dp(k, N-i) 两条直线的交点来得到。搜索这个交点的过程便可以通过二分搜索来实现。具体实现见源代码。

    复杂度分析:
    优化后算法的总时间复杂度是 O(K *N *logN), 空间复杂度 O(KN)。效率上比之前的算法 O(KN^2) 要高效一些。

动态规划 (方法二)+ 迭代

  • 算法
    第二种动态规划的思路相较于第一种要难以理解一些。首先我们需要将原问题,即“有k个鸡蛋,N层楼,最坏情况下最少需要测几次”转换为“有k个鸡蛋,测t次,最多可以测完几层楼”,即从 dp(k, n) 转化为 dp(k, t)。当 t 从 1 不断增加,dp(k, t) 第一次 >= N 时,选取的 t 即 dp(k, n) 的答案。

    那么现在问题变成了如何来求取 dp(k, t)。现在想象自己正好处于一栋楼高为 dp(k, t) 的大楼中的某一层,你不知道你的楼下有多少层,也不知道你的楼上有多少层,此时你开始第一次测量,扔下了一枚鸡蛋,如果鸡蛋碎了,你就会去楼下继续测,你手上的鸡蛋数量减少了1,根据 dp 的定义,你最多可以在楼下测完 dp(k-1, t-1) 层楼;如果你的鸡蛋没有碎,呢么你就会去楼上测,那么你最多可以测完 dp(k, t-1) 层楼。现实中这两种情况都可能发生,所以楼上和楼下的楼层都是真实存在,且正好可以被测完的。再加上你所处于的这一层楼,最终得到整栋楼的高度为:
dp(k, t) = dp(k-1, t-1) + dp(k, t-1) + 1

~~~另外,当 k = 1 时,dp (k, t) = t, 即鸡蛋一直不碎,直到最后一次测完;
~~~~~~~~~~~~当 t = 1,k > 0 时,dp (k, t) = 1, 即只测一次只能测一层;

dp(k, t)的求解示意图(来源:labuladong)

  • 复杂度分析:
    得到了递推关系式之后,我们便可以通过迭代或是递归的方法来进行求解,其中迭代法采用自底向上的方法来得到 F。
    在迭代法中,不存在函数调用,只存在 k 与 n 的两个循环,意味着其时间复杂度为O(KN)。
    空间复杂度方面,其需要一个大小为 K*N 的二维数组来存储子问题的结果,O(KN)。

动态规划 (方法二)+ 递归 + 二分查找 + HashMap

  • 算法:上述的动态规划过程可以用递归法来实现,并采用二分来降低时间复杂度,此处我采用了每次取log2处的位置来缩小范围,比直接简单的对半分大部分情况下爱要高效很多,唯一需要注意的是k=1的情况下时间会比对半分来的更多,需要加上额外的条件判断来滤掉这种情况。此外,还采用HashMap来取代二维数组,节约了更多的空间。最终的运行结果及时间与空间消耗在源代码部分有对比。
  • 复杂度分析:
    采用了二分之后,其时间复杂度一般情况下应当接近O(K logN)。
    空间复杂度方面,其需要一个大小为O(K logN)的HashMap来记录。这方面的计算不太确定。

源代码

动态规划 (方法一)+ 二分查找

use std::collections::HashMap;

impl Solution {    
    pub fn super_egg_drop(k: i32, n: i32) -> i32 {
        let mut map: HashMap<(i32, i32), i32> = HashMap::new();

        Self::drop_egg(k, n, &mut map)
    }

    pub fn drop_egg(k: i32, n: i32, m: &mut HashMap<(i32, i32), i32>) -> i32 {
        if let Some(ans) = m.get(&(k, n)) {
            return *ans;
        }
        if k == 1 {
            return n;
        }

        let min = ((n as f32).log2() as i32) + 1; 
        if k >= min {
            return min;
        }
        
        let (mut left, mut right) = (1, n-1);
        let res = loop {
            let mid = left + (right - left) / 2;
            let (broken, unbroken) = (Self::drop_egg(k-1, mid, m), Self::drop_egg(k, n-mid-1, m));
            if right - left <= 1 {
                break std::cmp::max(broken, unbroken) + 1;
            }     
            if broken == unbroken {
                break broken + 1;
            }else if broken < unbroken {
                left = mid;
            }else {
                right = mid;
            }
        };
             
        m.insert((k, n), res);
        res
    }   
}

执行用时 : 60 ms, 在所有 Rust 提交中击败了40.00%的用户
内存消耗 : 2.7 MB, 在所有 Rust 提交中击败了50.00%的用户

输入 10000, 1000000000
输出 30
预期结果 30
执行时间 0 ms
输入 10, 1000000000
执行时间 超时

动态规划 (方法二)+ 迭代

use std::collections::HashMap;

impl Solution {    
    pub fn super_egg_drop(k: i32, n: i32) -> i32 {
        let mut dp: Vec<Vec<i32>> = vec![vec![0;(n+1) as usize];(k+1) as usize];
        let (mut hit, mut call) = (0, 0);
        let (mut _k, mut _t) = (1, 0);
        
        let t = loop {
            _t += 1;
            while _k as i32 <= k {
                call += 1;
                if _k == 1 {
                    dp[_k][_t] = _t as i32;
                }else {
                    dp[_k][_t] = dp[_k][_t-1] + dp[_k-1][_t-1] + 1;
                }
                _k += 1;
            }
            if dp[_k-1][_t] >= n {
                break _t;
            }
            _k = 1;
        };
        t as i32
    }   
}

执行用时 : 0 ms, 在所有 Rust 提交中击败了100.00%的用户
内存消耗 : 5.1 MB, 在所有 Rust 提交中击败了50.00%的用户

输入 1000, 100000
输出 17
预期结果 17
执行时间 36 ms

注:由于整型溢出和内存限制,此处最多只能取到(1000,100000)前后。


动态规划 (方法二)+ 递归 + 二分查找 + HashMap

use std::collections::HashMap;

impl Solution {    
    pub fn super_egg_drop(k: i32, n: i32) -> i32 {
        let mut dp: HashMap<(usize, usize), u64> = HashMap::new();
        let (mut left, mut right) = (1, n+1);
        if k == 1 {
            return n;
        }
        
        let t = loop {
            let mid = left + (right as f32- left as f32).log2() as i32;
            let floor = Self::drop_egg(k as usize, mid as usize, &mut dp);
            if floor > n as u64{
                right = mid;
            }else if floor < n as u64{
                left = mid+1;
            }else {
                break mid;
            }
            if left == right {
                break left;
            }
        };
        t as i32
    }

    pub fn drop_egg(k: usize, t: usize, dp: &mut HashMap<(usize, usize), u64>) -> u64 {
        if let Some(floor) = dp.get(&(k, t)) {
            return *floor;
        }
        if k == 1 || t == 1{
            return t as u64;
        }
        let floor = Self::drop_egg(k, t-1, dp) + Self::drop_egg(k-1, t-1, dp) + 1;
        dp.insert((k, t), floor);
        floor
    }
}

执行用时 : 0 ms, 在所有 Rust 提交中击败了100.00%的用户
内存消耗 : 2.4 MB, 在所有 Rust 提交中击败了50.00%的用户

输入 10000, 1000000000
输出 30
预期结果 30
执行时间 0 ms
输入 1000, 100000
输出 17
预期结果 17
执行时间 0ms
输入 10, 1000000000
输出 40
预期结果 40
执行时间 0 ms

总结

在DP问题中,往往可以通过转换角度,重新寻找状态转移函数的方式来优化算法,另外二分搜索也是常用的工具。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342