单调栈

This commit is contained in:
yinkanglong_lab
2021-03-27 06:13:21 +08:00
parent cf638659dd
commit e284cf0070
11 changed files with 407 additions and 6 deletions

View File

@@ -0,0 +1,52 @@
# 数组离散化
## 1 问题描述
* 离散化一个序列的前提是我们只关心这个序列里面元素的相对大小,而不关心绝对大小(即只关心元素在序列中的排名);离散化的目的是让原来分布零散的值聚集到一起,减少空间浪费。那么如何获得元素排名呢,我们可以对原序列排序后去重,对于每一个$a_i$通过二分查找的方式计算排名作为离散化之后的值。当然这里也可以不去重,不影响排名。
* 数组本质上是一种有序的映射。i----A---->x 即(A[i]=x)的映射。有时候为了建立值的数量与坐标i的反向映射但此时值x的范围是离散化的。需要建立离散数组。一个的额外的映射数组完成反向映射x----B---->i。
## 2 离散数组——数组实现方法
* 使用数组B作为离散数组的映射。B的下标i能够反映原来数组中各个元素的大小顺序。但不是原来数组中各个元素的值。
* 建立离散数组的时间复杂度为O(nlog n)需要对原数组进行快速排序。
![](image/2021-03-27-04-40-18.png)
代码实现
```C++
vector<int> nums;//表示原来的数组
vector<int> tmp = nums;
//对离散数组进行排序
sort(tmp.begin(), tmp.end());
//建立原数组到离散数组的映射
for (int& num: nums) {
num = lower_bound(tmp.begin(), tmp.end(), num) - tmp.begin() + 1;
}
```
## 3 离散数组——有序map实现方法
* 有序map的创建底层使用红黑树实现。本质上就是一种离散数组。根据键的大小进行了排序。
* 可以使用这种有序的映射直接完成原数组的值到下表i的映射。并且保持一定的顺序。
```
#include<iostream>
#include<map>
using namespace std;
int main(){
map<int,int> m;
m[10]=10;
m[4]=4;
m[5]=5;
m[1000]=1000;
m[1]=1;
for(auto k:m){
cout<<k.first<<endl;
}
return 0;
}
```

18
C++/面试/18.cpp Normal file
View File

