_ ___ _______
| |/ (_) |___ / |
| ' / _ _ __ __ _ / /| |__ __ _ _ __ __ _
| < | | '_ \ / _` | / / | '_ \ / _` | '_ \ / _` |
| . \| | | | | (_| |/ /__| | | | (_| | | | | (_| |
|_|\_\_|_| |_|\__, /_____|_| |_|\__,_|_| |_|\__, |
__/ | __/ |
|___/ |___/
基础队列
双端队列
双端队列可以有队列的(从尾插入,从尾弹出)当然也可以从头插入,从头弹出。
双端队列的表示
deque<int> q;
q.empty();///判断是否为空
q.size();///长度
q.front();///首元素
q.back();///尾元素
q.push_back();///从尾部插入
q.push_front();///从头部插入
q.pop_back();///将尾部元素弹出
q.pop_front();///从头部元素弹出
我们从双端队列的两头可进两头可出的特性得到,单调队列。—典型的题型就是“滑动窗口”.
例如:
洛谷 P1886 https://www.luogu.com.cn/problem/P1886
单调队列的板子题:
题目描述:有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如样例:
8 3
1 3 -1 -3 5 3 6 7
输出:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
其实这道题重点就是
- 维护长度为K的窗口。
- 维护区间最大/最小
从复杂度来看用暴力的话复杂度就是O(n*k);根据数据范围我们很清楚他会超时,所以我们应该怎么办那,这时神奇的单调队列出现了。(主要是利用了双端队列的两头可进可出的特性)。
- 思路就是首先看K窗口维护最大:
- 我们要维护最大值的话就需要把区间最大值维护在首或者尾,因为双端队列的特性,其实维护首 / 尾并没有太大的差别,怎么好像怎么来,我们那维护在首为例,维护的队列中如果首元素(也就是区间最大)的序号与最后一个元素维护的序号相差不能超过K因为我们要求得是长度为K的区间,大于这个长度输入外来的,不能要。所以头过长弹出。头我们正好了。因为我们想头维护区间最大值,所以头就是我要求的结果,这种操作统称为去头。
- 进一步引进了个问题如何维护区间的最大值?:这样想,区间最大值,那么我们就要维护一个单调递减的队列。这样就可以一直维持最大元素在首元素。
就像这个图一样,你要把8拿进去那么5你就得弹出来但是这个区间中最大的是10,所以10保留,那么5直接弹出就行,因为这个滑动窗口,有5的地方一定会有8,有8的话那么最大值就不会是5.这种操作统称为去尾 - 这样就把
去
头
,
去
尾
去头,去尾
去头,去尾一结合那么就完美的解决了维护长度为K和维护区间最大值的两个问题。
最小值也是按照这个思路来想,就是把维护最大值改成最小值。
/**
_ ___ _______
| |/ (_) |___ / |
| ' / _ _ __ __ _ / /| |__ __ _ _ __ __ _
| < | | '_ \ / _` | / / | '_ \ / _` | '_ \ / _` |
| . \| | | | | (_| |/ /__| | | | (_| | | | | (_| |
|_|\_\_|_| |_|\__, /_____|_| |_|\__,_|_| |_|\__, |
__/ | __/ |
|___/ |___/
**/
#include <map>
#include <queue>
#include <string>
#include<iostream>
#include<stdio.h>
#include<string.h>
#include <algorithm>
#include <bits/stdc++.h>
#include <math.h>
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
typedef pair<ll, ll> pii;
#define mem(a,x) memset(a,x,sizeof(a))
#define debug(x) cout << #x << ": " << x << endl;
#define rep(i,n) for(int i=0;i<(n);++i)
#define repi(i,a,b) for(int i=int(a);i<=(b);++i)
#define repr(i,b,a) for(int i=int(b);i>=(a);--i)
const int maxn = 1e6 + 1010;
#define inf 0x3f3f3f3f
#define sf scanf
#define pf printf
const int mod = 998244353;
const int MOD = 10007;
inline int read() {
int x = 0;
bool t = false;
char ch = getchar();
while((ch < '0' || ch > '9') && ch != '-')ch = getchar();
if(ch == '-')t = true, ch = getchar();
while(ch <= '9' && ch >= '0')x = x * 10 + ch - 48, ch = getchar();
return t ? -x : x;
}
/*
vector<ll> m1;
vector<ll> m2;
priority_queue<ll , vector<ll> , greater<ll> > mn;//上 小根堆 小到大
priority_queue<ll , vector<ll> , less<ll> > mx;//下 大根堆 大到小
map<ll,ll>mp;*/
deque<ll > q;
ll a[maxn], b[maxn];
#define read read()
int main() {
/**
q.empty();///判断是否为空
q.size();///长度
q.front();///首元素
q.back();///尾元素
q.push_back();///从尾部插入
q.push_front();///从头部插入
q.pop_back();///将尾部元素弹出
q.pop_front();///从头部元素弹出
**/
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
sf("%lld", &a[i]);
}
for(int i = 1; i <= n; i++) {///最小值
///队尾元素大于现加入的就把队尾元素弹出
while(!q.empty() && a[q.back()] > a[i]) q.pop_back(); ///去尾
q.push_back(i);
if(i >= m) {
///长度超过m的元素弹出
while(!q.empty() && q.front() <= i - m) { ///去头
q.pop_front();
}
pf("%lld ", a[q.front()]);///输出区间最小
}
}
cout << endl;
while(!q.empty()) q.pop_front();
for(int i = 1; i <= n; i++) {///最大值
///同上就是把大小反过来
while(!q.empty() && a[q.back()] < a[i]) q.pop_back();
q.push_back(i);
if(i >= m) {
while(!q.empty() && q.front() <= i - m) {
q.pop_front();
}
pf("%lld ", a[q.front()]);
}
}
cout << endl;
return 0;
}
还有一种题型就是最大子段和
hdu 1003 http://acm.hdu.edu.cn/showproblem.php?pid=1003
题意很简单明了,就是求最大字段和和开始位置和结束位置。
方法一:贪心
思想就是,就算一个和,如果加上当前数 > 0 (也就是和大于原本的最大值,就更新最大值和他的左端点),如果和小于零之后,就说明前面产生了负代价,就是相当于我们结果的累赘,我们就得重新计数来算和,就把计和器清空,并更新标记初始的位置。
/**
_ ___ _______
| |/ (_) |___ / |
| ' / _ _ __ __ _ / /| |__ __ _ _ __ __ _
| < | | '_ \ / _` | / / | '_ \ / _` | '_ \ / _` |
| . \| | | | | (_| |/ /__| | | | (_| | | | | (_| |
|_|\_\_|_| |_|\__, /_____|_| |_|\__,_|_| |_|\__, |
__/ | __/ |
|___/ |___/
**/
#include <map>
#include <stdio.h>
#include <string.h>
#include <queue>
#include <math.h>
#include <iostream>
#include <algorithm>
#include <string>
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
typedef pair<ll,ll> pii;
const int maxn=2e5+7;
const int inf = 0x7fffffff;
int main(){
int t;
cin>>t;
int T=0;
while(t--){
int n;
cin>>n;
int sum=0;
int maxx=-inf;
int top=1; ///记录首元素的位置
int down=1;///记录尾元素的位置
int flag=1;///记录每次更改的首元素的位置
for(int i=1;i<=n;i++){
int x;
cin>>x;
sum+=x;
if(sum>maxx){///和大于最大和 更新最大和 和 对应的首元素和尾元素
maxx=sum;///最大和
top=flag;///首地址 等于原本记录的哪个地址
down=i; ///尾地址 等于当前地址
}
if(sum<0){ /// 出现负数,说明前面产生负代价 归零
sum=0; /// 归零,重新计数
flag=i+1;/// 更新标记首地址
}
}
printf("Case %d:\n",++T);
printf("%d %d %d\n", maxx,top,down);
if(T!=t) cout << endl;
}
return 0;
}
方法二:DP
思路:其实中心思想还是贪心就是把记录标记的值放进DP里:推下状态转移方程吧
假设当前位置为
i
i
i那么他可以从两种状态转移过来
1.子段中有多个元素,他可以从前一个状态
i
−
1
i-1
i−1这个状态转移过来,也就会
a
[
i
]
+
d
p
[
i
−
1
]
a[i]+dp[i-1]
a[i]+dp[i−1].
2.子段中只有他自己本身,当然他也可以是他自己本身
a
[
i
]
a[i]
a[i].
然后这两状态取最优就是状态转移方程:
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
1
]
+
a
[
i
]
,
a
[
i
]
)
dp[i] = max(dp[i-1]+a[i],a[i])
dp[i]=max(dp[i−1]+a[i],a[i])
/**
_ ___ _______
| |/ (_) |___ / |
| ' / _ _ __ __ _ / /| |__ __ _ _ __ __ _
| < | | '_ \ / _` | / / | '_ \ / _` | '_ \ / _` |
| . \| | | | | (_| |/ /__| | | | (_| | | | | (_| |
|_|\_\_|_| |_|\__, /_____|_| |_|\__,_|_| |_|\__, |
__/ | __/ |
|___/ |___/
**/
#include <map>
#include <stdio.h>
#include <string.h>
#include <queue>
#include <math.h>
#include <iostream>
#include <algorithm>
#include <string>
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
typedef pair<ll,ll> pii;
const int maxn=2e5+7;
const int inf = 0x7fffffff;
int a[maxn],dp[maxn];
int main(){
int t;
cin>>t;
int T=0;
while(t--){
int n;
cin>>n;
int sum=0;
int maxx=-inf;
int top=1; ///记录首元素的位置
int down=1;///记录尾元素的位置
int flag=1;///记录每次更改的首元素的位置
for(int i=1;i<=n;i++) cin>>a[i];///输入
///也可以直接输入dp[i]这样就是不用a数组也行。
maxx=a[1];///初始值为第一个元素。
dp[1]=a[1]; ///初始赋值
for(int i=2;i<=n;i++){
if(dp[i-1]+a[i]>=a[i]){///如果从前一个状态转移过来的大,就直接赋值,这样不用更新初始点
dp[i]=dp[i-1]+a[i];
}else {/// 就是只有他本身更那一个 并且更新标记
dp[i]=a[i];
flag=i;
}
if(dp[i]>maxx){///比较大小跟贪心一样。
maxx=dp[i];
top=flag;
down=i;
}
}
printf("Case %d:\n",++T);
printf("%d %d %d\n", maxx,top,down);
if(T!=t) cout << endl;
}
return 0;
}
栈
stl栈的各种操作
stack<int> s;
在#include<stack> 这个头文件中
stack<int> s;///定义栈
s.push(x);///将x放入栈首
s.top();///返回栈首元素
s.pop();///弹出栈首元素
s.size();///栈长度
s.empty();///判断栈是否为空
栈的特性就是:后进先出
可以想成一个容器,先放的在下面,要拿下面的的先把上面的拿出来,所以后进的先出
单调栈
单调栈有单调递增,还有单调递减栈。
中心思想就是比较栈首元素和加入元素的大小。
例:
洛谷 P2947 https://www.luogu.com.cn/problem/P2947
**题意:**就是给出一个
N
N
N ,再给你
N
N
N高度,让你找每个向右找第一个大于他的下标,也就是当
i
<
j
i < j
i<j时让你找
a
i
<
a
j
a~i~ <a~j~
a i <a j 那么
j
j
j就是
i
i
i的答案,没有比ai大 的输出0;
**思路分析:**这个东西我们从前往后分析是不好分析的,因为我们看每一个的话,得再去他后面找答案,这样就让我们的计算变得多而繁琐,既然不能正这想那就画个思路,反着想,从后往前看,这样的话,这样想当前从后向前构造一个单调递增的栈,下标为
i
i
i把它与栈顶元素比较,如下图所示
就是比较
10
10
10和
9
9
9的大小,10大就把9弹出去,然后在比较,最后栈首的下标就是
i
i
i答案,然后把ai放入栈中,这样想,9能够办好的事10一定能办好,而且10离的近,为什么还要要9那,10都完不成的事,9更不能完成。就这样维护一个单调递增的栈。就能把这个题整出来。
/**
_ ___ _______
| |/ (_) |___ / |
| ' / _ _ __ __ _ / /| |__ __ _ _ __ __ _
| < | | '_ \ / _` | / / | '_ \ / _` | '_ \ / _` |
| . \| | | | | (_| |/ /__| | | | (_| | | | | (_| |
|_|\_\_|_| |_|\__, /_____|_| |_|\__,_|_| |_|\__, |
__/ | __/ |
|___/ |___/
**/
#include <stack>
#include <map>
#include <stdio.h>
#include <string.h>
#include <queue>
#include <math.h>
#include <iostream>
#include <algorithm>
#include <string>
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
typedef pair<ll, ll> pii;
const int maxn = 2e5 + 7;
const int inf = 0x7fffffff;
int a[maxn],b[maxn];
int main(){
int n,m;
cin>>n;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
stack<int>s;
for(int i=n;i>=1;i--){
while(!s.empty()&&a[s.top()]<=a[i]) s.pop();///维护单调递增的栈
if(s.empty()){///栈为空就说明,没有比他大的,答案就是0
b[i]=0;
}else {///否则就是她栈首元素
b[i]=s.top();
}
s.push(i);///把当前元素入栈
}
for(int i=1;i<=n;i++){
cout<<b[i]<<endl;
}
return 0;
}
堆
堆排序
对顶堆
加点东西:map上用pair<int ,int >;
pii x;
x.first=1;
x.second=2;
mp[x]=1;
mp[std::pair<ll,ll>(1,2)]=1;
其实很想简单的问题让我想复杂了,就昨天蓝桥杯我用到这个地方,然后在比赛的时候整出来了,平时用的话都没整出来。