特别鸣谢:来自夸夸群的 醉笑陪公看落花@知乎,王不懂不懂@知乎,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 下标指向的值