LeetCode 42接雨水全解:暴力超时→DP降维打击→双指针极限压缩空间→单调栈栈式凹槽定位,全景式解析算法优化路径

LeetCode 42接雨水全解:暴力超时→DP降维打击→双指针极限压缩空间→单调栈栈式凹槽定位,全景式解析算法优化路径

文章目录

在这里插入图片描述

本篇摘要

本篇围绕LeetCode 42“接雨水”展开,剖析四种解法:暴力法通过嵌套循环统计每柱接水量,易超时;动态规划预先记录左右最大值,将复杂度降至O(n);双指针边遍历边更新极值,空间优化至O(1);单调栈利用栈维护“凹槽”,高效定位存水区间。各方法层层递进,展现算法优化的核心思路。

LeetCode 42 接雨水 详解

在这里插入图片描述

① 暴力解法(多循环嵌套,卡超时,因此后续使用了两种基于暴力优化的方法)

拿到这道题,我们首先看图:

在这里插入图片描述
  • 这里我们不难发现当前位置装水的高度其实和两边高度有关,但是并不是相邻的,就拿索引为5的位置来说,这里最多装2个,其实和对应的3号索引以及对应大的7号索引位置有关。
  • 多观察几个就能得到一个结论:i位置处能装多少水取决于它左侧比它大的最大值与右侧比它大的最大值取个min减去i位置高度即可。

因此我们能得到:

int water =min(leftMax, rightMax)- height[i];

下面可以通过时间复杂度为O(N^2)走一遍,详细思路:

超时!!!!!
暴力法: 我们不难得出,只要我们遍历一遍数组,然后当前位置能接的水的高度其实,就是它左边比它大的,最大值与它右边比它大的最大值求个min
然后再减去当前位置高度即水的高度;然后我们暴力法是一个个位置走的,也就是求出每单位宽度接入的雨水进行累加,故此时的面积就是它的高度!

代码如下(详细见注释):

