XingHuiSamaの宝藏之地
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面

Leetcode一百题——接雨水

2026-04-12 11:59:16
# Leetcode
# C++
# 题解
# 算法

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

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

提示:

  • n == height.length
  • 1 <= n <= 2 * 104
  • 0 <= height[i] <= 105

这道题我一个想到的是横向的计算,但是时间太长

接雨水(从“横向切分”到“纵向双指针”的思维演进)

方案一:横向切分(按行求水)

1. 算法核心思想:切蛋糕法

这是一种极其符合人类物理直觉的解法。我们把整个地形想象成一个积木,从高度为 1 的底层开始,一层一层向上“横向切割”,一直切到最高的那根柱子的顶部。

  • 对于单独的每一层(高度设为 i),我们先从左到右找到第一个能挡水的高度(左边界 start),再从右到左找到第一个能挡水的高度(右边界 end)。
  • 在这两个边界内部,只要真实的柱子高度小于当前层的高度 i,这就必然是一个能装 1 格水的空缺。
class Solution {
public:
    int trap(vector<int>& height) {
        if (height.empty()) return 0;

        int count = 0;
        int heightMax = 0;
        // 1. 找出整个地形的最高峰
        for (int i = 0; i < height.size(); i++) {
            if (height[i] > heightMax) {
                heightMax = height[i];
            }
        }

        // 2. 从高度 1 开始,一层一层往上横向扫描
        for (int i = 1; i <= heightMax; i++) {
            int start = 0, end = height.size() - 1;

            // 找当前层的左侧兜底边界
            while (start < height.size() && height[start] < i) {
                start++;
            }
            // 找当前层的右侧兜底边界
            while (end >= 0 && height[end] < i) {
                end--;
            }

            // 在有效边界内,计算空缺的水格
            for (int j = start; j <= end; j++) {
                if (height[j] < i) {
                    count++;
                }
            }
        }
        return count;
    }
};

3. 复杂度剖析与局限性

  • 时间复杂度:O(N×H)。其中 N 是数组长度,H 是最大高度 heightMax。这种解法在高度 H 较小时运行完美,但在极端测试用例(如柱子极高,且交错出现)中,会导致循环次数激增,最终触发 Time Limit Exceeded (TLE,超出时间限制)。
  • 空间复杂度:O(1)。没有使用额外的数据结构。

💡 题解:接雨水(从“横向切分”到“纵向双指针”的思维演进)

解决接雨水问题,本质上是在计算复杂地形中的“有效凹陷面积”。我们在思考这道题时,经历了从“按行切分(直观物理模拟)”到“按列计算(数学极限降维)”的思维跨越。

方案一:横向切分(按行求水)—— 直观但触碰性能天花板

1. 算法核心思想:切蛋糕法

这是一种极其符合人类物理直觉的解法。我们把整个地形想象成一个积木,从高度为 1 的底层开始,一层一层向上“横向切割”,一直切到最高的那根柱子的顶部。

  • 对于单独的每一层(高度设为 i),我们先从左到右找到第一个能挡水的高度(左边界 start),再从右到左找到第一个能挡水的高度(右边界 end)。
  • 在这两个边界内部,只要真实的柱子高度小于当前层的高度 i,这就必然是一个能装 1 格水的空缺。

2.代码实现

class Solution {
public:
    int trap(vector<int>& height) {
        if (height.empty()) return 0;

        int count = 0;
        int heightMax = 0;
        // 1. 找出整个地形的最高峰
        for (int i = 0; i < height.size(); i++) {
            if (height[i] > heightMax) {
                heightMax = height[i];
            }
        }

        // 2. 从高度 1 开始,一层一层往上横向扫描
        for (int i = 1; i <= heightMax; i++) {
            int start = 0, end = height.size() - 1;

            // 找当前层的左侧兜底边界
            while (start < height.size() && height[start] < i) {
                start++;
            }
            // 找当前层的右侧兜底边界
            while (end >= 0 && height[end] < i) {
                end--;
            }

            // 在有效边界内,计算空缺的水格
            for (int j = start; j <= end; j++) {
                if (height[j] < i) {
                    count++;
                }
            }
        }
        return count;
    }
};

3. 复杂度剖析与局限性

  • 时间复杂度:O(N×H)。其中 N 是数组长度,H 是最大高度 heightMax。这种解法在高度 H 较小时运行完美,但在极端测试用例(如柱子极高,且交错出现)中,会导致循环次数激增,最终触发 Time Limit Exceeded (TLE,超出时间限制)。
  • 空间复杂度:O(1)。没有使用额外的数据结构。

方案二:纵向双指针(按列求水)

1. 算法核心思想:局部木桶效应

一根单独的柱子头顶能装多少水,完全取决于**“它左侧的最高峰(LeftMax)”与“它右侧的最高峰(RightMax)”中的较短者**。只要当前柱子的高度低于这个短板,就能装下两者差值的水量。

2. 双指针降维逻辑

与其针对每根柱子向两边寻找最高峰,不如使用双指针从两端向中间相向而行,一边走一边维护历史最高峰。

  • 短板优先判断:比较 LeftMax 和 RightMax,哪边的最高峰更矮,哪边柱子的命运(蓄水量)就已经被锁死了,因为另一边一定存在一个更高的墙在兜底。
  • 结算与更新:确定了短板一侧后,拿历史最高峰与脚下真实柱子比较。如果形成凹陷,直接结算接水量;如果脚下的柱子比历史最高峰还高,不仅接不到水,它还会成为新的最高峰,用于挡住后面的水。
class Solution {
public:
    int trap(vector<int>& height) {
        // 1. 防御边界数据:少于 3 根柱子无法形成坑洼接水
        if (height.size() <= 2) {
            return 0;
        }

        int sum = 0;
        // 2. 记录初始左右两端的最高峰
        int LeftMax = height[0];
        int RightMax = height[height.size() - 1];

        // 3. 双指针从两侧次边缘开始向中间逼近
        int left = 1;
        int right = height.size() - 2;

        // 4. 确保左右指针相遇的最后一根柱子也能被结算
        while (left <= right) { 
            // 左边是短板,左边命运确定
            if (LeftMax < RightMax) {
                if (LeftMax > height[left]) {
                    sum += (LeftMax - height[left]); // 遇到坑洼,算水
                } else {
                    LeftMax = height[left];          // 遇到新高山,更新短板
                }
                left++; 
            }
            // 右边是短板,右边命运确定
            else {
                if (RightMax > height[right]) {
                    sum += (RightMax - height[right]);
                } else {
                    RightMax = height[right];
                }
                right--; 
            }
        }

        return sum;
    }
};

4. 复杂度剖析

  • 时间复杂度:O(N)。双指针遍历,数组中的每个元素最多被访问一次,直接降维打击,彻底解决超时问题。
  • 空间复杂度:O(1)。仅维护了几个指针和极值变量,内存节约。
avatar

XingHuiSama

在代码、学术与分子动力学模拟间穿梭的普通人。近期正埋头于 GROMACS 模拟研究与神经网络计算。

RECOMMENDED

GROMACS 2025 分子动力学模拟初探2222

2026-03-24 07:00:45

Computational Chemistry Tool 工具二介绍

2026-04-01 07:00:23

Leetcode一百题——字母异位词分组

2026-04-07 15:34:18

Table of Contents