887鸡蛋掉落

一、前言

分类:动态规划。

问题来源LeetCode 887 难度:困难。

问题链接:https://leetcode-cn.com/problems/super-egg-drop/

 

二、题目

你将获得 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

提示:

  1. 1 <= K <= 100
  2. 1 <= N <= 10000

 

三、题意分析

题目看了半天没整明白T_T。最后一句是关键,你的目标是确切地知道 F 的值是多少。无论 F 的初始值如何,你确定 F的值的最小移动次数是多少?

可以这样理解:无论F不管是多少,至少要丢多少次鸡蛋?

也可以这样理解:最坏情况下,你至少要扔几次鸡蛋?

 

四、思路分析

思路来源网上,按照自己的理解进行整理一下。

参考:

  1. https://leetcode-cn.com/problems/super-egg-drop/solution/ji-dan-diao-luo-by-leetcode-solution-2/
  2. https://leetcode-cn.com/problems/super-egg-drop/solution/887-by-ikaruga/

 

4.1 第一类解题思路:动态规划 + 二分搜索

我们可以考虑使用动态规划来做这道题,状态可以表示成 (K,N),其中 K 为鸡蛋数,N 为楼层数。当我们从第 X 楼扔鸡蛋的时候:

  • 如果鸡蛋不碎,那么状态变成 (K, N-X),即我们鸡蛋的数目不变,但答案只可能在上方的 N−X 层楼了。也就是说,我们把原问题缩小成了一个规模为 (K,N−X) 的子问题;
  • 如果鸡蛋碎了,那么状态变成 (K−1,X−1),即我们少了一个鸡蛋,但我们知道答案只可能在第 XX 楼下方的 X−1 层楼中了。也就是说,我们把原问题缩小成了一个规模为(K−1,X−1) 的子问题。

这样一来,我们定义 dp(K, N) 为在状态 (K,N) 下最少需要的步数。根据以上分析我们可以列出状态转移方程:

这个状态转移方程是如何得来的呢?对于 dp(K, N) 而言,我们像上面分析的那样,枚举第一个鸡蛋扔在的楼层数 X。由于我们并不知道真正的 F 值,因此我们必须保证 鸡蛋碎了之后接下来需要的步数 和 鸡蛋没碎之后接下来需要的步数 二者的 最大值 最小,这样就保证了在 最坏情况下(也就是无论 F 的值如何) dp(K, N) 的值最小。如果能理解这一点,也就能理解上面的状态转移方程,即最小化max(dp(K-1, X-1), dp(K, N-X))。

复杂度分析

时间复杂度:O(K∗NlogN)。我们需要计算 O(K∗N) 个状态,每个状态计算时需要 O(logN) 的时间进行二分搜索。

空间复杂度:O(K∗N)。我们需要 O(K∗N) 的空间存储每个状态的解。

 

4.2 第二类解题思路:动态规划,问题转换+逆向推到

N 和 F 的关系

  1. N 的定义:使用一栋从 1 到 N 共有 N 层楼的建筑
  2. F 的定义:满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破
  3. 因此得知,F 比 N 多一个 0 层

问题转换

  1. 将问题从: N 个楼层,有 K 个蛋,求最少要扔 T 次,才能保证当 F 无论是 0 <= F <= N 中哪个值,都能测试出来
  2. 转变为:有 K 个蛋,扔 T 次,就可以确定 F 的个数,然后得出 N 个楼层

 

递推公式

  1. F(K,T) 记为有K个鸡蛋,T次扔鸡蛋的机会,可以测试的最大楼层数;
  2. 当扔一个鸡蛋,产生的效果是:【蛋碎了减 1 个,机会减 1 次】 + 【蛋没碎,机会减 1 次】
  3. 通过步骤2,可以获取到递推公式: F(K, T) = F(K - 1, T - 1) + F(K, T - 1)

