水果成篮-III 分块思想

129次阅读
一条评论

共计 3781 个字符,预计需要花费 10 分钟才能阅读完成。

问题背景:

给你两个长度为  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。

注意:n == fruits.length == baskets.length

1 <= n <= 105

1 <= fruits[i], baskets[i] <= 109

该题 中数据量较大,直接使用暴力破解的方式遍历每个水果 对所有篮子进行判断 会非常影响效率。所以需要进行优化。因为篮子数较多,每个篮子都有它的容量,因此想到用分块的思想。

将篮子数量进行分块 成组,每组 m 个篮子,维护一个数组 maxV[] 用于存储每组的最大容量

当篮子的最大容量大于等于水果数量时,进入该分组,选择一个最优的篮子 放入该水果。

基本思路

该问题的核心挑战是如何高效地为每个水果找到合适的篮子。暴力解法会 ** 对每个水果遍历所有篮子 **,时间复杂度为 O(n*m),当 n 和 m 较大时效率低下。平方根分解(SQRT Decomposition)是一种将数据分成块进行处理的优化方法,可以将时间复杂度降低到 O(m√n)。

优化思想

此处进行分组的方式 - 平方根分组

平方根分解的基本思想是:

  1. 将篮子数组分成若干块,每块大小约为√n
  2. 为每个块维护一个最大值,用于快速判断该块是否可能有合适的篮子
  3. 处理每个水果时,先检查各块的最大值,跳过不可能的块
  4. 在可能的块内遍历篮子,找到第一个符合条件的篮子

实现:

变量定义与初始化

在代码实现中,关键变量的含义如下:

  • n:篮子的总数量
  • m:每个块的大小,取约√n
  • section:块的数量,计算为Math.ceil(n/m) 篮子总数量 / 每个块大小 得到的就是 分组数也就是块数
  • maxV:数组,存储每个块中的最大篮子容量

Math.ceil() 用于将数字向上舍入到最接近的整数。若参数不是数字类型,将返回 TypeError

初始化结果:

const n = baskets.length;  // 篮子数量
const m = Math.floor(Math.sqrt(n)); // 每个块的大小约为√n
const section = Math.ceil(n / m); // 块的数量
let count = 0; // 记录未放置的水果数量
const maxV = new Array(section).fill(0); // 存储每个块的最大容量

Math.floor() 是一个用于向下取整的函数,它返回小于或等于给定数字的最大整数

    // 遍历篮子
    for(let i =0;i<n;i++) {
        // 当前篮子所属的块索引
        // 每个块包含 m 个篮子,第 i 个篮子除以 m 的整数部分,就是它所在的块索引。const sec = Math.floor(i / m)
        //  记录每组的最大容量
        maxV[sec] = Math.max(maxV[sec],baskets[i])
    }
    

开始放置水果(遍历水果)

for (fruit of fruits) {
  let unset = 1   // 表示当前没有被放置,后续的叠加结果作为没有被放置的水果总数
   for(let sec= 0;sec<section;sec++) {
            // 如果 当前分块的最大容量也小于水果量 直接跳过当次遍历 进入下一次遍历
            if(maxV[sec] < fruit) {continue}
            // 可以存放水果   
            let choose = 0   // 当前块中是否已找到合适的篮子
            maxV[sec] = 0    // 放置水果以后  后面会重新计算当前块的最大容量 以保证准确性
            // 遍历当前分块 m 就是每个块的长度
            for (let i =0;i<m;i++) {
                const pos = sec * m +i  //pos 是当前块内第 i 个篮子在原始数组 baskets 中的索引。// 块 sec 的起始索引是 sec * m(每个块有 m 个元素),加上块内的偏移量 i(0 到 m -1),就是原始数组中的具体位置。//  放入水果
                if(pos < n && baskets[pos] >= fruit && !choose) {baskets[pos] =0
                    // 当前分块 找到合适的篮子
                    choose =1
                }
                // 在块内有篮子被使用后,及时更新该块的最大可用容量
                //
                // 分块策略中维持“块最大值准确性”的关键步骤
                if(pos < n) {  //:防止数组越界、//  重新计算当前块的最大篮子容量
                    maxV[sec] = Math.max(maxV[sec],baskets[pos])
                }
            }
            // 水果已经放置
            unset = 0
            break
        }
count +=unset  // 记录无法放置的水果
}

