leecode 1884,887 高楼扔鸡蛋 -二分法的一般框架

特别鸣谢:来自夸夸群的 醉笑陪公看落花@知乎王不懂不懂@知乎QFIUNE@csdn
感谢醉笑陪公看落花@知乎 倾囊相授,感谢小伙伴们督促学习,一起进步、

1884. 鸡蛋掉落-两枚鸡蛋

给你 2 枚相同 的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都 会碎 ,从 f 楼层或比它低 的楼层落下的鸡蛋都 不会碎 。

每次操作,你可以取一枚 没有碎 的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

示例 1:

输入:n = 2
输出:2
解释:我们可以将第一枚鸡蛋从 1 楼扔下,然后将第二枚从 2 楼扔下。
如果第一枚鸡蛋碎了,可知 f = 0;
如果第二枚鸡蛋碎了,但第一枚没碎,可知 f = 1;
否则,当两个鸡蛋都没碎时,可知 f = 2。

示例 2:

输入:n = 100
输出:14

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/egg-drop-with-2-eggs-and-n-floors

在这里插入图片描述

在这里插入图片描述

  • 不同策略扔鸡蛋的时候,找操作次数最小的策略 min
  • 每一次操作之后,有两种情况,选择最坏情况 max
def eggTwo(N):
    '''
    :param N: 楼层总数
    :return:
    '''
    dp = [[float('inf') for i in range(3)] for j in range(N + 1)]
    dp[0][2] = 0
    for i in range(N):
        dp[i][1] = i
    for n in range(1, N + 1):
        for i in range(1, n + 1):
            dp[n][2] = min(dp[n][2], max(dp[i - 1][1], dp[n - i][2]) + 1)  # (1<=i<=n)
    return dp[N][2]

887. 鸡蛋掉落

给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。

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

每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

请你计算并返回要确定 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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/super-egg-drop

在上题的基础上,增加鸡蛋个数的循环

解法1-动态规划+三重for循环

'''
K个鸡蛋,超时
'''
def eggK(N,K):
    dp = [[float('inf') for i in range(K+1)] for j in range(N + 1)]
    dp[0][K] = 0
    for i in range(N):
        dp[i][1] = i
    for k in range(K):
        dp[0][k] = 0
    for n in range(2, N + 1):
        for i in range(1, n + 1):
            for k in range(1,K+1):
                dp[n][k] = min(dp[n][k], max(dp[i - 1][k-1], dp[n - i][k]) + 1)  # (1<=i<=n)
    return dp[N][K]

由于三重循环导致了超时,观察一下循环部分是否可以优化

带备忘录的递归形式

这里把扔鸡蛋问题写成一个带备忘录的递归函数形式,简化描述

memor = {}
def eggK(n,k):
    if (n,k) in memor:return memor[(n,k)]
    if k == 1 :
        memor[(n,k)] = n
        return n
    if n == 1:
        memor[(n,k)] = 1
        return 1
    tp = float('inf')
    for i in range(1,n+1):
        tp = min(tp,max(eggK(n-i,k),eggK(i-1,k-1))+1)
    memor[(n,k)] = tp
    return tp

把递归修改为自底向上

动态规划代码相当于自底向上求备忘录里面的内容

'''
自底向上
'''
memor = {}

def eggK(n,k):
    for j in range(n):
        for i in range(1, k + 1):
            eggK(j,i)
    return eggK(n, k)
def doEggK(n,k):
    if (n,k) in memor:return memor[(n,k)]
    if k == 1 or n<k/2:
        memor[(n,k)] = n
        return n
    tp = float('inf')
    for i in range(1,n+1):
        tp = min(tp,max(memor[(n-i,k)],memor[(i-1,k-1)])+1)
    memor[(n,k)] = tp
    return tp

解法2-用二分查找优化

分析

优化方向

  • 每一次操作之后,有两种情况,选择最坏情况 max

  • 单调性1,随着楼层增高,最小操作次数是增加或不变
    在这里插入图片描述
    横轴表示楼层高度,纵轴表示最少操作次数。上图中的每个子图表示,在给定鸡蛋个数k下,随着楼层增高,最小操作次数的变化

  • 单调性2,随着可以使用的鸡蛋个数增加,最小操作次数减少或不变
    在这里插入图片描述
    横轴表示鸡蛋个数,纵轴表示最少操作次数。上图中的每个子图表示,在给定楼层n的条件下,随着鸡蛋个数增加,最小操作次数的变化

tp = min(tp,max(eggK(n-i,k),eggK(i-1,k-1))+1)

在这里插入图片描述

f(n) = eggK(n,k) 代表n层楼有k个鸡蛋的时候,最坏情况最小操作次数,单增

f1(i) = eggK(n-i,k) 单减
f2(i) = eggK(i-1,k-1) 单增

需要求的是i 取所有可能值(1<=i<=n)的时候, tp = min(max(f1(i),f2(i)))
即是求:

min(max(f1(1),f2(1)),max(f1(2),f2(2)),...max(f1(i),f2(i)),...max(f1(n),f2(n)))

max(f1(i),f2(i)) 转换为一个单调函数

f1(i) = eggK(n-i,k) 单减
f2(i) = eggK(i-1,k-1) 单增
y = f2(i) - f1(i) 单增

y>=0 max(eggK(n-i,k),eggK(i-1,k-1)) = eggK(i-1,k-1) = f2(i)
y<0 max(eggK(n-i,k),eggK(i-1,k-1)) = eggK(n-i,k) = f1(i)