临界条件

  1. 当只有一个鸡蛋,只能从1楼开始往上扔直到鸡蛋破碎,其中还有一个0楼,也就是一个鸡蛋可以测试最大楼层=T+1,F(1, T) = T + 1
  2. 只有一次扔鸡蛋的机会,等价于只有一个鸡蛋且只能扔一次 F(K+1) = 2。也可以这样理解,只有能扔一次那就从1楼扔下去,破了可以确定0层不会破,1层会破F=2;不破,可以确定0层和1层都不会破F=2,也就是 F(K+1) = 2

 

图:F(K, T),F(K, T)为可以确定的楼层数,包含0层,也就是题意中 N-1 = F(K, T)。

 

五、解决方法

提供了四种解决方法。

方法一:第一类,动态规划 + 二分搜索。时间复杂度:O(K∗NlogN),空间复杂度:O(K∗N)。

方法二: 第二类,自顶向下通过递归解决。时间复杂度:O(K∗N),空间复杂度:O(1)。

class Solution_2
{
public:
    int superEggDrop(int K, int N)
    {
        int T = 1;
        while (calcF(K, T) < N + 1) T++;
        return T;
    }

    int calcF(int K, int T)
    {
        if (T < 1 && K < 1) return 0;	// T 和 K 不能都小于1
        if (K == 1) return T + 1;		// 只有一个鸡蛋,从一楼开始往扔,最多确定楼层为T+1
        if (T == 1) return 2;			// 只扔一次那就从1楼扔下去,可以确定0层和1层
        //if (T == 1 || K == 1) return T + 1;
        return calcF(K - 1, T - 1) + calcF(K, T - 1);
    }
};

代码解析:

  • calcF(K, T),计算f(K,T)值也就是可以确定的楼层数,由于K是固定的,结果随T增加而增加,当可以确定的楼层数大于或等N+1,T即为所求,N+1是因为包含了0层。

方法三:将方法二转为自底向上,非递归解决。时间复杂度:O(K∗N),空间复杂度:O(2N)。

class Solution_3
{
public:
    int superEggDrop(int K, int N)
	{
        if (K <= 0 || N <= 0)
            return 0;
		
        vector<vector<int>> dp(2, vector<int>(K+1, 2));
        vector<int>* pre = &dp[0];
        vector<int>* cur = &dp[1];

        for (int i = 2; i <= N; ++i)
        {
            (*cur)[1] = i + 1;
            for (int j = 2; j <= K; ++j)
                (*cur)[j] = (*pre)[j - 1] + (*pre)[j];

            if ((*cur)[K] > N)
                return i;

            swap(cur, pre);
        }
        return 1;
    }
};

辅助空间说明,图形观察如下, F(K, T) = F(K - 1, T - 1) + F(K, T - 1),上一层结果觉得下一次值,只需要用2*(K+1)个辅助空间即可。

方法四:对方法三空间进行优化。时间复杂度:O(K∗N),空间复杂度:O(N)。

class Solution_4
{
public:
    int superEggDrop(int K, int N)
	{
        if (K <= 0 || N <= 0)
            return 0;

        vector<int> dp(K+1,2);
        for (int i = 2; i <= N; ++i)
        {
            for (int j = K; j >= 2; --j)
                dp[j] += dp[j - 1];
			
           dp[1] = i + 1;
           if (dp[K] > N)
               return i;
        }

        return 1;
    }
};

继续观察这个图发现,从后完前计算辅助空间中数据,只需要一行辅助空间就可以了。

 

六、总结

  1. 正确理解题意;
  2. 两类解题思路都是讲大问题拆分成小问题;
  3. 尤其是第二类解题思路,当扔一下鸡蛋会产生两个结果F(K-1,T-1)和F(K,T-1),这个和《跳台阶》问题很类似;
  4. 动态规划问题最最最重要的是问题拆分(状态转换)找到递推公式。

 

七、完整代码

//==========================================================================
/*
* @file    : 887_SuperEggDrop.h
* @blogs   : 
* @author  : niebingyu
* @date    : 2020/07/08
* @title   : 887.鸡蛋掉落
* @purpose : 你将获得 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
*  
* 提示:
* 1 <= K <= 100
* 1 <= N <= 10000
*
* 来源:力扣(LeetCode)
* 难度:困难
* 链接:https://leetcode-cn.com/problems/super-egg-drop
*/
//==========================================================================
#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;

