代码随想录算法训练营第42天 | 第九章动态规划 part2

发布于:2024-10-16 ⋅ 阅读:(7) ⋅ 点赞:(0)

第十章 单调栈 part02

42. 接雨水

接雨水这道题目是面试中特别高频的一道题,也是单调栈应用的题目,大家好好做做。

建议是掌握双指针和单调栈,因为在面试中写出单调栈可能有点难度,但双指针思路更直接一些。

在时间紧张的情况下,能写出双指针法也是不错的,然后可以和面试官慢慢讨论如何优化。

传送门:0042.接雨水

示例数组:

在这里插入图片描述
height = [ 0 , 1 , 0 , 2 , 1 , 0 , 1 , 3 , 2 , 1 , 2 , 1 ] \text{height} = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] height=[0,1,0,2,1,0,1,3,2,1,2,1]

过程解释表格:

i height[i] 栈内容 (st) 栈中高度 (对应 height) 弹出 mid 下标 左边界 (st.top()) 右边界 (i) 宽度 (w) 高度 (h) 当前累计水量 总水量 (sum)
0 0 [0] [0] - - - - - - 0
1 1 [0, 1] [0, 1] - - - - - - 0
2 0 [0, 1, 2] [0, 1, 0] - - - - - - 0
3 2 [0, 1] [0, 1] 2 1 3 1 1 1 1
3 2 [0] [0] 1 0 3 2 0 0 1
3 2 [0, 3] [0, 2] - - - - - - 1
4 1 [0, 3, 4] [0, 2, 1] - - - - - - 1
5 0 [0, 3, 4, 5] [0, 2, 1, 0] - - - - - - 1
6 1 [0, 3, 4] [0, 2, 1] 5 4 6 1 1 1 2
6 1 [0, 3, 4, 6] [0, 2, 1, 1] - - - - - - 2
7 3 [0, 3, 4] [0, 2, 1] 6 4 7 2 1 2 4
7 3 [0, 3] [0, 2] 4 3 7 3 1 3 7
7 3 [0] [0] 3 0 7 6 0 0 7
7 3 [0, 7] [0, 3] - - - - - - 7
8 2 [0, 7, 8] [0, 3, 2] - - - - - - 7
9 1 [0, 7, 8, 9] [0, 3, 2, 1] - - - - - - 7
10 2 [0, 7, 8] [0, 3, 2] 9 8 10 1 1 1 8
10 2 [0, 7] [0, 3] 8 7 10 2 0 0 8
10 2 [0, 7, 10] [0, 3, 2] - - - - - - 8
11 1 [0, 7, 10, 11] [0, 3, 2, 1] - - - - - - 8

过程解析:

  1. 栈中高度的追踪:每当一个新柱子入栈时,我们不仅记录其索引,还可以追踪其对应的高度。这样可以很直观地看出栈中保存的每个柱子的高度是如何变化的。

  2. 形成凹槽时弹出的逻辑:每次当 height[i] > height[st.top()] 时,我们弹出栈顶元素(即 mid),并用栈中新的顶元素(st.top())作为左边界,当前元素 i 作为右边界,计算出宽度和高度差,然后累加水量。

  3. 水量计算示例

    • i = 3 时,弹出 2,左右边界分别是 13,宽度为 1,高度差为 1,水量为 1
    • i = 7 时,弹出多个柱子形成多个凹槽,水量分别累加。

这题确实挺难的,看了一遍视频后,再看文字,依然搞不懂。琢磨了半天。还是又看了一遍,才又懂了一点。确实对于不懂的东西,不要硬干,要老老实实的再看一遍。

 stack<int> st;
        int sum=0;
        for(int i=0;i<height.size();i++)
        {
            while(!st.empty()&&height[i]>=height[st.top()])//发现有更高的右边界
            {       
                int mid = st.top();//单调栈第一个拿来当作盛水的低
                st.pop();//拿来用就给扔了,没用了
                if (!st.empty()) {//看下单调栈是否为空,别是空的,保证左边能盛水
        int h = min(height[st.top()], height[i]) - height[mid];//这是找左边最大值
        int w = i - st.top() - 1; // 注意减一,只求中间宽度
        sum += h * w;
    }
            }//注意while还在循环,因为右边多了一组墙,左边多了几组雨水        
            st.push(i);//把当前这个最大值扔进去,当作左边的墙
        }
        return sum;
    }

部分内容看代码注释。

双指针法