理论上y= 0时 能得到 min(max(f1(i),f2(i)))
实际上i 是一个离散的变量,因此需要找一个i使y最接近0

在这里插入图片描述

二分查找 tips

  • 待查找部分是一个有序的区间
  • 时间复杂度O(n^2) => O(nlog2(n))
  • for (i in range(n)) ⇒ while(start<end)

在y = f1(i) - f2(i) 这个单增函数上,找某个i 使 y接近0
当y>0 时,移动end
当y<0 时,移动start

即是用 二分法优化下面这段for循环代码

for i in range(1,n+1):
        tp = min(tp,max(eggK(n-i,k),eggK(i-1,k-1))+1)
start = 2
end = n
while (start <= end):
    global count #22853
    count += 1
    mid = (start + end) // 2
    y = eggK(mid - 1,k - 1) - eggK(n - mid,k)
    if y > 0:
        end = mid - 1
    elif y < 0:
        start = mid + 1
    else:
        start = end = mid
        break
tp = min(max(eggK(start - 1,k - 1), eggK(n - start,k)),
               max(eggK(end - 1,k - 1), eggK(n - end,k))) + 1

完整实现代码

count =0
memor = {}
def eggK(n,k):
    if (n,k) in memor:return memor[(n,k)]
    if k == 1 or n== 0:
        memor[(n,k)] = n
        return n
    if n == 1:
        memor[(n,k)] = 1
        return 1
    start = 2
    end = n
    while (start <= end):
        global count #22853
        count += 1
        mid = (start + end) // 2
        y = eggK(mid - 1,k - 1) - eggK(n - mid,k)
        if y > 0:
            end = mid - 1
        elif y < 0:
            start = mid + 1
        else:
            start = end = mid
            break
    tp = min(max(eggK(start - 1,k - 1), eggK(n - start,k)),
                   max(eggK(end - 1,k - 1), eggK(n - end,k))) + 1
    memor[(n,k)] = tp
    return tp

在动态规划代码中实现二分查找

待优化代码片段

for i in range(1, n + 1):
            for k in range(1,K+1):
                dp[n][k] = min(dp[n][k], max(dp[i - 1][k-1], dp[n - i][k]) + 1)  # (1<=i<=n)

优化之后的代码片段

for k in range(2, K + 1):
	if k >n:
	    dp[n][k]=dp[n][n]
	    break
	start = 2
	end = n
	while(start <= end):
	    global count_dp #98248
	    count_dp +=1
	    mid = (start+end)//2
	    y = dp[mid - 1][k-1] - dp[n - mid][k]
	    if y>0:
	        end = mid-1
	    elif y<0:
	        start = mid + 1
	    else:
	        start = end = mid
	        break
	dp[n][k] = min(max(dp[start - 1][k-1],dp[n - start][k]),
	               max(dp[end - 1][k-1],dp[n - end][k])) +1

完整实现代码

'''
二分 K个鸡蛋
'''
count_dp =0
def eggK_dp(N,K):
    dp = [[float('inf') for i in range(K+1)] for j in range(N + 1)]
    dp[0][K] = 0
    for i in range(N+1):
        dp[i][1] = i
    for k in range(K):
        dp[0][k] = 0
        dp[1][k] = 1
    for n in range(2, N + 1):
        for k in range(2, K + 1):
            if k >n:
                dp[n][k]=dp[n][n]
                break
            start = 2
            end = n
            while(start <= end):
                global count_dp #98248
                count_dp +=1
                mid = (start+end)//2
                y = dp[mid - 1][k-1] - dp[n - mid][k]
                if y>0:
                    end = mid-1
                elif y<0:
                    start = mid + 1
                else:
                    start = end = mid
                    break
            dp[n][k] = min(max(dp[start - 1][k-1],dp[n - start][k]),
                           max(dp[end - 1][k-1],dp[n - end][k])) +1
    # return dp[N][K]
    return dp[N][K]

解法3-逆向思考,求t能检验的楼层

参考官方解法
给定楼层数目和鸡蛋个数,求最坏情况最小操作次数 => 给定操作次数,和鸡蛋个数,最多可以检测多少层楼
dp数组定义
转移函数

f(t,k)=1+f(t−1,k−1)+f(t−1,k)

在这里插入图片描述

class Solution:
    def superEggDrop(self, k: int, n: int) -> int:
        return eggK2(n,k)
memor = {}
def eggK2(n,k):
    for t in range(1,n+1):
        cn = doEggK2(t, k)
        if cn>=n:return t
def doEggK2(t,k):
    if (t,k) in memor:return memor[(t,k)]
    if t==1 or k==1:return t
    cn = 1+doEggK2(t-1,k-1)+doEggK2(t-1,k)
    memor[(t, k)] = cn
    return cn

在这里插入图片描述

二分查找的一般框架

二分查找 tips

  • 待查找部分是一个有序的区间
  • 时间复杂度O(n^2) => O(nlog2(n))
  • for (i in range(n)) ⇒ while(start<end)
  • 最后查找到的值在start ,end ,和mid 附近
start = 2
end = n
while (start <= end):
    mid = (start + end) // 2
    y = ...
    if y > 0:
        end = mid - 1
    elif y < 0:
        start = mid + 1
	else: ...
# handle start ,end ,和 mid 下标指向的值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值