#define NAMESPACE_SUPEREGGDROP namespace NAME_SUPEREGGDROP {
#define NAMESPACE_SUPEREGGDROPEND }
NAMESPACE_SUPEREGGDROP

// 方法一
// 动态规划 + 二分搜索
// 递推公式
// dp(K, N) = 1 + min(max(dp(K−1, X−1), dp(K, N−X)))
//              1<=x<=N
class Solution_1 
{
public:
    int superEggDrop(int K, int N) 
    {
        return dp(K, N);
    }

private:
    unordered_map<int, int> memo;
    int dp(int K, int N) 
    {
        if (memo.find(N * 100 + K) == memo.end()) 
        {
            int ans;
            if (N == 0) ans = 0;
            else if (K == 1) ans = N;
            else 
            {
                int lo = 1, hi = N;
                while (lo + 1 < hi) 
                {
                    int x = (lo + hi) / 2;
                    int t1 = dp(K-1, x-1);
                    int t2 = dp(K, N-x);

                    if (t1 < t2) lo = x;
                    else if (t1 > t2) hi = x;
                    else lo = hi = x;
                }

                ans = 1 + min(max(dp(K-1, lo-1), dp(K, N-lo)),
                    max(dp(K-1, hi-1), dp(K, N-hi)));
            }

            memo[N * 100 + K] = ans;
        }

        return memo[N * 100 + K];
    }
};

// 方法二:动态规划,问题转换+逆向推到
// 自顶向下,递归解决
// F(K, T) = F(K - 1, T - 1) + F(K, T - 1)
// F(1, T) = T + 1
// F(K, 1) = 2
/*
* N 和 F 的关系
* N 的定义:使用一栋从 1 到 N 共有 N 层楼的建筑
* F 的定义:满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破
* 因此得知,F 比 N 多一个 0 层
*
* 问题转换
* 将问题从: N 个楼层,有 K 个蛋,求最少要扔 T 次,才能保证当 F 无论是 0 <= F <= N 中哪个值,都能测试出来
* 转变为:有 K 个蛋,扔 T 次,求可以确定 F 的个数,然后得出 N 个楼层
*/
class Solution_2
{
public:
    int superEggDrop(int K, int N)
    {
        int T = 1;
        while (calcF(K, T) < N + 1) T++;
        return T;
    }

    int calcF(int K, int T)
    {
        if (T < 1 && K < 1) return 0;	// T 和 K 不能都小于1
        if (K == 1) return T + 1;		// 只有一个鸡蛋,从一楼开始往扔,最多确定楼层为T+1
        if (T == 1) return 2;			// 只扔一次那就从1楼扔下去,可以确定0层和1层
        //if (T == 1 || K == 1) return T + 1;
        return calcF(K - 1, T - 1) + calcF(K, T - 1);
    }
};

// 方法三:动态规划,问题转换+逆向推到
// 自底向上,非递归解决
//F(K, T) = F(K - 1, T - 1) + F(K, T - 1)
//F(1, T) = T + 1
//F(K, 1) = 2
class Solution_3
{
public:
    int superEggDrop(int K, int N)
    {
        if (K <= 0 || N <= 0)
            return 0;
		
        vector<vector<int>> dp(2, vector<int>(K+1, 2));
        vector<int>* pre = &dp[0];
        vector<int>* cur = &dp[1];

        for (int i = 2; i <= N; ++i)
        {
            (*cur)[1] = i + 1;
            for (int j = 2; j <= K; ++j)
                (*cur)[j] = (*pre)[j - 1] + (*pre)[j];

            if ((*cur)[K] > N)
                return i;

            swap(cur, pre);
        }
        return 1;
    }
};

// 方法四:动态规划,问题转换+逆向推到
// 自底向上,非递归解决,对方法三空间优化
// F(K, T) = F(K - 1, T - 1) + F(K, T - 1)
// F(1, T) = T + 1
// F(K, 1) = 2
class Solution_4
{
public:
	int superEggDrop(int K, int N)
	{
        if (K <= 0 || N <= 0)
            return 0;

        vector<int> dp(K+1,2);
        for (int i = 2; i <= N; ++i)
        {
            for (int j = K; j >= 2; --j)
                dp[j] += dp[j - 1];
			
           dp[1] = i + 1;
           if (dp[K] > N)
               return i;
        }

        return 1;
    }
};

