给你两个长度为 n 的整数数组,fruits 和 baskets,其中 fruits[i] 表示第 i 种水果的 数量,baskets[j] 表示第 j 个篮子的 容量。
你需要对 fruits 数组从左到右按照以下规则放置水果:
- 每种水果必须放入第一个 容量大于等于 该水果数量的 最左侧可用篮子 中。
- 每个篮子只能装 一种 水果。
- 如果一种水果 无法放入 任何篮子,它将保持 未放置。
返回所有可能分配完成后,剩余未放置的水果种类的数量。
示例 1
输入: fruits = [4,2,5], baskets = [3,5,4]
输出: 1
解释:
fruits[0] = 4放入baskets[1] = 5。fruits[1] = 2放入baskets[0] = 3。fruits[2] = 5无法放入baskets[2] = 4。
由于有一种水果未放置,我们返回 1。
示例 2
输入: fruits = [3,6,1], baskets = [6,4,7]
输出: 0
解释:
fruits[0] = 3放入baskets[0] = 6。fruits[1] = 6无法放入baskets[1] = 4(容量不足),但可以放入下一个可用的篮子baskets[2] = 7。fruits[2] = 1放入baskets[1] = 4。
由于所有水果都已成功放置,我们返回 0。
3477. 水果成篮 II 提示:
n == fruits.length == baskets.length1 <= n <= 1001 <= fruits[i], baskets[i] <= 1000
3479. 水果成篮 III 提示:
n == fruits.length == baskets.length1 <= n <= 10^51 <= fruits[i], baskets[i] <= 10^9
首先,对于数据量较小的 第 3477 题 ,根据题目要求,直接枚举并比对水果数量 i 与篮子容量 j 即可。
解题步骤
1. 初始化:创建数组 used 记录每个篮子的初始状态为未使用 [False] * n 。
2. 处理每个水果:
外层循环:遍历每个水果。
内层循环:从左到右遍历每个篮子,找到第一个未使用的、容量>水果数量的篮子,标记该篮子已被使用、当前水果已被放置。
3. 统计未放置水果:内层循环遍历完所有篮子都没找到可用篮子,标记为未放置 unplaced += 1 。
4. 返回结果:遍历完成,返回最终结果。
复杂度分析
时间复杂度:O(n²),其中n为水果/篮子数量。最坏情况下对每个水果需要扫描所有篮子。
空间复杂度:O(n),创建用于存储篮子状态的数组。
class Solution:
def numOfUnplacedFruits(self, fruits: List[int], baskets: List[int]) -> int:
n = len(fruits)
used = [False] * n
unplaced = 0
for i in range(n):
placed = False
for j in range(n):
if baskets[j] >= fruits[i] and not used[j]:
used[j] = True
placed = True
break
if not placed:
unplaced += 1
return unplaced
执行用时33ms,消耗内存17.4MB。
接下来,对于 第 3479 题,由于数据量庞大(n = 10^5), 如果暴力匹配会导致运行超时。于是引入以下两种方法:
方法一、线段树 + 二分
基本思路与上述解题步骤一致,不同的是在遍历水果前,以篮子数组为基础,构建维护区间最大值的线段树,便于快速查找并更新第一个满足条件的篮子。
解题步骤
1. 线段树类 SegmentTree :
(1)空间分配: self.max = [0] * (2 ) ,确保树结构完整。
(2)初始化:从索引 1 递归构建区间为 [0, n-1] 的线段树。
(3)构建线段树:到达叶子节点直接赋值;到达非叶子节点,分别递归构建左、右子树,并更新当前节点值。
(4)定义核心操作:查找首个 >= fruit 的篮子并更新。终止条件为:
当前区间最大值 < fruit ,返回 -1 (无解);
或到达叶子节点,标记 -1 (已使用),返回下标。
2. 主逻辑类 Solution :
(1)初始化线段树,使用 baskets 作为原始数据。
(2)遍历每种水果 fruit :调用 find_and_update 查找符合条件的篮子,返回 -1 时计数。
(3)返回未放置水果的总数 unplaced 。
复杂度分析
时间复杂度:O(n log n),其中n为水果/篮子数量,总建树时间为 O(n) ,主循环时间为 O(n log n) 。
空间复杂度:O(n),线段树存储空间 O(n) ,递归栈空间 O(log n) ,输入存储空间 O(2n)。
class SegmentTree:
def __init__(self, baskets: List[int]):
n = len(baskets)
self.max = [0] * (4 * n) # 分配线段树空间(4*n大小)
self.baskets = baskets
self.n = n
self.build(baskets, 1, 0, n - 1) # 初始化:从根节点(索引1)递归构建区间[0, n-1]
def build(self, node, left, right):
# 递归构建线段树
if left == right: # 叶子节点
self.max[node] = self.baskets[left] # 直接赋值
return
mid = (left + right) // 2 # 区间中点
self.build(node * 2, left, mid) # 构建左子树
self.build(node * 2 + 1, mid + 1, right) # 构建右子树
# 更新当前节点的最大值
self.max[node] = max(self.max[node * 2], self.max[node * 2 + 1])
def find_and_update(self, node, left, right, fruit):
# 查找并更新第一个可用篮子
if self.max[node] < fruit: # 当前区间无解
return -1
# 找到目标叶子节点(单个)
if left == right:
self.max[node] = -1 # 标记篮子为已使用
return left # 返回篮子索引
mid = (left + right) // 2
result = -1
# 优先搜索左子树(保证找到最左侧篮子)
if self.max[node * 2] >= fruit:
result = self.find_and_update(node * 2, left, mid, fruit)
# 左子树没找到再搜索右子树
if result == -1 and self.max[node * 2 + 1] >= fruit:
result = self.find_and_update(node * 2 + 1, mid + 1, right, fruit)
# 更新当前节点的最大值
self.max[node] = max(self.max[node * 2], self.max[node * 2 + 1])
return result
class Solution:
def numOfUnplacedFruits(self, fruits, baskets):
tree = SegmentTree(baskets) # 初始化线段树(篮子容量)
unplaced = 0
# 遍历每种水果
for fruit in fruits:
# 尝试查找并更新可用篮子
if tree.find_and_update(1, 0, len(baskets) - 1, fruit) == -1: # 无法放置
unplaced += 1
return unplaced
执行用时2.167s,消耗内存38.90MB。
Note: 为什么给线段树分配的空间为 4n ?
完全二叉树中, n 个叶子节点最多需要 4n 空间,实际需要 2m ( m 为不小于 n 的最小2的幂)。可以验证, 2m 是 4n 的上界,二者渐进等价。
# 安全空间分配(保守估计)
self.max = [0] * (4 * n)
# 精确空间分配(空间优化)
self.max = [0] * 2 << (n-1).bit_length() # 上述代码执行耗时2.103s,消耗空间36.42MB
(n-1).bit_length() 计算表示整数 (n-1) 所需的最小二进制位数,例如:
- n = 1,则 n - 1 = 0,二进制为 0 ,因而 (n-1).bit_length() = 0 ;
- n = 2,则 n - 1 = 1,二进制为 1 ,因而 (n-1).bit_length() = 1 ;
- n = 3,则 n - 1 = 2,二进制为 10 ,因而 (n-1).bit_length() = 2 ;
- n = 4,则 n - 1 = 3,二进制为 11 ,因而 (n-1).bit_length() = 2 。
2 << k ( k 为位长)等价于 2 ^ {k + 1} ,例如:
- k = 0,即 (n-1).bit_length() = 0 ,此时 2 << (n-1).bit_length() = 2 ^ {0+1} = 2;
- k = 1,即 (n-1).bit_length() = 1 ,此时 2 << (n-1).bit_length() = 2 ^ {1+1} = 4;
- k = 2,即 (n-1).bit_length() = 2 ,此时 2 << (n-1).bit_length() = 2 ^ {2+1} = 8。
方法二、分块法
将 baskets 数组分为 m 块,满足 √n = m ,其中 n 为数组的长度。同时,维护块上的最大值 max_vals ,表示当前块中最大的篮子容量。可以看到,其基本思想与上述方法类似,都是“分而治之”。
复杂度分析
时间复杂度:O(n√n)。每个水果处理最多遍历 O(√n) 个块,每个块内扫描最多 O(√n) 个元素,删除和更新最大值操作也是 O(√n)。
空间复杂度:O(n),用于存储分块信息。
class Solution:
def numOfUnplacedFruits(self, fruits, baskets):
n = len(baskets)
m = isqrt(n) # 计算块大小
block_count = (n + m - 1) // m # 计算分块数
# 初始化分块结构
# blocks 存储每个块的篮子信息(原始下标、容量)
blocks = [[] for _ in range(block_count)]
# max_vals 存储每个块中未被占用篮子的最大容量
max_vals = [-10**18] * block_count
# 填充分块结构,根据下标i计算所属块索引
for i in range(n):
block_idx = i // m
blocks[block_idx].append((i, baskets[i])) # 将篮子信息添加到对应块
if baskets[i] > max_vals[block_idx]:
max_vals[block_idx] = baskets[i] # 更新当前块的最大容量
#初始化未放置水果计数器
count_unplaced = 0
# 遍历每种水果,标记是否找到可用篮子
for f in fruits:
found = False
# 按块遍历,不满足条件直接跳过(加速查找)
for block_idx in range(block_count):
if max_vals[block_idx] < f:
continue
# 满足条件的进行块内顺序遍历
j = 0
while j < len(blocks[block_idx]):
idx, cap = blocks[block_idx][j] # 获取当前篮子的原始下标
if cap >= f:
del blocks[block_idx][j] # 检查篮子容量是满足需求
# 若找到可用篮子,需移除该篮子
if cap == max_vals[block_idx]:
if blocks[block_idx]:
max_vals[block_idx] = max(c for _, c in blocks[block_idx]) # 如果移除的是最大容量篮子,重新计算该块的最大容量
else:
max_vals[block_idx] = -10**18 # 否则将该块内最大值设为极小值
found = True # 标记当前水果已被放置
break # 跳出当前块循环
else: # 如果当前篮子容量不足,继续检查下一个篮子
j += 1
if found:
break # 如果已被放置在当前块,跳出块遍历循环
if not found:
count_unplaced += 1 # 遍历所有块仍未被放置,增加计数
return count_unplaced # 返回最终未被放置的水果计数
执行用时4.435s,消耗内存41.54MB。
2025年8月6日
本文参考
力扣官方题解 链接:https://leetcode.cn/problems/fruits-into-baskets-iii/solutions/3737092/shui-guo-cheng-lan-iii-by-leetcode-solut-zlvd/ 来源:力扣(LeetCode) 著作权归作者所有。
灵茶山艾府 链接:https://leetcode.cn/problems/fruits-into-baskets-iii/solutions/3603049/xian-duan-shu-er-fen-pythonjavacgo-by-en-ssqf/ 来源:力扣(LeetCode) 著作权归作者所有。
945

被折叠的 条评论
为什么被折叠?