return count   // 返回结果 就是无法被放置的水果种类数

完整实现代码 JS 版本

var numOfUnplacedFruits = function(fruits, baskets) {

    // 将篮子分组,取得每组中最大容量,然后遍历水果

    const n = baskets.length

    const m = Math.floor(Math.sqrt(n))   // ?? 为何是这样的数量

    const section = Math.ceil(n/m)         // 组数,用 篮子的总数量 / 分组的大小

    let count = 0  //  计数

    const maxV = new Array(section).fill(0)

    // 遍历篮子

    for(let i =0;i<n;i++) {

        // 当前篮子所属的块索引

        // 每个块包含 m 个篮子,第 i 个篮子除以 m 的整数部分,就是它所在的块索引。const sec = Math.floor(i / m)

        //  记录每组的最大容量

        maxV[sec] = Math.max(maxV[sec],baskets[i])

    }

    // 遍历水果

    for(const fruit of fruits) {

        let unset = 1   // 表示当前没有被放置,后续的叠加结果作为没有被放置的水果总数

        // 找到 一个块 最大容量大于水果量

        for(let sec= 0;sec<section;sec++) {

            // 如果 当前分块的最大容量也小于水果量 直接跳过当次遍历 进入下一次遍历

            if(maxV[sec] < fruit) {continue}

            // 可以存放水果  

            let choose = 0   // 当前块中是否已找到合适的篮子

            maxV[sec] = 0    // 重新计算当前块的最大容量

            // 遍历当前分块 m 就是每个块的长度

            for (let i =0;i<m;i++) {

                const pos = sec * m +i  //pos 是当前块内第 i 个篮子在原始数组 baskets 中的索引。// 块 sec 的起始索引是 sec * m(每个块有 m 个元素),加上块内的偏移量 i(0 到 m -1),就是原始数组中的具体位置。//  放入水果

                if(pos < n && baskets[pos] >= fruit && !choose) {baskets[pos] =0

                    // 当前分块 找到合适的篮子

                    choose =1

                }

                // 在块内有篮子被使用后,及时更新该块的最大可用容量

                //

                // 分块策略中维持“块最大值准确性”的关键步骤

                if(pos < n) {  //:防止数组越界、//  重新计算当前块的最大篮子容量

                    maxV[sec] = Math.max(maxV[sec],baskets[pos])

                }

            }

            // 水果已经放置

            unset = 0

            break

        }

        // 记录数量

        count+=unset

    }

    // 返回结果

    return count

};

本题中学到的地方

pos 当前块内第 i 个篮子在原始数组 baskets 中的索引

计算方法为:const pos = sec * m + i // 块 sec 的起始索引是sec * m 加上第 i 个篮子的偏移量 i

符合放置条件的篮子的判断

    //  放入水果
                if(pos < n && baskets[pos] >= fruit && !choose) {baskets[pos] =0
                    // 当前分块 找到合适的篮子
                    choose =1
                }

维持分块思想 准确性的 更新机制

    // 分块策略中维持“块最大值准确性”的关键步骤
                if(pos < n) {  //:防止数组越界、//  重新计算当前块的最大篮子容量
                    maxV[sec] = Math.max(maxV[sec],baskets[pos])
                }

该方法优势

避免了对每个篮子进行线性遍历,时间复杂度为 O(√n),适合篮子数量较多的情况下。

正文完
文章二维码
post-qrcode
 0
xyblog
版权声明:本文于2025-08-06转载自leetcode,共计3781字。
转载提示:此文章非本站原创文章,若需转载请联系原作者获得转载授权。
评论(一条评论)
努力. 评论达人 LV.1
2025-08-06 11:06:27 回复

该题中 有顺序的要求

 Android  Chrome  中国江苏省南京市电信