以下为测试代码//
// 测试 用例 START
void test(const char* testName, int K, int N, int expect)
{
    Solution_1 s1;
    int result1 = s1.superEggDrop(K, N);

    Solution_2 s2;
    int result2 = s2.superEggDrop(K, N);

    Solution_3 s3;
    int result3 = s3.superEggDrop(K, N);

    Solution_4 s4;
    int result4 = s4.superEggDrop(K, N);

    if (expect == result1 && expect == result2 && expect == result3 && expect == result4)
        cout << testName << ", solution passed." << endl;
    else
        cout << testName << ", solution failed. expect: "<< expect
		<< ", result1: "<< result1 << ", result2: " << result2
		<< ", result3: "<< result3 << ", result4: " << result4 << endl;
}

// 测试用例
void Test1()
{
    int K = 1;
    int N = 1;
    int expect = 1;

    test("Test1()", K, N, expect);
}

void Test2()
{
    int K = 1;
    int N = 2;
    int expect = 2;

    test("Test2()", K, N, expect);
}

void Test3()
{
    int K = 1;
    int N = 3;
    int expect = 3;
    
    test("Test3()", K, N, expect);
}

void Test4()
{
    int K = 3;
    int N = 2;
    int expect = 2;

    test("Test4()", K, N, expect);
}

void Test5()
{
    int K = 3;
    int N = 14;
    int expect = 4;

    test("Test5()", K, N, expect);
}

void Test6()
{
    int K = 3;
    int N = 92;
    int expect = 8;

    test("Test6()", K, N, expect);
}

void Test7()
{
    int K = 7;
    int N = 501;
    int expect = 9;

    test("Test7()", K, N, expect);
}
NAMESPACE_SUPEREGGDROPEND
// 测试 用例 END
//

void SuperEggDrop_Test()
{
    NAME_SUPEREGGDROP::Solution_2 s2;
    for (int t = 10; t <= 20; ++t)
    {
        for (int k = 1; k <= 9; ++k)
        {
            cout << s2.calcF(k, t) << " ";
        }
        cout << endl;
    }
    cout << endl;

    NAME_SUPEREGGDROP::Test1();
    NAME_SUPEREGGDROP::Test2();
    NAME_SUPEREGGDROP::Test3();
    NAME_SUPEREGGDROP::Test4();
    NAME_SUPEREGGDROP::Test5();
    NAME_SUPEREGGDROP::Test6();
    NAME_SUPEREGGDROP::Test7();
}

执行结果:

 

八、拓展一,加深理解

找出N=110,K=5,最优扔鸡蛋楼层可能的一种情况,并进行分析?

  1. 先在K=5,中找最小大于110的值,结果是K=5,T=7;
  2. 最优的结果并不是一种,这里提供其中一种扔鸡蛋的方法,在第N+1 = 57,也就是第56层扔一个鸡蛋;
  3. 如果鸡蛋没有破,此时K=5,T=6,破在N=57-109层中查找,57-109层和0-42层是等价的,f(K=5,T=6)最大可以查找63层(0-62),查找0-42层是没有问题的;
  4. 如果鸡蛋破了,此时K=4,T=6,此时刚好可以查找57层(0-56)满足要求;
  5. 后续重复步骤1直到K=1或者T=1。

这里我们可以进一步理解递推公式: F(K, T) = F(K - 1, T - 1) + F(K, T - 1)

 

九、拓展二,面试问题

有 2 个蛋,用一座 100 层的楼,要使用最少次数测试出蛋几层会碎(F)。
问第一次应该从几层扔。注意不包含0层。

这个问题曾经也遇到过,那时候一头雾水。T.T

从图可以直观的看到第一次扔是在第14层。(1+1)+2+3+4+...+14 = 106

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值