【重温经典】鸡蛋掉落

【重温经典】鸡蛋掉落

在这里插入图片描述

背景
  • 这是一道经典的谷歌面试题,本文没有涉及「决策单调性」和「数学法」来解决本题

在这里插入图片描述

方法1:暴力递归
  • 方法3的分析中,可以得出一个结论:需要不断的根据鸡蛋的碎与不碎来向下或者向上继续搜索,递归的最终则是要找到base case:
    • 当前楼层是0,则需要0次尝试
    • 当前楼层是1,则需要1次尝试
    • 当前楼层为n,但鸡蛋的个数为1,需要从1到n层一层一层往上试,尝试的次数即是楼层
public int superEggDrop(int K, int N) {
    return helper(K, N);
}

private int helper(int k, int n) {
    //楼层为0和1的时候
    if (n == 0 || n == 1) return n;
    //鸡蛋只有1个的时候
    if (k == 1) return n;
    int res = Integer.MAX_VALUE / 2;
    for (int i = 1; i <= n; i++) {
        //碎与不碎的情况
        int t = Math.max(helper(k - 1, i - 1), helper(k, n - i)) + 1;
        res = Math.min(res, t);
    }
    return res;
}
  • 上面的普通的暴力递归很显然不能通过,考虑方法2采用记忆化的方式
方法2:自顶向下记忆化递归(Top-down)
		 Integer[][] memo;

        public int superEggDrop(int K, int N) {
            memo = new Integer[K + 1][N + 1];
            return helper(K, N);
        }

        private int helper(int k, int n) {
            if (n == 0 || n == 1) return n;
            if (k == 1) return n;
            if (memo[k][n] != null) return memo[k][n];
            int res = Integer.MAX_VALUE / 2;
            for (int i = 1; i <= n; i++) {
                int t = Math.max(helper(k - 1, i - 1), helper(k, n - i)) + 1;
                res = Math.min(res, t);
            }
            return memo[k][n] = res;
        }
  • 还是TLE了,不过有进步了,方法1过了34个用例,方法2过了74个,需要继续优化
优化
  • 方法3中介绍了二分优化的方式查找楼层, O ( n ) O(n) O(n)的复杂度能退化到 O ( l o g N ) O(logN) O(logN)
Integer[][] memo;

public int superEggDrop(int K, int N) {
    memo = new Integer[K + 1][N + 1];
    return helper(K, N);
}

private int helper(int k, int n) {
    if (n == 0 || n == 1) return n;
    if (k == 1) return n;
    if (memo[k][n] != null) return memo[k][n];
    int lo = 1, hi = n, t = 0;
    int res = Integer.MAX_VALUE / 2;
    while (lo <= hi) {
        int mid = lo + hi >> 1;
        //两部分
        int t1 = helper(k - 1, mid - 1);
        int t2 = helper(k, n - mid);
        t = Math.max(t1, t2) + 1;//去子问题最大
        if (t1 < t2) {
            lo = mid + 1;
        } else {
            hi = mid - 1;
        }
        res = Math.min(res, t);//更新res
    }
    return memo[k][n] = res;//记忆化
}
方法3:自底向上填表DP(Bottom-up)
定义状态

f [ i ] [ j ] f[i][j] f[i][j]​​表示还有 i i i​层楼时并且当前持有的鸡蛋个数为 j j j​​个时,确定出临界楼层需要的最少操作数

  • 注意这里,还有的 i i i层楼的时候,可以往上数,也可以往下数

在这里插入图片描述

状态转移

当在当前楼层 k k k​​扔下一个鸡蛋的时候,有两种结果:「broken」与「not broken

  • 鸡蛋没有碎:当前层 k k k以下都不会碎,因为当前层没有碎,往下的楼层不会碎,因此需要往上找,当前鸡蛋是完好的,往上找便是 f [ i − k ] [ j ] f[i-k][j] f[ik][j]
  • 鸡蛋碎了:当前层 k k k以上都是会碎的,因为楼层更高,当前 k k k层都碎了,我们要找的临界的楼层不在 k k k以上,因此需要往下找,而当前 k k k已经用掉了一个鸡蛋,往下找便是 f [ k − 1 ] [ j − 1 ] f[k-1][j-1] f[k1][j1]

在满足最坏的情况下,取上述两个讨论的最大值,而 k k k​在1和 i i i​之间,在这个整个区间中取最小值,没操作一次需要记录一次操作,执行+1操作

