浅谈单调队列优化

引子

单调队列其实是一种具有单调性的双端队列,通常最佳答案位于队首。可以用\(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只排成一排的奶牛,编号为1N。每只奶牛的效率是不同的,奶牛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 - 1i - 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 保存的是对当前点的影响,所以赋值在维护后

posted @ 2021-02-12 15:35  cqbzzyq  阅读(65)  评论(0编辑  收藏  举报