高级农民
- 积分
- 4089
- 大米
- 颗
- 鳄梨
- 个
- 水井
- 尺
- 蓝莓
- 颗
- 萝卜
- 根
- 小米
- 粒
- 学分
- 个
- 注册时间
- 2017-1-22
- 最后登录
- 1970-1-1
|
LC. 3477. Fruits Into Baskets II
Easy
Topics
conpanies icon
Companies
Hint
You are given two arrays of integers, fruits and baskets, each of length n, where fruits[i] represents the quantity of the ith type of fruit, and baskets[j] represents the capacity of the jth basket.
From left to right, place the fruits according to these rules:
Each fruit type must be placed in the leftmost available basket with a capacity greater than or equal to the quantity of that fruit type.
Each basket can hold only one type of fruit.
If a fruit type cannot be placed in any basket, it remains unplaced.
Return the number of fruit types that remain unplaced after all possible allocations are made.
Example 1:
Input: fruits = [4,2,5], baskets = [3,5,4]
Output: 1
Explanation:
fruits[0] = 4 is placed in baskets[1] = 5.
fruits[1] = 2 is placed in baskets[0] = 3.
fruits[2] = 5 cannot be placed in baskets[2] = 4.
Since one fruit type remains unplaced, we return 1.
Example 2:
Input: fruits = [3,6,1], baskets = [6,4,7]
Output: 0
Explanation:
fruits[0] = 3 is placed in baskets[0] = 6.
fruits[1] = 6 cannot be placed in baskets[1] = 4 (insufficient capacity) but can be placed in the next available basket, baskets[2] = 7.
fruits[2] = 1 is placed in baskets[1] = 4.
Since all fruits are successfully placed, we return 0.
Constraints:
n == fruits.length == baskets.length
1 <= n <= 100
1 <= fruits[i], baskets[i] <= 1000
我的解法直接模拟,- class Solution:
- def numOfUnplacedFruits(self, fruits: List[int], baskets: List[int]) -> int:
- ans = 0
- for f in fruits:
-
- for i, b in enumerate(baskets):
- if b >= f:
- break
- delIndex = -1
- if baskets[i] >= f:
- delIndex = i
- else:
- ans += 1
-
- if delIndex != -1: baskets.pop(i)
-
- return ans
-
复制代码 优化
隐患 1:循环变量 i 的作用域泄漏 (Scope Leakage)
你写了这样一段逻辑:
Python
for i, b in enumerate(baskets):
if b >= f:
break
delIndex = -1
if baskets[i] >= f: # <--- 这里依赖了循环结束后的 i
在 Python 中,如果循环正常结束(没有被 break 打断),i 会保留最后一个元素的索引。虽然在这里因为 baskets 不会为空,代码碰巧能运行,但如果 baskets 为空,i 将完全未定义,直接抛出 UnboundLocalError。这在面试中会被认为是不好的编程习惯。
隐患 2:在遍历时使用 .pop(i) 物理删除元素
在数组中间使用 .pop(i) 去删除元素,底层会把该索引后面的所有元素全部向前移动一位。这会导致该操作本身的时间复杂度变为 O(N)。
虽然本题数据量极小(N <= 100),怎么写都能过,但如果 N 是 10^5,这种写法会直接导致 Time Limit Exceeded (超时)。
🧠 核心思路:原地标记 (In-place Marking)
面对“元素用过就作废”的场景,与其去物理删除它(修改数组长度和索引),不如在原地给它打个标记。
题目说 baskets[i] >= 1,那么我们只要把用过的篮子容量改成 -1 或者 0,它就自然变成了一个“废弃”的篮子,后续再大的水果也放不进去了。这样既保留了原数组的索引,又省去了删除元素的开销。- class Solution:
- def numOfUnplacedFruits(self, fruits: list[int], baskets: list[int]) -> int:
- unplaced = 0
-
- for f in fruits:
- # 标记当前水果是否成功放入篮子
- placed = False
-
- for i in range(len(baskets)):
- if baskets[i] >= f:
- baskets[i] = -1 # 核心技巧:原地标记为 -1,代表该篮子已作废
- placed = True
- break # 找到了最左边的,直接停止搜索
-
- # 如果遍历完所有篮子都没放进去,未放置数量 +1
- if not placed:
- unplaced += 1
-
- return unplaced
复制代码
补充内容 (2026-08-13 10:25 +08:00):
进阶思路:线段树 (了解即可,应对面试追问)我们可以用一棵线段树来维护数组中每个区间的最大值(Max Capacity)。
- 查找最左边满足条件的篮子:
当拿到一个大小为 $f$ 的水果时,我们从线段树的根节点(代表整个数组)开始往下找: - 先看左半边区间的最大值是不是 $\ge f$?如果是,说明左边一定有合格的篮子,毫不犹豫往左子树走(保证了“最左边”的要求)。
- 如果左半边不够大,再看右半边区间的最大值是不是 $\ge f$?如果是,往右子树走。
- 如果整个根节点的最大值都 $< f$,说明所有篮子都装不下,直接跳过。
- 更新(原地作废):
找到那个具体的篮子(叶子节点)后,把它所在位置的值更新为,并一路往上更新线段树区间最大值。 每一次查找和更新的时间复杂度都是严格的 $O(\log N)$,整体时间复杂度完美降到 $O(N \log N)$,且完美保留了原数组的顺序。
补充内容 (2026-08-13 10:27 +08:00):
太棒了!你有主动去啃高级数据结构的意识,这在面试大厂(尤其是字节、谷歌这类喜欢考 Hard 题的公司)是非常核心的竞争力。
线段树(Segment Tree)听起来吓人,但只要你看透它的本质,它其实就是一个“自带导航功能的二叉树”。
针对“找最左边大于等于 $f$ 的篮子并更新”这个需求,我带你一步步拆解并手写这棵线段树。
🧠 1. 核心思路:线段树是怎么“导航”的?
线段树的每个节点,代表原数组的一个区间。我们让每个节点记录它所代表区间的 最大值 (Max)。
假设,对应的线段树是这样的:
[0-3] 最大值:6
/ \
[0-1] 最大值:6 [2-3] 最大值:4
/ \ / \
[0]值:3 [1]值:6 [2]值:1 [3]值:4
现在来了一个水果,大小为,我们从树根(代表整个数组)开始找,核心逻辑如下:
- 看整个数组最大值:根节点最大值是,说明这四个篮子里肯定有能装下的篮子。进树!
- 永远先看左边(保证最左原则):
- 左半区的最大值是,说明左边够用!毫不犹豫往左走(哪怕右边也有能装的,我们也不管,因为题目要求最左边)。
- 到了,再看它的左孩子,最大值是,装不下!
- 那只能看它的右孩子,最大值是,找到了!
- 把的值改成(代表用废了)。
- 最关键的一步:顺藤摸瓜往上回溯,重新计算途径节点的最大值。因为废了,的最大值就变成了,根节点的最大值就变成了(由右半区的撑场面)。
你看,一次查找 + 更新,只走了一条从根到叶子的路径,高度是 $\log N$,所以时间复杂度是 $O(\log N)$!
✨ 2. 面试标准代码实现 (Python)
在面试中,写线段树最稳妥、最干净的做法是用一维数组来模拟树(类似堆的写法:左孩子是,右孩子是)。标准需要开辟的空间。
class SegmentTree:
def __init__(self, arr: list[int]):
self.n = len(arr)
# 线段树的标准空间是原数组大小的 4 倍
self.tree = [0] * (4 * self.n)
# 构建线段树
self.build(arr, 0, 0, self.n - 1)
def build(self, arr: list[int], node: int, start: int, end: int):
"""递归构建线段树,维护每个区间的最大值"""
# 如果到了叶子节点(具体某一个篮子)
if start == end:
self.tree[node] = arr[start]
return
mid = (start + end) // 2
left_child = 2 * node + 1
right_child = 2 * node + 2
# 分别构建左半区和右半区
self.build(arr, left_child, start, mid)
self.build(arr, right_child, mid + 1, end)
# 当前节点的最大值 = max(左边最大值, 右边最大值)
self.tree[node] = max(self.tree[left_child], self.tree[right_child])
def query_and_update(self, node: int, start: int, end: int, f: int) -> int:
"""寻找最左边 >= f 的位置,找到后更新为 -1,返回对应的索引"""
# 1. 如果当前区间的最大值都比 f 小,说明这片区域全都不行,直接返回 -1
if self.tree[node] < f:
return -1
# 2. 如果找到了叶子节点,说明这就是我们要找的那个篮子!
if start == end:
self.tree[node] = -1 # 原地作废
return start
mid = (start + end) // 2
left_child = 2 * node + 1
right_child = 2 * node + 2
# 3. 核心机制:优先查左边!只要左边的最大值能满足 f,就死死咬住左边往下走
if self.tree[left_child] >= f:
res = self.query_and_update(left_child, start, mid, f)
else:
# 左边不行,才勉强去右边找
res = self.query_and_update(right_child, mid + 1, end, f)
# 4. 重点:底下的叶子节点被修改了,所以要向上回溯,更新当前区间的最大值
self.tree[node] = max(self.tree[left_child], self.tree[right_child])
return res
class Solution:
def numOfUnplacedFruits(self, fruits: list[int], baskets: list[int]) -> int:
n = len(fruits)
# 初始化线段树
seg_tree = SegmentTree(baskets)
unplaced = 0
for f in fruits:
# 在全区间 [0, n-1] 寻找并更新
# 我们不需要知道具体放到了哪个索引,只需要知道有没有放成功(!= -1)
res = seg_tree.query_and_update(0, 0, n - 1, f)
if res == -1:
unplaced += 1
return unplaced
📊 3. 复杂度分析与面试实战建议
- 时间复杂度:
- 建树:$O(N)$。要把原数组的值放到树的叶子上并向上汇总,遍历整个树的节点。
- 查询并更新:每个水果花费 $O(\log N)$。有 $N$ 个水果,总共 $O(N \log N)$。
- 整体时间:$O(N \log N)$。完美应对 $N = 10^5$ 的数据量!
- 空间复杂度:$O(N)$。数组需要 $4N$ 的空间,属于线性空间复杂度。
💡 面试伴侣的小贴士: 在线上面试时,如果遇到类似的题,第一法则永远是先写出 $O(N^2)$ 的暴力解法保证 AC 或拿到基础分(也就是我们上一轮写的代码)。 只有在以下两种情况,你才需要把这套线段树搬出来:
- 题目数据量明确标识了 $N = 10^5$,暴力法绝对会超时。
- 你写完了暴力法,面试官微笑着问:“代码不错,但如果我们想把它优化到 $O(N \log N)$,你有什么思路吗?” 这时你抛出“由于有寻找最左侧限制,普通的排序二分不可行,我们需要维护区间最大值的线段树来定向剪枝”,面试官绝对会眼前一亮!
这个模板你先消化一下,特别是“优先走左孩子”的这个 if 判断,它是解决“最左边”限制的灵魂。有没看懂的地方随时问我! |
|