class Solution {
public:
    int trap(vector<int>& height) {
    int sum=0;
    vector<int> maxLeft(height.size(), 0);
    vector<int> maxRight(height.size(), 0);

    for(int i=1;i<height.size()-1;i++)
    {
          maxLeft[i]=max(maxLeft[i-1],height[i-1]);     
    }
    for(int i=height.size()-2;i>0;i--)
    {
          maxRight[i]=max(maxRight[i+1],height[i+1]);     
    }
    for(int i=1;i<height.size()-1;i++)
    {
          sum+=max(min(maxLeft[i],maxRight[i])-height[i],0);     
    }
    return sum;
    }
};

双指针法还是比较好理解的。稍微用点动态规划的简单知识。记录每个柱子左边柱子最大高度,记录每个柱子右边柱子最大高度,求和。三个for循环。简简单单。

84. 柱状图中最大的矩形

有了之前单调栈的铺垫,这道题目就不难了。

双指针法

class Solution {
public:
    int largestRectangleArea(vector<int>& height) {
 int result=0;
 int n=height.size();
    vector<int> minLeft(n, 0);
    vector<int> minRight(n, 0);
    minLeft[0] = -1;
    for(int i=1;i<n;i++)
    {
        int t = i - 1;
        while (t >= 0 && height[t] >= height[i]) t = minLeft[t];
            minLeft[i] = t; 
    }
    minRight[n - 1] = n; 
    for(int i=n-2;i>=0;i--)
    {
         int t = i + 1;
            // 这里不是用if,而是不断向右寻找的过程
            while (t < n && height[t] >= height[i]) t = minRight[t];
            minRight[i] = t;
    }
    for(int i=0;i<n;i++)
    {
         int sum = height[i] * (minRight[i] - minLeft[i] - 1);
            result = max(sum, result);
    }
    return result;
    }
    
};

双指针法还是比较简单的。最重要的是知道循环找右边的思路

单调栈法

传送门:0084.柱状图中最大的矩形
在 height数组上后,都加了一个元素0, 为什么这么做呢?

首先来说末尾为什么要加元素0?

如果数组本身就是升序的,例如[2,4,6,8],那么入栈之后 都是单调递减,一直都没有走 情况三 计算结果的哪一步,所以最后输出的就是0了。
那么结尾加一个0,就会让栈里的所有元素,走到情况三的逻辑。

开头为什么要加元素0?

如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),right(6),但是得不到 left。

(mid、left,right 都是对应版本一里的逻辑)

因为 将 8 弹出之后,栈里没有元素了,那么为了避免空栈取值,直接跳过了计算结果的逻辑。

之后又将6 加入栈(此时8已经弹出了),然后 就是 4 与 栈口元素 6 进行比较,周而复始,那么计算的最后结果result就是0。

最后发现,单调栈的思路有点像砍山头。逐渐把前面的山头给砍平。看懂下面的过程,差不多

1 3 5 2 1 3 1 面积
1 3 5 2
1 3 5 2 5
1 3 比前大 2 6
1 比前大 比前大 2 3
1 比前大 比前大 2 1 2
1 比前大 比前大 2 1 2
1 比前大 比前大 比前大 1 3
1 比前大 比前大 比前大 1 3 1 3
1 比前大 比前大 比前大 1 比前大 1 6
1 1 1 1 1 1 1 7
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        stack<int> st;
        st.push(0);
        heights.insert(heights.begin(), 0); // 数组头部加入元素0
        heights.push_back(0); // 数组尾部加入元素0
    
        int result = 0;
        int n=heights.size();
        for(int i=1;i<n;i++)
        {
        while (heights[i] < heights[st.top()]) {
                int mid = st.top();//最顶就是st.top()
                st.pop();//用了就扔了,不用再管他了,它已经到极限了
                int w = i - st.top() - 1;//确定宽
                int h = heights[mid];//确定高
                result = max(result, w * h);//比较是否最大
            }//注意一直循环,只有有更大的就削山头。直到单调栈。
            st.push(i);//加入进去,准备下一个
        }
    return result;
    }
    
};

终于至少把《代码随想录》这本书刷完了。自己效率确实挺低的,两个多月才刷到这。图论不打算刷了,一方面卡玛网不太喜欢,一方面前面够我消化的了,还是不太想刷图论了。还是很有收获。确实自己速度很慢。但也已经尽力了。最重要的还是感觉自己没有什么动力,感觉自己刷题更多的是为了逃避现实吧。反而没有经历搞自己的学业。难搞,反正先暂停一段时间,搞搞课题。