主题4-攒青豆 题解 | 青训营 X 码上掘金

62 阅读3分钟

当青训营遇上码上掘金

活动页面 「青训营 X 码上掘金」主题创作活动入营版 开启!

仅根据活动页面给出的样例不具备不能完全模拟实际情况,本题与leetcode平台题目 —— 42. 接雨水 相同,因而作者直接在leetcode平台进行代码编写与测试。

题目

题意

现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

image.png

样例

示例 1:

输入:height = [5,0,2,1,4,0,1,0,3] 
输出:17 
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

示例 2:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6

示例 3:

输入:height = [4,2,0,3,2,5]
输出:9

数据范围

根据leetcode平台补充了数据范围:

  • n==height.lengthn == height.length
  • 1<=n<=2×1041 <= n <= 2 \times 10^4
  • 0<=height[i]<=1050 <= height[i] <= 105

题目分析

我们试分析题目或构思新的样例时可以发现,水位的分布一定是成一个山字行,即从左至右先上升(准确来说是非递减),再下降(准确说是非递增)。除极端情况下柱子的高度全部为0外,成一个近似“凸”字形,不可能出现“凹”字形。因为即使会出现“凹”字形,中间的凹槽也会被雨水(青豆)填满。

因而我们可以用优先队列或直接对柱子进行排序(注意要记录索引)。作者采用排序,这里可以注意到快速排序的时间复杂度结合nn的数据范围是可以的。从对柱子从高到低进行排序后,最高的两个柱子一定会构成最高的池子(因为坐标在这两个柱子之间的其它柱子的高度一定小于等于他们的高度),首先计算该池子能蓄的水,使用一个visvis的布尔数组记录已经处理过的柱子,并记录已经处理的最高的池子的左右范围坐标[l,r][l, r]

接下来继续从高到低遍历剩余的柱子,如果已经处理过(背visvis数组记录过)则跳过。未处理过的话则该柱子的坐标一定>r > r<l < l。因而判断属于这两种情况中的哪一种,即该柱子与已有的池子的外墙又形成了一个小池子,继续处理并扩充[l,r][l, r],如此循环直到结束。

题解

代码

class Solution {
public:
    int trap(vector<int>& height) {
        // 对height进行排序,然后从高向低扫描,并用[l, r] 记录高的池子(已经处理过的)
        int n = height.size();
        vector<int> ind(n);
        iota(ind.begin(), ind.end(), 0);
        sort(ind.begin(), ind.end(), [&](int &a, int &b){
            return height[a] > height[b];
        });

        if(n == 1 || height[ind[1]] == 0) return 0;

        // 记录已经处理过的池子
        vector<bool> vis(n, false);
        // 第一个池子即是
        int l = min(ind[0], ind[1]), r = max(ind[0], ind[1]);
        int ans = 0, h = height[ind[1]];

        // 处理第一个池子
        vis[l] = vis[r] = true;
        for(int i=l+1;i<r;i++){
            ans += h - height[i];
            // cout << i << " ";
            vis[i] = true;
        }

        // cout << l << " " << r << " " << ans << endl;

        // 处理后续池子
        for(int i=2;i<n;i++){
            if(vis[ind[i]]) {
                // cout << i << " " << ind[i] << endl;
                continue;
            }
            // 一定在[l, r]的左边或右边
            h = height[ind[i]];
            if(h == 0) break;
            vis[ind[i]] = true;

            if(ind[i] > r){
                for(int j=r+1;j<ind[i];j++){
                    ans += h - height[j];
                    // cout << i << " ";
                    vis[j] = true;
                }
                r = ind[i];
            } else{
                for(int j=ind[i]+1;j<l;j++){
                    ans += h - height[j];
                    // cout << i << " ";
                    vis[j] = true;
                }
                l = ind[i];
            }
            cout << endl;
            // cout << i << " " << l << " " << r << " " << ans << endl;
        }

        return ans;
    }
};

复杂度分析

其中复杂度最高的操作为快速排序,因而复杂度为 O(nlog(n))O(nlog(n))

运行结果

image.png

本题还有许多更好地算法如动态规划、单调栈等。这里暂时不一一介绍了。