classSolution{public:inttrap(vector<int>& height){int n = height.size();int totalWater =0;for(int i =1; i < n -1; i++){int leftMax =0, rightMax =0;// 找左边最大值,如果不存在比它大的,就找最大的for(int j =0; j < i; j++){ leftMax =max(leftMax, height[j]);}// 找右边最大值,如果不存在比它大的,就找最大的for(int j = i +1; j < n; j++){ rightMax =max(rightMax, height[j]);}// 这里如果对应的左右的最大值有一个小于当前位置的高度,则说明当前位置不能接雨水if((leftMax < height[i])||(rightMax < height[i]))continue;// 计算当前位置能接的水量int water =min(leftMax, rightMax)- height[i];if(water >0){//这一步就可以取消了(如果前面判断了即continue那里) totalWater += water;}}return totalWater;}};

复杂度分析:

  • 时间复杂度:O(n²) - 平方时间复杂度
  • 空间复杂度:O(1) - 常数空间复杂度

② 动态规划解法

核心思想

对于每个位置 i,它能存多少水,取决于:

左边最高柱子右边最高柱子 中的较小者,再减去当前高度。

公式:

water[i] = max(0, min(leftMax[i], rightMax[i]) - height[i]) 
  • leftMax[i]:从 0i 的最大高度(包含 i)
  • rightMax[i]:从 in-1 的最大高度(包含 i)

步骤(三步走)
  1. 从左到右扫描,记录每个位置左边(含自己)的最大高度 → leftMax
  2. 从右到左扫描,记录每个位置右边(含自己)的最大高度 → rightMax
  3. 遍历每个位置,用上述公式累加雨水量

举例说明
height = [4, 2, 0, 3, 2, 5] 
iheight[i]leftMax[i]rightMax[i]min(L,R)water[i]
044540
124542
204544
334541
424542
555550

总和: 0 + 2 + 4 + 1 + 2 + 0 = 9


代码实现思路
  • 从暴力优化:按照暴力肯定会超时,因此我们不妨提前保存对应位置,也就是左边或者右边的最大值,用的时候直接取即可。
  • 先设置状态转移方程: dp_left[i]表示i位置及i位置以左比大的最大高度(如果不存在比它大的就是它自己) dp_right[i]表示i位置及i位置以右比大的最大高度(如果不存在比它大的就是它自己)

代码实现(详细见注释):

classSolution{public:inttrap(vector<int>&height){ vector<int>dp_left(height.size(),0);//当前位置左边的最大高度(包含当前位置) vector<int>dp_right(height.size(),0);//当前位置右边的最大高度(包含当前位置) dp_left[0]= height[0];//初始化防止越界,保证后面填表的正确性for(int i=1;i<height.size();i++){ dp_left[i]=max(dp_left[i-1], height[i]);}int n=height.size(); dp_right[n-1]= height[n-1];//初始化防止越界,保证后面填表的正确性for(int i=n-2;i>=0;i--){ dp_right[i]=max(dp_right[i+1], height[i]);}//再遍历一遍保存结果:int ans=0;//保存结果for(int i=1;i<n-1;i++){int h =min(dp_left[i], dp_right[i])- height[i];int w=1;//这里也是如果当前位置dp_left或者dp_right可能为height[i],此时故自然h就为0了:if(h>0) ans += h*w;}return ans;}};

复杂度分析

  • 时间复杂度:O(n) —— 三次遍历
  • 空间复杂度:O(n) —— 两个辅助数组
虽然空间不是最优(双指针可做到 O(1)),但这是最容易理解的动态规划解法

③ 双指针解法(优化对应的dp的空间复杂度变成O(1))

下面这个有点难理解,虽然代码写起来比较简单,做到了时间复杂度 O(N),空间复杂度O(1)。

双指针优化思路

首先我们使用dp的时候需要先把所有的左最大值以及右最大值找出保存起来,那么是不是可以边记录这俩种最大值,边进行雨水统计呢

首先要想理解下面这句话👇,我们首先直到这个原理:

双指针开始走,left和right哪个位置对应高度小,就统计哪个,然后决定++还是- -,所以不会出现比如left左边有比right大的,甚至比rightmax都大的(也就是这个位置被统计完了)这种情况!

下面再理解下:

因此我们就想到两个指针一个从左往右走,一个从右往左走,直到相遇,然后如果左边比右边大,说明右边的话,肯定左边比它高(也就是leftmax一定大于它),而右边👉没有比leftmax大的,然后我们直接拿着rightmax去right位置进行比较获取新的rightmax即可,然后再与right位置处作差即可(此时可能rightmax与right位置高度相同,此时right位置也就是不能接水);如果右边比左边大同理即可!!!

如果还不理解那就直接上图:

下面我们就以这模式为例:

在这里插入图片描述

下面我们采用反证法证明下:

在这里插入图片描述
  • 如果这种情况发生了,也就是对应的走到了图中的位置,那么此时我们就需要更新leftmax,然后就是8-6=2;难道最后真的是2吗,肯定是错的。
  • 因为我们要知道left只有比right小才会进行统计更新等,然后右移动,如果left能走到6处,说明8这个位置已经被统计更新完了,然而这样就违背我们代码的判断逻辑了,故不可能。
  • 也就是上面的情况不可能发生,即最后到达中间6位置处的一定是right,此时就是7-6=1了。

对应代码:

///双指针解法:优化对应的dp的空间复杂度变成O(1)//两个指针从首尾边走边统计接雨水量以及维护左侧和右侧对应的最大值(包括当前位置)//这里采取两个指针都向中间走的策略,然后走过的位置就要完成对应的雨水统计+跟新leftmax或者rightmaxinttrap(vector<int>& height){int left =0, right = height.size()-1;int leftMax =0, rightMax =0;int ans =0;while(left <= right){//这里如果left能走到这里,那么必然left左边没有比rightmax大的高度,因为要是有比右边最大还大的那么此时来到当前left位置的必然是right了,故不可能if(height[left]< height[right]){ leftMax =max(leftMax, height[left]); ans += leftMax - height[left]; left++;}//这里如果right能走到这里,那么必然right右边没有比leftmax大的高度,因为要是有比左边最大还大的那么此时来到当前right位置的必然是left了,故不可能else{ rightMax =max(rightMax, height[right]); ans += rightMax - height[right]; right--;}}return ans;}};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

④单调栈解法

单调栈简介

首先简单认识下单调栈 :

单调栈是一种特殊的栈结构,栈内元素始终保持单调递增单调递减的顺序。

核心特点
  • 单调性:栈内元素要么一直增大(单调递增栈),要么一直减小(单调递减栈)。
  • 出栈规则:新元素入栈时,若破坏单调性,则弹出栈顶元素,直到满足单调性再入栈。
常见用途

主要用于解决 “下一个更大/更小元素” 类问题,例如:

  • 找数组中每个元素右边第一个比它大的数(单调递增栈,从右往左遍历);
  • 找数组中每个元素左边第一个比它小的数(单调递增栈,从左往右遍历);
  • 计算柱状图中最大矩形面积、接雨水等问题。
左边最近比当前数大的数(用单调栈)
步骤:
  1. 初始化一个空栈和一个结果数组(初始值设为-1)。
  2. 遍历数组中的每个元素:
    • 当栈不为空且当前元素 ≥ 栈顶元素时,弹出栈顶元素(因为栈顶元素不可能是后续元素的“左边更大的数”了),然后依次循环比较。
    • 如果栈不为空,说明栈顶元素就是当前元素左边最近的比它大的数,将其存入结果数组;否则存入-1。
    • 将当前元素的索引入栈。
示例:

数组:[7, 5, 3, 2, 4, 1, 6, 9, 8]

在这里插入图片描述

详细步骤:

步骤当前元素栈状态操作说明结果数组
17[]栈空,结果=-1;入栈7[-1, _, _, _, _]
25[7]5 < 7,结果=7;入栈5[-1, 7, _, _, _]
33[7, 5]3 < 5,结果=5;入栈3[-1, 7, 5, _, _]
42[7, 5, 3]2 < 3,结果=3;入栈2[-1, 7, 5, 3, _]
54[7, 5, 3, 2]4 > 2 → 弹出2;
4 > 3 → 弹出3;
4 > 5?否,结果=5;入栈4
[-1, 7, 5, 3, 5]
61[7, 5, 4]1 < 4,结果=4;入栈1[-1, 7, 5, 3, 5, 4]
76[7, 5, 4, 1]6 > 1 → 弹出1;
6 > 4 → 弹出4;
6 > 5 → 弹出5;
6 < 7,结果=7;入栈6
[-1, 7, 5, 3, 5, 4, 7]
89[7, 6]9 > 6 → 弹出6;
9 > 7 → 弹出7;
栈空,结果=-1;入栈9
[-1, 7, 5, 3, 5, 4, 7, -1]
98[9]8 < 9,结果=9;入栈8[-1, 7, 5, 3, 5, 4, 7, -1, 9]
最终结果:

[-1, 7, 5, 3, 5, 4, 7, -1, 9]

单调栈一般模版
在这里插入图片描述
关键点
  • 单调栈性质:栈中元素从栈底到栈顶递减(大→小),保证每次弹出的都是“不可能成为后续元素左边更大数”的元素。
  • 时间复杂度:O(n),每个元素最多入栈、出栈一次。
注意点
  • 单调栈是一种数据结构,用于维护可能成为答案的候选集合。
  • 当保持栈内的元素单调递增时,可以通过该结构找到左边离自己最近且比自己小的数。
  • 若从右向左遍历数组,则可以利用单调栈来查找右边离自己最近且比自己更大或更小的数。
  • 我们一般题目实际应用并不是直接按照这个模版求的,而是有些附加的条件,此时一般情况就是我们在进行出栈逻辑的时候也就是对应while循环里面,根据对应条件完成对应统计 计算等操作(结合题目要求),但是思想上都是一样的。
单调栈不同选型需求
目标遍历方向栈的类型弹出条件
左边第一个比它大从左向右单调递减栈当前元素 ≥ 栈顶
左边第一个比它小从左向右单调递增栈当前元素 ≤ 栈顶
右边第一个比它大从右向左单调递减栈当前元素 ≥ 栈顶
右边第一个比它小从右向左单调递增栈当前元素 ≤ 栈顶
优势
  • 时间复杂度 O(n):每个元素最多入栈、出栈一次;
  • 空间复杂度 O(n):最坏情况栈存储所有元素。

单调栈通过维护单调性,将暴力枚举的 O(n²) 问题优化为线性时间,是处理“相邻元素大小关系”类问题的利器。

引入单调栈

根据暴力的思路(它左边比它大的,最大值与它右边比它大的最大值求个min然后再减去当前位置高度即水的高度),进行优化–>是否能转化成左边离它最近比它大的以及右边离它最近比它大的---->引入单调栈(这里有两种选择):

  • 1·单调递减栈( 即右边第一个比它大 )
  • 2·单调递减栈(即左边第一个比它大 )

下面我们选择第二种进行解答。

虽然这种方法理解起来比较抽象,但是博主尽量讲明白:

只要小于top位置的值就一直入栈,直到大于的时候的时候进行出栈处理(也就是找到第一个凹槽处,下面就开始从右往左进行求接入雨水的量了),直到栈出完了,最后退出;下面重复对应操作来找凹槽

下面画图说明下:

在这里插入图片描述
  • 此时我们只需要在出栈逻辑(即while循环时刻)进行对应统计相关操作即可。
  • 因此可以看出,它的处理逻辑是出现一个凹槽,然后从这个凹槽的右边位置开始处理(也就是从右到左的处理逻辑),之前位置处理过之后就默认填平了,因此它的左边在进行处理的时候就可以认为右边对应空位已经填平了就行。

详细代码(注释详解):

classSolution{public:inttrap(vector<int>&height){int ans =0; stack<pair<int,int>> s;//保存对应的索引以及高度 s.push(make_pair(0, height[0]));int Min,w,h;for(int i =1; i < height.size(); i++){// 进行维护的单调递减栈的检查while(!s.empty()&& height[i]> s.top().second){int mid = s.top().second;//为了求top--mid--i位置能接的最大雨水 s.pop();//如果当前i位置位置比我们要求接雨水位置小,必然top--mid--i范围内无法接入雨水,而栈是单调递减的,后面的也肯定如此,直接//break即可!if(height[i]< mid)break;//可能pop后为空了即,此次出栈任务完成,直接breakif(s.empty())break; Min =min(s.top().second, height[i]); w =abs(i - s.top().first)-1; h = Min - mid;//因为我们把可能与栈顶元素高度相同的也给入栈了,故此时可能mid和top相同因此面积及为0了,需要特判一下!if(w * h >0) ans += w * h;} s.push(make_pair(i, height[i]));}return ans;}};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

本篇小结

从暴力枚举到动态规划,再到双指针与单调栈,四种解法覆盖时间与空间复杂度的权衡。暴力法直观但低效,动态规划预处理打破嵌套循环,双指针进一步压缩空间,单调栈则以栈结构精准捕捉存水条件。掌握这些技巧,可灵活应对“区间极值”类问题。

Read more

信奥赛C++提高组csp-s之数论基础专题课:从同余到分数模运算1(知识地图:同余、裴蜀定理、扩展欧几里得、乘法逆元、分数模运算)

信奥赛C++提高组csp-s之数论基础专题课:从同余到分数模运算1(知识地图:同余、裴蜀定理、扩展欧几里得、乘法逆元、分数模运算)

信奥赛C++提高组csp-s之数论基础专题课:从同余到分数模运算1(知识地图:同余、裴蜀定理、扩展欧几里得、乘法逆元、分数模运算) 课程目标 1. 理清脉络:理解同余、裴蜀定理、扩展欧几里得、乘法逆元、分数模运算之间的逻辑关系。 2. 掌握核心:熟练运用扩展欧几里得算法求解不定方程及逆元。 3. 实战应用:能够解决相关的数论模板题和简单变式题。 第一部分:知识地图与衔接逻辑 这五个知识点可以看成是一条 “生产流水线”,最终目的是为了解决 “分数取模” 这个看似复杂的问题。 1. 起点:同余 * 这是所有问题的背景。我们研究的世界不再是整数,而是 “模 m 的世界”。在这里,所有的数都只关心它除以 m 的余数。 * 核心矛盾:在这个世界里,加法、减法、乘法都很好用,唯独 除法

By Ne0inhk
C++:二叉搜索树

C++:二叉搜索树

Hello大家好! 很高兴与大家见面! 给生活添点快乐,开始今天的编程之路。 我的博客:<但愿. 我的专栏:C语言、题目精讲、算法与数据结构、C++ 欢迎点赞,关注 目录   一  ⼆叉搜索树的性能分析和概念       1.1⼆叉搜索树的概念       1.2⼆叉搜索树的性能分析  二   ⼆叉搜索树增删查的实现,这里不支持修改(修改一个就可能不是二叉搜索树的结构了)       2.1⼆叉搜索树基本结构的实现                 2.1.1节点的定义(由于后面实现⼆叉搜索树要访问节点成员所以这里使用struct定义(默认是公有))                 2.1.2默认成员函数                         2.1.2.1⼆叉搜索树的基本结构                         2.1.2.2⼆叉搜索树的默认构造                         2.

By Ne0inhk
《C++ 递归、搜索与回溯》第2-3题:合并两个有序链表,反转链表

《C++ 递归、搜索与回溯》第2-3题:合并两个有序链表,反转链表

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 《Git深度解析》:版本管理实战全解 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 2. 合并两个有序链表 算法原理(递归): 思路: 解法代码(C++): 博主手记(字体还请见谅哈): 3. 反转链表 算法原理(递归): 思路: 解法代码(C++): 博主手记(字体还请见谅哈): 结尾: 前言: 聚焦算法题实战,系统讲解三大核心板块:“精准定位最优解”——优选算法,“简化逻辑表达,系统性探索与剪枝优化”——递归与回溯,“以局部最优换全局高效”——贪心算法,讲解思路与代码实现,帮助大家快速提升代码能力 2.

By Ne0inhk
2026春节近了!用C++为你送上烟花祝福!

2026春节近了!用C++为你送上烟花祝福!

2026新年烟花庆祝程序 效果展示 【2026春节来了!C++为你呈现烟花视觉盛宴】 第一部分:环境准备 1.1 系统要求 本程序设计在WSL(Windows Subsystem for Linux)的Fedora系统上运行,需要以下环境: * WSL 2.0或更高版本 * Fedora Linux发行版 * C++编译器(g++) * SFML图形库 1.2 安装依赖 在Fedora终端中执行以下命令: # 1. 更新系统包管理器sudo dnf update # 2. 安装C++编译器和构建工具sudo dnf install gcc-c++ make cmake # 3. 安装SFML图形库(注意:Fedora中SFML包名可能有差异)sudo dnf install SFML

By Ne0inhk