【C/C++】699. 掉落的方块

229 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情


题目链接:699. 掉落的方块

题目描述

在二维平面上的 x 轴上,放置着一些方块。

给你一个二维整数数组 positions ,其中 positions[i]=[lefti,sideLengthi]positions[i] = [left_i, sideLength_i] 表示:第 i 个方块边长为 sideLengthisideLength_i ,其左侧边与 x 轴上坐标点 leftileft_i 对齐。

每个方块都从一个比目前所有的落地方块更高的高度掉落而下。方块沿 y 轴负方向下落,直到着陆到 另一个正方形的顶边 或者是 x 轴上 。一个方块仅仅是擦过另一个方块的左侧边或右侧边不算着陆。一旦着陆,它就会固定在原地,无法移动。

在每个方块掉落后,你必须记录目前所有已经落稳的 方块堆叠的最高高度

返回一个整数数组 ans ,其中 ans[i] 表示在第 i 块方块掉落后堆叠的最高高度。

提示:

  • 1positions.length10001 \leqslant positions.length \leqslant 1000
  • 1lefti 1081 \leqslant left_i \leqslant 10^8
  • 1sideLengthi 1061 \leqslant sideLength_i \leqslant 10^6

示例 1: fallingsq1-plane.jpg

输入:positions = [[1,2],[2,3],[6,1]]
输出:[2,5,5]
解释:
第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 2 。
第 2 个方块掉落后,最高的堆叠由方块 12 组成,堆叠的最高高度为 5 。
第 3 个方块掉落后,最高的堆叠仍然由方块 12 组成,堆叠的最高高度为 5 。
因此,返回 [2, 5, 5] 作为答案。

示例 2:

输入:positions = [[100,100],[200,100]]
输出:[100,100]
解释:
第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 100 。
第 2 个方块掉落后,最高的堆叠可以由方块 1 组成也可以由方块 2 组成,堆叠的最高高度为 100 。
因此,返回 [100, 100] 作为答案。
注意,方块 2 擦过方块 1 的右侧边,但不会算作在方块 1 上着陆。

整理题意

该题机制类似于俄罗斯方块,题目给定每个正方形方块左下角点落在 x 轴上的位置,且告诉正方形方块的边长,问当第 i 块正方形掉落后堆叠的最高高度为多少。 需要注意的是:

  • 所有方块落在别的方块上或者 x 轴上着陆,一旦着陆,它就会固定在原地,无法移动。
  • 一个方块仅仅是擦过另一个方块的左侧边或右侧边不算堆叠。

解题思路分析

观察题目数据范围,总方块数量在 1000 以内,数据范围较小,所以我们可以采用暴力的方法解决。

方法一:暴力

对于每个落下的方块,我们记录当前方块下落后的高度,只需要遍历之前已经下落的方块,检查当前方块下落位置是否有重叠,对于有重叠的位置进行高度更新,保存最大高度即可。

因为题目求的是当第 i 块方块掉落后,求包括当前方块以及前面已经掉落的方块最高高度,所以最后需要从第一块方块开始,从前往后不断更新最大高度,也就是求前缀的最大高度。

方法二:有序集合 map

我们在有序集合中存放位置节点以及位置节点的高度,如 mp[i] = h 表示位置节点 i 的高度为 h,当我们放入一个方块时,仅需修改方块左端点所在位置节点 left 的高度和方块右端所在位置节点加一 right + 1 处的高度即可,这样就可以表示在区间 [left, right] 之间的高度是同一高度。

首先我们考虑使用数组存放,但由于位置节点数据范围在 10810^8 以内,无法创建这么大的数组,所以我们采用 有序集合 map 进行存储。

对于当前掉落的方块,左端点为 left,右端点为 right,我们在有序集合中二分查找该区间 [left,right] 内所有位置节点的高度,然后取最大的高度来更新当前掉落方块的高度,由于当前方块掉落后区间 [left,right] 内的高度都为更新后的高度,所以我们需要将有序集合中位于区间 [left,right] 内的位置节点全部删除。然后更新有序集合中位置节点 left 和位置节点 right + 1 即可。

这里就需要注意一个细节,我们需要提前保存 right 在掉落当前方块之前的高度,以便后面更新 right + 1 处的高度

具体实现

方法一:暴力

  1. 第一层循环遍历每块方块,表示当前下落的方块
  2. 第二层循环遍历之前已经下落的方块。
  3. 检查当前方块与之前已经落下的方块是否有重叠,更新高度即可。
  4. 最后遍历一遍高度,求前缀最大高度即可。

方法二:有序集合 map

  1. 初始化有序集合中 x 轴高度为 0,表示所有方块都要落在高度为 0 上。
  2. 遍历每个方块依次将方块左右位置节点插入有序集合并更新有序集合中的节点和高度。
  3. 采用有序集合 map 自带的二分查找,寻找当前放入方块的左端点所在的位置,这里使用 upper_bound(left) 进行查找 lpos,那么 lpos 的前一个节点 lpre 也可以通过 lpos 求得。

