LeetCode | 水果成篮 II & III (Fruits into Baskets II & III)

        给你两个长度为 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.length
  • 1 <= n <= 100
  • 1 <= fruits[i], baskets[i] <= 1000

3479. 水果成篮 III 提示:

  • n == fruits.length == baskets.length
  • 1 <= n <= 10^5
  • 1 <= 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) 著作权归作者所有。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值