1.前言
兩個字 單調
单调栈是维护一个单调的栈,单调队列就是维护一个单调的队列,通过单调的性质解决问题。
2.单调队列简述
单调队列一般分为单调递增队列和单调递减队列。 单调递增队列是解决最小值的问题 单调递减队列是解决最大值的问题
滑动窗口
题目描述
给定一个长度为N(N≤10^6)的数列A,求A中所有长度为k(k<=n)的子序列A[l, r]的最大值。
输入格式
第1行:两个用空格分隔的整数n和k。
第2行:n个用空格分隔的整数,表示数列中的每个数(每个数的范围都在[-10^9, 10^9]
输出格式
1行:从左到右依次输出A数列中所有长度为k的区间的最大值,两个数之间用一个空格分隔。
输入输出样列
输入样例1:
8 3
1 3 -1 -3 5 3 6 7
输出样例1:
3 3 5 5 6 7
思路1:枚举
枚举每一个长度为k的区间[i,i+k-1],然后取最大值。
对于每一个i的取值范围是[1,n-k+1]
时间复杂度O(n^2^)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f;
int a[N], n, k;
int main () {
cin >> n >> k;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
for (int i = 1; i <= n - k + 1; i ++ ) {
int maxs = -INF;
for (int j = i; j < i + k; j ++ )
maxs = max (maxs, a[j]);
cout << maxs << '\n';
}
return 0;
}
思路2:单调队列
优化思路:
单调队列思想:
继续,首个窗口覆盖的区间范围为[1,k],同时有i, j满足:
1.i<=k&&j<=k
2.i<j
简单点说也就是i, j是[1,k]两数,并且i<j
继续像单调栈一样分析A[i]和A[j]的情况:
A[i]<A[j]:则A[i]是一个无用的值,因为显然它不可能作为所在窗口的值。A[i]=A[j]:和A[i]<A[j]一样,虽然A[i]=A[j],但是A[j]的位置更优,因为在这个问题里存在过期,就是当前窗口的第一个值在下一个窗口就会过期、不能使用,所以说明A[i]会比A[j]更先过期。
当处理到一个
A[j]时,就可以排除不可能的答案也就是前面所有<=A[i]的元素
剔除过期元素:
对于队列里的元素可以只记一个下标,不仅可以查到下标,也可以通过下标来查值。
所以如果下标index满足index>=i-k+1&&index<=i说明index在当前窗口内。
反过来:如果下标index满足index<=i-k说明index不在当前窗口内。
思考使用什么数据结构实现
需要满足的要求:
可以实现队头出队的操作(剔除过期元素)踢掉队尾元素(剔除所有<=A[j]元素)入队(将当前元素加入队尾)
并且这些操作必须是O(1)的时间复杂度完成。
发现双端队列deque可以实现:
第一个是deque的pop_front操作
第二个是deque的pop_back操作
最后一个是deque的push_back操作
最后上Dear Code:
#include <bits/stdc++.h>
#define FOR(i, a, b) for(int i = a; i <= b; i ++ )
#define DOR(i, a, b) for(int i = b; i >= a; i -- )
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
const int N = 1e6 + 10;
int n, k, a[N];
deque<int> dq;
int main () {
scanf ("%d%d", &n, &k);
for (int i = 1; i <= n; i ++ ) {
scanf ("%d", &a[i]);
if (i - k + 1 > dq.front ()) dq.pop_front ();
while (dq.size () && a[i] >= a[dq.back ()]) dq.pop_back ();
dq.push_back (i);
if (i >= k) printf ("%d ", a[dq.front ()]);
}
return 0;
}
滑动窗口类问题
题目描述
小科的期中考试成绩单拿到了,小科共有N门课,其中第i门课考了S[i]分(可能为负
数),N门课的成绩按照S[1]到S[N]的顺序记录在成绩单上。成绩单需要家长签字,所
以小科需要将成绩单拿给自己的爸爸大科签字。爸爸大科的心情会受到成绩单上分数
的影响,确切的说大科任意时刻的心情等于他已经看过的分数之和。如果分数之和为
负数大科的爸爸就会发怒,一旦发怒他就会揍小科。小科不想挨揍,所以他想调整一
下给爸爸看分数的顺序。由于分数都是连续的写在成绩单上的,所以小科没法跳跃着
给爸爸看分数,但是他可以选择让爸爸从哪门课开始看起。比如,他可以让爸爸从第5
门课开始看起,这样的话大科会先依次的看完第5门课到第N门课,然后再回头看第1门
课到第4门课。也就是说如果小科选择让爸爸从第i们课开始看,那么大科将会先依次看
完第i到第N门课的成绩,再依次看完第1到第i-1门课的成绩。初试时大科的心情是0,
大科每看完一门课的成绩后如果心情变为负数就会揍小科。小科想知道为了使自己不
挨揍,有多少个i可以作为给爸爸看的第一门课。请你帮帮小科。
输入格式
第1行:一个正整数N,表示小科考试的科目数。
第2行:N个空格分隔的整数,其中第i个整数S[i],表示第i门课的成绩。
输出格式
一行:一个整数,表示有多少个i可以作为起点,使得小科不会挨揍。
输入输出样列
输入样例1:
5
-5 4 -1 3 2
输出样例1:
2
说明
【样例说明】
小科可以选择以第2门课为起始,或者第4门课为起始给大科看。
【数据范围】
1 <= N <= 10^6, -1000 <= S[i] <= 1000
题目类型:环形的单调队列问题
算法思路:枚举1-N作为起点,判断是否能够让小科不挨揍(任意时刻分数的累加和都>=0)
小科爸爸的5次心情,分别是子段s[2, 2], s[2, 3], s[2, 4], s[2, 5], s[2, 6(1)]的子段和。
为了方便处理右端点>N的子段,可以将原数据复制一份,s[N+1] = s[1], s[N+2] = s[2],以此类推。
子段和可以通过前缀和计算获得:
s[2, 2] = sum[2] - sum[1]
s[2, 3] = sum[3] - sum[1]
s[2, 4] = sum[4] - sum[1]
s[2, 5] = sum[5] - sum[1]
s[2, 6] = sum[6] - sum[1]
当以i为起点时,计算子段和计算时都要减去sum[i-1],所以可以先算出sum[i]到sum[i+n-1]
的最小值,然后再减去sum[i-1]判断是否<0。
随着i的增大,区间[i, i+n-1]也在右移,问题转换为求宽度为N的滑动窗口的最小值。
算法过程:
① 将S数组复制一份,从N扩大为2N。
② 求前缀和sum[1]到sum[2N]。
③ 利用单调队列在数组sum中求长度为N的滑动窗口的最小值。
④ 当以i为左端点时,判断sum[i]到sum[i+n-1]的最小值 - sum[i-1]是否<0。
最后上Dear Code:
#include <bits/stdc++.h>
#define FOR(i, a, b) for(int i = a; i <= b; i ++ )
#define DOR(i, a, b) for(int i = b; i >= a; i -- )
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
const int N = 2e6 + 10;
int n, k, a[N], mins[N];
deque<int> dq;
int main () {
scanf ("%d", &n); k = n;
for (int i = 1; i <= n; i ++ ) {
cin >> a[i];
a[i + n] = a[i];
} n <<= 1;
for (int i = 1; i <= n; i ++ ) a[i] += a[i - 1];
for (int i = 1; i < n; i ++ ) {
if (i - k + 1 > dq.front ()) dq.pop_front ();
while (dq.size () && a[i] <= a[dq.back ()]) dq.pop_back ();
dq.push_back (i);
if (i >= k) mins[i - k + 1] = a[dq.front ()];
} int res = 0;
for (int i = 1; i <= k; i ++ ) {
int val = mins[i] - a[i - 1];
if (val >= 0) res ++;
} printf ("%d", res);
return 0;
}
合并果子
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同
的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之
和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时
总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。
假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计
出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力
为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。
所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
将水果堆分成两类:还未合并的原始堆和合并后形成的新堆。 考虑排序好的原始堆:如果不将合并后的新堆加入原始堆,则原始堆始终 是有序的。 同时,对于合并后的新堆,后合并出来的堆中的果子数量一定大于先合并 出来的。 可以使用两个单调递增队列q1和q2分别存储原始堆和合并形成的新堆。
每次比较q1和q2的队首元素,取两者较小的,生成的新堆加入新堆队列 q2。
最后上Dear Code:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
int n, res, a[N];
queue<int> q1, q2;
int main () {
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
sort (a + 1, a + 1 + n);
for (int i = 1; i <= n; i ++ ) q1.push (a[i]);
for (int i = 1; i < n; i ++ ) {
int s = 0;
for (int j = 1; j < 3; j ++ )
if (q1.empty ()) s += q2.front (), q2.pop ();
else if (q2.empty ()) s += q1.front (), q1.pop ();
else {
if (q1.front () < q2.front ()) s += q1.front (), q1.pop ();
else s += q2.front (), q2.pop ();
}
res += s;
q2.push (s);
}
cout << res;
return 0;
}