使用 upper_bound() 的原因是可以通过 lposlpre 知道 left 所在的区间为 [lpre, lpos] 之间。

  1. 同理使用 upper_bound(right) 求得 rposrpre方块.jpg
  2. 记录右节点落入之前的高度 int rHeight = rpre->second;
  3. 遍历查找 (left, right) 中最高高度,注意这里不包括 leftright,因为擦过另一个方块的左侧边或右侧边不算堆叠。
  4. 删除有序集合中位于区间 [left,right] 内的节点。
  5. 更新节点 left 高度和 right + 1 的高度。

需要注意的是在更新 right + 1 的高度时需要用到第 5 步记录的 rHeight

  1. 不断与之前最大高度进行取 max 即可。

复杂度分析

方法一:暴力

  • 时间复杂度:O(n2)O(n^2),其中 n 是数组 positions 的长度。
  • 空间复杂度:O(1)O(1)。返回值不计入空间复杂度。

方法二:有序集合 map

  • 时间复杂度:O(nlogn)O(nlogn),其中 n 是数组 positions 的长度。有序集合 map 最多插入 2n + 1 个元素,因此整个循环最多执行删除操作 2n + 1 次,而每次循环里的查询操作只比删除操作多一次,因此总的查询操作最多为 3n + 1 次;插入操作、删除操作、迭代器递增操作以及二分查找操作都需要 O(logn)O(\log n),因此总共需要 O(nlogn)O(n \log n)
  • 空间复杂度:O(n)O(n)。有序集合最多保存 2n + 1 个元素。

代码实现

方法一:暴力

class Solution {
public:
    vector<int> fallingSquares(vector<vector<int>>& positions) {
        int n = positions.size();
        vector<int> ans(n, 0);
        for(int i = 0; i < n; i++){
            int li = positions[i][0], ri = positions[i][0] + positions[i][1] - 1;
            ans[i] = positions[i][1];
            for(int j = 0; j < i; j++){
                int lj = positions[j][0], rj = positions[j][0] + positions[j][1] - 1;
                if(li <= rj && ri >= lj){
                    ans[i] = max(ans[i], ans[j] + positions[i][1]);
                }
            }
        }
        for(int i = 1; i < n; i++){
            ans[i] = max(ans[i], ans[i - 1]);
        }
        return ans;
    }
};

方法二:有序集合 map

class Solution {
public:
    vector<int> fallingSquares(vector<vector<int>>& positions) {
        //有序集合记录节点高度
        map<int, int> mp;
        //初始化x轴高度为0
        mp[0] = 0;
        int n = positions.size();
        vector<int> ans(n);
        //将每个方块依次落入有序集合并判断高度
        for(int i = 0; i < n; i++){
            //正方形左节点和右节点
            int left = positions[i][0], right = positions[i][0] + positions[i][1] - 1;
            //在有序集合中找到左右节点所在位置(二分)
            auto lpos = mp.upper_bound(left), rpos = mp.upper_bound(right);
            //left位于 [lpos - 1, lpos) 左闭右开区间内,rpos位于 [rpos - 1, rpos) 左闭右开区间内
            //记录右节点落入之前的高度
            auto rpre = rpos;
            rpre--;
            //还可以直接调用函数取前一个迭代器:prev(rpos);
            int rHeight = rpre->second;
            //找到(left, right)中最高的
            int height = 0;
            auto lpre = lpos;
            lpre--;
            //注意如果写成auto iter = lpre--; 会造成先赋值后--
            for(auto iter = lpre; iter != rpos; iter++){
                height = max(height, iter->second + positions[i][1]);
            }
            //删除[left, right]内所有节点
            mp.erase(lpos, rpos);
            /*需要注意的是 如果 mp.erase(iter) 后对 iter++ 是错误的
            auto itnxt = lpos;
            for(auto iter = lpos; iter != rpos; iter = itnxt){
                itnxt = ++iter;
                iter--;
                mp.erase(iter);
            }
            */
            //更新left高度和right + 1 的高度
            mp[left] = height;
            //只有 right + 1  不在 mp 中才更新为落入之前的高度
            if(rpos == mp.end() || rpos->first != right + 1){
                mp[right + 1] = rHeight;
            }
            ans[i] = i > 0 ? max(ans[i - 1], height) : height;
        }
        return ans;
    }
};

总结

  • 该题虽然暴力也可解决,但有序集合的思想更为巧妙,且时间复杂度更优。
  • 同时需要注意有序集合的函数调用,包括自带的二分查找函数 upper_bound() 是找到一个大于的,找不到返回 end();在需要求当前迭代器的上一个迭代器时可以调用函数取前一个迭代器:prev();,因为迭代器支支持 ++-- 操作,当我不想修改迭代器时又想获取上一个迭代器时可以使用函数获得。
  • 另外需要注意 auto iter = lpre--auto iter = --lpre 两个语句的区别,lpre-- 会先赋值再 --,而 --lpre 会先 -- 后赋值。
  • 测试结果: 微信截图_20220612155558.png 微信截图_20220612161010.png

结束语

为什么我们要努力?每个人的答案都不同。时间对待所有人都是公平的,是努力让每段人生不同。努力,能成全更多小小的梦想,能让你遇见更多可能的自己,能让你再回首时说一句问心无愧。生活会回报真正努力的你。