浅谈单调队列优化
引子
单调队列其实是一种具有单调性的双端队列,通常最佳答案位于队首。可以用\(O(n)\)的时间复杂度可以求出整个队列的区间最值,调用时为\(O(1)\)。
可以用于求区间最值,和降低DP的时间复杂度。
具体实现
通过一个数组和两个指针来维护,一个维护队首head
,一个维护队尾tail
初值
head = 1; //从1开始
tial = 0; //为前一位的位置
维护答案
while(i - q[head].side + 1 > m && head <= tial)head ++; //因为dp[i]已经更新,head维护的是dp[i + 1]的值所以要加1
维护队尾
将答案放进对应的位置,维护队列的单调性
while(a[i] < q[tial].num && tial >= head)tial --;
q[++ tial].num = a[i];
q[tial].side = i;
关键代码
head = 1;
tial = 0;
for(int i = 1; i <= n; i ++){
scanf("%d", &a[i]);
printf("%d\n", q[head].num);
while(i - q[head].side + 1 > m && head <= tial)head ++;
while(a[i] < q[tial].num && tial >= head)tial --;
q[++ tial].num = a[i];
q[tial].side = i;
}
例题
求m区间内的最小值
题目描述
一个含有n项的数列(\(n <= 2000000\)),求出每一项前m个数的最小值。若前面的数不足m项,则从第1个数开始,若前面没有数则输出0。
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 2000005;
struct node{
int num, side;
};
int n, m;
int a[maxn];
node q[maxn];
int head, tial;
int main() {
scanf("%d %d", &n, &m);
head = 1;
tial = 0;
for(int i = 1; i <= n; i ++){
scanf("%d", &a[i]);
printf("%d\n", q[head].num);
while(i - q[head].side + 1 > m && head <= tial)head ++;
while(a[i] < q[tial].num && tial >= head)tial --;
q[++ tial].num = a[i];
q[tial].side = i;
}
return 0;
}
修建草坪
题目描述
原题来自:USACO 2011 Open Gold
在一年前赢得了小镇的最佳草坪比赛后,FJ 变得很懒,再也没有修剪过草坪。现在,新一轮的最佳草坪比赛又开始了,FJ 希望能够再次夺冠。
然而,FJ 的草坪非常脏乱,因此,FJ 只能够让他的奶牛来完成这项工作。FJ 有N
只排成一排的奶牛,编号为1
到N
。每只奶牛的效率是不同的,奶牛i
的效率为E[i]
。
靠近的奶牛们很熟悉,如果 FJ 安排超过K
只连续的奶牛,那么这些奶牛就会罢工去开派对。因此,现在 FJ 需要你的帮助,计算 FJ 可以得到的最大效率,并且该方案中没有连续的超过K
只奶牛。
思路
很明显,这是一道DP。dp[i][1]
表示选第i
头奶牛的最高效率,dp[i][0]
为不选第i
头的最高效率。
\(dp[i][0] = max(dp[i - 1][1], dp[i - 1][0])\)
$dp[i][1] = min(dp[i][1], dp[j][0] + sum[i] - sum[j - 1])) $j在i - 1
到i - k + 1
对于dp[j][0]
进行一个单调队列维护就好了
代码
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
const int maxn = 1e5 + 5;
using namespace std;
struct node{
long long num;
int side;
};
int n, m;
long long a[maxn], dp[maxn][2], sum[maxn];
node q[maxn];
int main() {
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i ++){
scanf("%d", &a[i]);
sum[i] = sum[i - 1] + a[i];
}
int head = 1, tial = 1; //tail为当前位置
for(int i = 1; i <= n; i ++){
dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);
while(i - q[head].side > m && head <= tial)head ++;//维护当前位置
dp[i][1] = q[head].num + sum[i];
while(q[tial].num < (dp[i][0] - sum[i]) && head <= tial)tial --;
q[++ tial].num = dp[i][0] - sum[i];
q[tial].side = i;
// printf("%d %d\n", dp[i][1], dp[i][0]);
}
printf("%lld", max(dp[n][1], dp[n][0]));
return 0;
}
旅行问题
题目描述
原题来自:POI 2004
John 打算驾驶一辆汽车周游一个环形公路。公路上总共有 车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。
任务:判断以每个车站为起点能否按条件成功周游一周。
思路
这道题代码的细节令人ex
对于每一个点,sum
为行走到这个点还剩多少油,因为我们只需要知道能否走完,所以我们只需要知道能否到达sum
最小的点就可以了
思路没什么难道,细节看代码注释吧
代码
#include <cstdio>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
const int maxn = 1e6 + 5;
struct node{
int p, d, s;
int sum;
};
struct data{
int num;
int side;
};
int n;
node a[maxn * 2];
bool flag[maxn * 2];
int min_l;
data q[maxn];
data q2[maxn];
int Min[2 * maxn];
int Min2[2 * maxn];
bool ans[maxn];
signed main() {
memset(ans, false, sizeof(ans));
scanf("%d", &n);
for(int i = 1; i <= n; i ++){
scanf("%d %d", &a[i].p, &a[i].d);
a[i].s = a[i].p - a[i].d; //因为 a[i].d为到下一个点的路径,所以处理逆时针的时候减去的是i
a[i + n].s = a[i].s; //一个环不好维护,就通过倍长这个路径来处理
}
a[0].sum = 0;
for(int i = 1; i <= 2 * n; i ++){
a[i].sum = a[i - 1].sum + a[i].s; //逆时针走到这一站还剩多少油
}
int head2 = 0, tail2 = -1; //和head = 1, tail = 0是一样的
for(int i = 2 * n; i >= 1; i --){
while(head2 <= tail2 && q2[head2].side >= i + n)head2 ++;
while(head2 <= tail2 && q2[tail2].num >= a[i].sum)tail2 --;
q2[++ tail2].side = i;
q2[tail2].num = a[i].sum;
if(i <= n && q2[head2].num >= a[i - 1].sum){
ans[i] = 1;
}
}
a[0].sum = 0;
a[0].d = a[n].d;
for(int i = 1; i <= n; i ++){
a[i].s = a[i].p - a[i - 1].d; //a[i].s为到这个点时还剩多少油,这里为顺时针,减去的自然是上一个点到这个点的路径
a[i + n].s = a[i].s;
}
for(int i = 1; i <= 2 * n; i ++){ //顺时针
a[i].sum = a[i - 1].sum + a[i].s;
}
int head = 0, tail = -1;
for(int i = 1; i <= 2 * n; i ++){
while(head <= tail && i - q[head].side >= n)head ++;
if(i > n && a[i].sum - q[head].num >= 0)
ans[i - n] = 1;
while(head <= tail && q[tail].num <= a[i].sum)tail --;
q[++ tail].side = i;
q[tail].num = a[i].sum;
}
for(int i = 1; i <= n; i ++){
if(ans[i]){
printf("TAK\n");
}
else{
printf("NIE\n");
}
}
return 0;
}
辨析 head 和 tail 的初值及获取答案的位置
tail = head - 1
tail 和 head 在 维护之后保存的都是对下一个点的影响,所以赋值应在维护前
tail = head
tail 和 head 保存的是对当前点的影响,所以赋值在维护后