练这个专题的原因是被多校赛第三场 Problem A. Ascending Rating 虐了, 看别人博客发现只有一句解释:单调队列的应用 。
这才决定要把单调队列单调栈摸熟(感谢多校赛虐我千百遍)
1、单调队列
给出下标为1 -- n 的数组 ,从左到右扫(也可以从右向左), 对于当前元素i , 从队尾不断地把小于a[i]的出队(规则自己定)
维护一个从队头到队尾递减的队列。多用于维护区间内最大值, 或者区间内递增序列包含的元素个数。
2)Problem A. Ascending Rating
题意:
给1-n数组,每次移动长度m的窗口,求窗口第一个元素 到 窗口内最大元素之间递增序列个数, 和最大元素的值(具体题意不是这样的 ,只不过求得上诉数据再处理一下就得到答案了)
思路:
本题考虑的是对于 i 而言,[i,i+m-1]区间对 i位置 的影响, 因此此题从右往左扫。满足区间长度为m 后才开始计算值
维护一个长度为m的严格递减单调队列, 队列里的内容就是当前元素a[i] 向后扫的 严格大于a[i]的元素。且队首元素就是区间m内的最大值
代码:
#include <iostream>
#include <cstdio>
#include <map>
#include <cmath>
#include <queue>
#include <cstring>
using namespace std;
const int maxn = 1e7+5;
typedef long long ll;
ll a[maxn], qq[maxn];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
ll n,m,k,p,q,r,mod;
scanf("%lld%lld%lld%lld%lld%lld%lld",&n,&m,&k,&p,&q,&r,&mod);
ll i=1;
for(; i<=k; i++)
scanf("%lld",&a[i]);
while(i<=n)
{
a[i] = (p*a[i-1]+q*i+r)%mod;
i++;
}
ll tail = 0,head = 1; // head 队头 tail队尾,因为是 ++tail = i 所以tail置0
ll ansa = 0,ansb = 0;
for(ll i=n;i;i--)
{
while(tail>=head && a[qq[tail]] <= a[i]) // 当前元素 ,把队尾的小弱鸡全踢出去
tail--;
qq[++tail] = i;
if(i<=n-m+1) // 后面区间长度 m
{
while(tail>=head && qq[head]>i+m-1) // 超出区间m了
head++;
ansa += i^a[qq[head]];
ansb += i^(tail-head+1);
}
}
printf("%lld %lld\n",ansa,ansb);
}
return 0;
}
2)淹没木板
转自https://www.cnblogs.com/tham/p/8038828.html
问题描述
地上从左到右竖立着 n 块木板,从 1 到 n 依次编号,如下图所示。我们知道每块木板的高度,在第 n 块木板右侧竖立着一块高度无限大的木板,现对每块木板依次做如下的操作:对于第 i 块木板,我们从其右侧开始倒水,直到水的高度等于第 i 块木板的高度,倒入的水会淹没 ai 块木板(如果木板左右两侧水的高度大于等于木板高度即视为木板被淹没),求 n 次操作后,所有 ai 的和是多少。如图上所示,在第 4 块木板右侧倒水,可以淹没第 5 块和第 6 块一共 2 块木板,a4 = 2。
思路:
这是个简单题,有多种思路, 从左向右扫, 把队尾元素小于当前元素的弹出队,1)则剩余在队列内的,就是当前能对其的贡献,ans += tail - head +1 。 2)弹出队的,当前点对他产生价值,ans += i - qq[tail]-1
代码:第一种
#include <iostream>
#include <cstdio>
#include <map>
#include <cmath>
#include <queue>
#include <cstring>
using namespace std;
const int maxn = 1e7+5;
typedef long long ll;
int a[maxn], qq[maxn];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n;
scanf("%d",&n);
int head = 1, tail = 0;
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
int ans = 0;
for(int i=1; i<=n; i++)
{
while(head <= tail && a[qq[tail]] < a[i])
tail --;
ans += tail - head + 1;
qq[++tail] = i;
}
printf("%d\n",ans);
}
return 0;
}
3)单调队列优化DP
经过几个月辛勤的工作,FJ决定让奶牛放假。假期可以在1…N天内任意选择一段(需要连续),每一天都有一个享受指数W。但是奶牛的要求非常苛刻,假期不能短于P天,否则奶牛不能得到足够的休息;假期也不能超过Q天,否则奶牛会玩的腻烦。FJ想知道奶牛们能获得的最大享受指数。
Input
第一行:N,P,Q.
第二行:N个数字,中间用一个空格隔开,每个数都在longint范围内。
Output
一个整数,奶牛们能获得的最大享受指数。
Sample Input
5 2 4
-9 -4 -3 8 -6
Sample Output
5
Data Constraint
50% 1≤N≤10000
100% 1≤N≤100000
1<=p<=q<=n
思路:
求区间和,那么就用前缀和思想 ,令sum【r】 = sun[r-1]+a[r] ,那么区间[l , r]的和就等于 sum[r] - sum[l - 1] 。我们令R固定,L的取值范围是 R-Q+1 <= L <= R-P+1 ,则状态转移方程为 DP[R] = DP [J] + a[R] ( R-Q <= L <= R-P max(DP[J]) )使用单调队列维护区间范围 [ R-Q , R-P] 内的sum[J]数组最小值。
代码:
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10050;
const int inf = 0x3f3f3f3f;
int dp[maxn], a[maxn], qq[maxn];
int main()
{
int n, p ,q ;
scanf("%d%d%d",&n,&p,&q);
a[0] = 0;
for(int i=1; i<=n; i++)
{ int t;
scanf("%d",&t);
a[i] = a[i-1] + t; // 前缀和
}
int head = 1, tail = 0;
int ans = -inf;
for(int i=p; i<=n; i++)
{
while(head <= tail && a[i-p] <= a[qq[tail]])
tail--;
while(head <= tail && qq[head]<i-q)
head++;
qq[++tail] = i-p;
ans = max(ans, a[i] - a[qq[head]]);
}
printf("%d\n",ans);
return 0;
}
2、单调栈
多用于找到当前点左边(右边) 第一个大于当前值(小于)的位置。
例如希望找到左边第一个比当前大的元素, 加入一个元素之前,对栈顶进行操作,遇到比他小的 ,出栈, 否则就是我们要的结果
还是淹没木板那题
发现做法和单调队列很像, 数组模拟仅仅是踢出元素方式不同, 不过在其他应用里 这两种数据结构还是有不一样的奇效
代码:
include <iostream>
#include <cstdio>
#include <map>
#include <cmath>
#include <queue>
#include <cstring>
using namespace std;
const int maxn = 1e7+5;
typedef long long ll;
const int inf = 0x3f3f3f3f;
int a[maxn], qq[maxn];
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
int n;
scanf("%d",&n);
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
int ans = 0;
int top = 0, bottom = 1;
a[n+1] = inf; // n+1 个板子,无穷大
for(int i=1; i<=n+1; i++)
{
while(top >= bottom && a[qq[top]] < a[i])
{
ans += i - qq[top] - 1; // 比如第三个板子 8 此时栈顶是 第二个板子5 则贡献是 3-2-1 = 0
top--;
}
qq[++top] = i;
}
printf("%d\n",ans);
}
return 0;
}