f [ i ] [ j ] = lim ⁡ 1 ≤ k ≤ i m i n ( m a x ( f [ k − 1 ] [ j − 1 ] , f [ i − k ] [ j ] ) + 1 ) f[i][j]=\lim \limits_{1\leq k \leq i}min(max(f[k-1][j-1],f[i-k][j])+1) f[i][j]=1kilimmin(max(f[k1][j1],f[ik][j])+1)​​

初始化
  • f [ N + 1 ] [ K + 1 ] f[N+1][K+1] f[N+1][K+1] 其中 N N N为总的楼层,K为鸡蛋的个数
    • f [ 0 ] [ j ] f[0][j] f[0][j]:当前层是0层,j个鸡蛋,值为0
    • f [ i ] [ 0 ] f[i][0] f[i][0]:当前层从0到N,但没有鸡蛋,没有办法做测试,值为0
    • f [ 1 ] [ j ] f[1][j] f[1][j]:当前层是1层,但是手里有j个鸡蛋, j ≥ 0 j\geq0 j0​,只需要扔1次即可确定出结果
    • f [ i ] [ 1 ] f[i][1] f[i][1]:当前层从0到N,只有一个鸡蛋,这种时候最好的方式是从0到N层每一层挨个试,值为楼层的高度
实现
int INF = Integer.MAX_VALUE / 2;

public int superEggDrop(int K, int N) {
    int[][] f = new int[N + 1][K + 1];
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= K; j++) {
            f[i][j] = INF;
        }
    }
    f[0][0] = 0;//0层0个鸡蛋
    for (int i = 1; i <= N; i++) f[i][1] = i;//1层以上1个鸡蛋
    for (int j = 1; j <= K; j++) f[1][j] = 1;//1层超过1个鸡蛋
    for (int i = 2; i <= N; i++) {
        for (int j = 2; j <= K; j++) {
            for (int k = 1; k <= i; k++) {
                //一般情况
                f[i][j] = Math.min(f[i][j], Math.max(f[k - 1][j - 1], f[i - k][j]) + 1);
            }
        }
    }
    return f[N][K];//返回N层K个鸡蛋的结果
}
  • 上述实现TLE了,证明思路是对的,开始优化
优化
  • 上述的做法,时间复杂度是 O ( N 2 ∗ K ) O(N^2*K) O(N2K)​​,对于每个楼层来说,需要 O ( N ) O(N) O(N)​​的时间,是线性的,需要优化。

  • f [ k ] [ j ] f[k][j] f[k][j]​​​​​​​ 是一个关于 k 的单调递增函数,也就是说在鸡蛋数 j 固定的情况下,楼层数k越多,需要的操作数会越来越多。

重新再审视一遍状态转移方程

f [ i ] [ j ] = lim ⁡ 1 ≤ k ≤ i m i n ( m a x ( f [ k − 1 ] [ j − 1 ] , f [ i − k ] [ j ] ) + 1 ) f[i][j]=\lim \limits_{1\leq k \leq i}min(max(f[k-1][j-1],f[i-k][j])+1) f[i][j]=1kilimmin(max(f[k1][j1],f[ik][j])+1)

  • 在上述的状态转移方程中,第一项 T 1 ( x ) = f [ x − 1 ] [ j − 1 ] T_1(x) = f[x-1][j-1] T1(x)=f[x1][j1]​是一个随x增加而单调递增函数,第二项 T 2 ( x ) = f [ i − x ] [ j ] T_2(x) = f[i-x][j] T2(x)=f[ix][j]

是一个随x增加而单调递减函数

在这里插入图片描述

简言之,需要找到两项在交叉处的点,筛选掉不要的

实现
int INF = Integer.MAX_VALUE / 2;

public int superEggDrop(int K, int N) {
    int[][] f = new int[N + 1][K + 1];
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= K; j++) {
            f[i][j] = INF;
        }
    }
    f[1][0] = 0;
    for (int i = 1; i <= N; i++) f[i][1] = i;
    for (int j = 1; j <= K; j++) f[1][j] = 1;
    for (int i = 2; i <= N; i++) {
        for (int j = 2; j <= K; j++) {
            int l = 1, r = i;
            while (l < r) {
                int m = l + (r - l + 1) / 2;
                //二分,比较t1部分和t2部分
                int t1 = f[m - 1][j - 1], t2 = f[i - m][j];
                if (t1 > t2) {
                    r = m - 1;
                } else {
                    l = m;
                }
            }
            f[i][j] = Math.min(f[i][j], Math.max(f[l - 1][j - 1], f[i - l][j]) + 1);
        }
    }
    return f[N][K];
}
FollowUp
  • 利用上面的结论就很容易解决下面的这个问题了「双蛋掉落」
    在这里插入图片描述
   public int twoEggDrop(int n) {
        return superEggDrop(2,n);
    }

Reference

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值