@@ -0,0 +1,18 @@
#include<iostream>
#include<map>
using namespace std;
int main(){
map<int,int> m;
m[10]=10;
m[4]=4;
m[5]=5;
m[1000]=1000;
m[1]=1;
for(auto k:m){
cout<<k.first<<endl;
}
// cout<<map[3].first<<endl;
return 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

View File

@@ -0,0 +1,9 @@
## 计划
- [ ] 单调栈的总结
- [ ] 双指针环的入接点总结
- [ ] 字符串分割与格式化方法总结
- [ ] 位运算与补码总结
- [ ] 位运算与大数乘法总结
## 收获

View File

@@ -0,0 +1,54 @@
# 单调栈
## 定义
* 单调栈中存放的数据应该是有序的,所以单调栈也分为单调递增栈和单调递减栈
* 单调递增栈:单调递增栈就是从栈底到栈顶数据是从小到大
* 单调递减栈:单调递减栈就是从栈底到栈顶数据是从大到小
## 规则
* 单调递减栈现在有一组数1037412。从左到右依次入栈则如果栈为空或入栈元素值小于栈顶元素值则入栈否则如果入栈则会破坏栈的单调性则需要把比入栈元素小的元素全部出栈。
* 0入栈时栈为空直接入栈栈内元素为10。
* 入栈时栈顶元素10比3大则入栈栈内元素为103。
* 入栈时栈顶元素3比7小则栈顶元素出栈此时栈顶元素为10比7大则7入栈栈内元素为107。
* 入栈时栈顶元素7比4大则入栈栈内元素为1074。
* 2入栈时栈顶元素4比12小4出栈此时栈顶元素为7仍比12小栈顶元素7继续出栈此时栈顶元素为10仍比12小10出栈此时栈为空12入栈栈内元素为12。
## 应用
* 局部顺序问题,可以使用单调栈解决。例如存在一个局部增序的序列。需要找到所有的局部增序的序列。则单调增栈的每次连续入栈,都是增序。每次连续出栈都是违反增序规则的。
* 单调递减栈:
* 在一个队列中针对每一个元素从它右边寻找第一个比它大的元素
* 在一个队列中针对每一个元素从它左边寻找第一个比它大的元素(从后往前遍历)
* 可以以 O(1) 的时间复杂度得知某个位置左右两侧比他大(或小)的数的位置,当你需要高效率获取某个位置左右两侧比他大(或小)的数的位置的的时候就可以用到单调栈。
* 求解数组中元素右边第一个比它小的元素的下标从前往后构造单调递增栈比它小的元素会将其pop掉。
* 求解数组中元素右边第一个比它大的元素的下标从前往后构造单调递减栈比它大的元素会将其pop掉。
* 求解数组中元素左边第一个比它大的元素的下标从后往前构造单调递减栈比他大的元素会将其pop掉
* 求解数组中元素左边第一个比它小的元素的下标从后往前构造单调递增栈。比他小的元素会将其pop掉。
## 代码
* 单调递增栈
```
for(int i = 0; i < T.size(); i++){
while(! stk.empty() && stk.top() > T[i]){
stk.pop();
}
stk.push(A[i]);
}
```
* 单调递减站
```
for(int i = T.size() - 1; i >= 0; i--){
while(! stk.empty() && T[i] >= stk.top()){
stk.pop();
}
stk.push(i);
}
```

View File

@@ -54,12 +54,13 @@ $$
## 3 应用
### 把位置x的元素都加k
### 把位置k的元素加x
* 我们每次执行操作A把位置x的值+k),只需要把"能管理到x的所有位置"都+k就行
1. A[i]+k
2. i=i+lowbit(i)
3. 重复以上步骤直到i>max截止
* 我们每次执行操作A把位置k的值+x),只需要把"能管理到k的所有位置"都+x就行
1. i=k
2. A[i]+x
3. i=i+lowbit(i)
4. 重复2-3步骤直到i>max截止
### 求前缀区间的值
@@ -79,13 +80,26 @@ $$
sum(L,R)=sum(1,R)-sum(1,L)
$$
### 创建树状数组
* 将树状数组初始化为0.依次添加位置k的元素添加x。创建数组的时间复杂度为O(nlog n)
## 实例1 基本运算
```C++
//求最小幂2^k:
int Lowbit(int t)
{
return t & ( t ^ ( t - 1 ) );
return t & ( t ^ ( t - 1 ) );//把最后一个1变为0然后再按位与。
//return t&(-t)
//return t&(~t+1)
}
// 创建树状数组
int tree_array(){
for(int i=1;i<)
}
//求前n项和

View File

@@ -18,6 +18,7 @@
|-----| -----|
n & (n - 1) | n中的最后一个1变成0。
n & (\~n + 1) | lowbit()运算n中最后一个1保留。
(\~n) + 1== -n | 计算机中补码表示的-n。位运算取反
n/2 | 等价于 右移一位 n >> 1
n*2 | 等价于 左移一位 n << 1
n % 2 |等价于 判断二进制最右一位值 n \& 1

View File

@@ -0,0 +1,253 @@
# 单调栈
* 用来解决**局部顺序问题**
## 1 视野总和
### 问题描述
* 有n个人站队所有的人全部向右看个子高的可以看到个子低的发型给出每个人的身高问所有人能看到其他人发现总和是多少。
```
输入4 3 7 1
输出2
解释个子为4的可以看到个子为3的发型个子为7可以看到个子为1的身高所以1+1=2
```
* []()
### 问题分析
* 问题分类:
* 思路:观察题之后,我们发现实际上题目转化为找当前数字向右查找的第一个大于他的数字之间有多少个数字,然后将每个  结果累计就是答案但是这里时间复杂度为O(N^2),所以我们使用单调栈来解决这个问题。
### 策略选择
* 数据结构:单调栈
* 算法思想:局部降序统计
### 算法设计
1. 设置一个单调递减栈
2. 当入栈的的时候,栈内的人肯定能看到该元素。
### 算法分析
* 时间复杂度为O(N)
* 空间复杂度O(N)
### 算法实现
```C++
int FieldSum(vector<int>& v)
{
stack<int> s;
int sum = 0;
for (int i = 0; i < (int)v.size(); i++)
{
//除去违反规则的人
while (!st.empty() && v[st.top()] <= v[i])s.pop();
// 增加能看到该元素的人
sum+=s.size();
st.push(i);
}
return sum;
}
```
## 2 柱状图中的最大矩形
### 问题描述
![](image/2021-03-27-05-15-13.png)
### 问题分析
### 策略选择
* 思路当前的数字可以向两边拓展遇到比自己大的就接着拓展小的就停止然后用自己的高度乘以拓展的宽度每次更新最大面积时间复杂度同样为O(N^2),所以我们接着借助单调栈
### 算法设计
1. 设置一个单调递增栈
2. 当遇到小于栈顶元素的值,我们开始更新数据,因为有可能最大面积就会出现在栈中的序列里
3. 牢记栈中数据永远是有序的,这个问题比较复杂,所以读者不妨对照着代码来理解问题
### 算法分析
* 时间复杂度为O(N)
* 空间复杂度O(N)
### 算法实现
```
int largestRectangleArea(vector<int>& heights) {
heights.push_back(-1);/同理,我们希望栈中所有数据出栈,所以给数组最后添加一个负数
stack<int> st;
int ret = 0, top;
for (int i = 0; i < heights.size(); i++)
{
if (st.empty() || heights[st.top()] <= heights[i])
{
st.push(i);
}
else
{
while (!st.empty() && heights[st.top()] > heights[i])
{
top = st.top();
st.pop();
//i-top指的是当前矩形的宽度heights[top]就是当前的高度
//再次强调栈中现在为单调递增
int tmp = (i - top)*heights[top];
if (tmp > ret)
ret = tmp;
}
st.push(top);
heights[top] = heights[i];
}
}
return ret;
}
```
## 3 求最大区间
### 问题描述
* 描述:给出一组数字,求一区间,使得区间元素和乘以区间最小值最大,结果要求给出这个最大值和区间的左右端点
```
输入3 1 6 4 5 2
输出60
3 5
```
* 解释将3到56+4+5这段区间相加将和与区间内最小元素相乘获得最大数字60
### 问题分析
### 策略选择
* 思路:使用暴力解法求出所有区间,再求出区间的最小值相乘跟新数据,并不是一种很好的算法,所以经过上面俩题的磨  炼,此时我们应该使用一个单调递减栈
### 算法设计
1. 设置一个单调递增栈
2. 当遇到小于栈顶元素的值,我们开始更新数据,因为当前遇到的值一定是当前序列最小的
### 算法分析
* 时间复杂度O(n)
* 空间复杂度O(n)
### 算法实现
```C++
int GetMaxSequence(vector<int>& v)
{
stack<int> st;
vector<int> vs(v.size()+1);
vs[0] = 0;
for (int i = 1; i < vs.size(); i++)
{
vs[i] = vs[i - 1] + v[i-1];
}
v.push_back(-1);
int top, start, end, ret = 0;
for (int i = 0; i < v.size(); i++)
{
if (st.empty() || v[st.top()] <= v[i])
{
st.push(i);
}
else
{
while (!st.empty() && v[st.top()] > v[i])
{
top = st.top();
st.pop();
int tmp = vs[i] - vs[top];
tmp = tmp * v[top];
if (tmp > ret)
{
ret = tmp;
start = top+1;
end = i;
}
}
st.push(top);
v[top] = v[i];//与第二题相同的道理将当前数据的更改最左的top下标防止出现比当前数据更小的数据
//这句在这道题里真的超级难理解,但是只要你有耐心相信你可以理解的
}
}
return ret
}
```
## 4 132 模式
### 问题描述
* 给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成并同时满足i < j < k 和 nums[i] < nums[k] < nums[j] 。
* 如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。
* 进阶:很容易想到时间复杂度为 O(n^2) 的解决方案,你可以设计一个时间复杂度为 O(n logn) 或 O(n) 的解决方案吗?
* 示例 1
```
输入nums = [1,2,3,4]
输出false
解释:序列中不存在 132 模式的子序列。
```
* 示例 2
```
输入nums = [3,1,4,2]
输出true
解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。
```
### 问题分析
### 策略选择
### 算法设计
* 因此,我们可以使用单调栈作为维护 22 的数据结构,并给出下面的算法:
* 我们用单调栈维护所有可以作为 22 的候选元素。初始时,单调栈中只有唯一的元素 \textit{a}[n-1]a[n1]。我们还需要使用一个变量 \textit{max\_k}max_k 记录所有可以真正作为 22 的元素的最大值;
* 随后我们从 n-2n2 开始从右到左枚举元素 a[i]a[i]
* 首先我们判断 a[i]a[i] 是否可以作为 11。如果 a[i] < \textit{max\_k}a[i]<max_k那么它就可以作为 11我们就找到了一组满足 132132 模式的三元组;
* 随后我们判断 a[i]a[i] 是否可以作为 33以此找出哪些可以真正作为 22 的元素。我们将 a[i]a[i] 不断地与单调栈栈顶的元素进行比较,如果 a[i]a[i] 较大,那么栈顶元素可以真正作为 22将其弹出并更新 \textit{max\_k}max_k
* 最后我们将 a[i]a[i] 作为 22 的候选元素放入单调栈中。这里可以进行一个优化,即如果 a[i] \leq \textit{max\_k}a[i]≤max_k那么我们也没有必要将 a[i]a[i] 放入栈中,因为即使它在未来被弹出,也不会将 \textit{max\_k}max_k 更新为更大的值。
* 在枚举完所有的元素后,如果仍未找到满足 132132 模式的三元组,那就说明其不存在。
### 算法分析
* 时间复杂度O(n)。
* 空间复杂度O(n)
### 算法实现
```
class Solution {
public:
bool find132pattern(vector<int>& nums) {
stack<int> s;
int mask2=INT_MIN;
// 找比它大的第一个数。单调递减栈。大的pop出去
for(int i=nums.size()-1;i>=0;i--){
if(nums[i]<mask2)return true;
while(!s.empty() && nums[i]>s.top()){
if(mask2<s.top())mask2=s.top();
s.pop();
}
s.push(nums[i]);
}
return false;
}
};
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB