浅析单调队列优化

什么是单调队列

模板题

先来个模板题压压惊(bushi)。

滑动窗口 /【模板】单调队列 - 洛谷

单调队列解释

单调队列分为“单调”和“队列”

  1. 单调:元素有序(递增or递减)
  2. 队列:可以在队头和队尾对队列进行维护(听起来很像双端队列)

思路

以上面模板题中的最大值为例,分析一下单调队列的维护思路:

本题中队列维护的是某区间的最大值。

区间意味着有头有尾,尾即目前遍历到的下标i,头可以用长度来计算:max(1,i-len)。跟不上区间移动的,即下标在目前有效区间头之前的,踢出队列。

最大值的处理则比较有趣了:出现在a[i]之前比a[i]小的数一定不可能是区间max了(因为它与a[i]共处同一区间且a[i]更大),踢出去;但出现在a[i]之后比a[i]小的数有可能是区间max(a[i]是更先出队的),得先放在队列里待后续评估。

为实现元素的唯一标识与区间长度的记录,队列中维护的是数组中元素的序号(下标+1)

实现

实现思路

不满足区间范围的,是从队头pop出去;

出现在a[i]之前比a[i]小的,从队尾pop出去;

根据前面的分析,无论如何a[i]都是要入队看看的。

细节:注意队列的初始化and队列的非空判定

实现手段

STL——deque

贴模板题代码

#include <iostream>
#include <deque>
using namespace std;

#define maxn 1000005
deque<int> sq, bq;
int n, k;
int a[maxn];

int main() {
	cin >> n >> k;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	sq.push_back(0);
	for (int i = 1; i <= n; i++) {
	    while(!sq.empty() && sq.front() <= i - k){
	        sq.pop_front();
	    }
		while (!sq.empty() && a[i] <= a[sq.back()]) {
			//cout << "pop" << sq.back() << endl;
			sq.pop_back();
		}
		sq.push_back(i);
		//cout << "push" << i << endl;
		if (i >= k) {
			cout << a[sq.front()] << " ";
		}
	}
	cout << endl;
    
    bq.push_back(0);
	for (int i = 1; i <= n; i++) {
		while (!bq.empty() && a[i] >= a[bq.back()]) {
			//cout << "pop" << bq.back() << endl;
			bq.pop_back();
		}
		bq.push_back(i);
		//cout << "push" << i << endl;
		if (i >= k) {
			if (bq.front() <= i - k) {
				//cout << "pop" << bq.front() << endl;
				bq.pop_front();
			}
			cout << a[bq.front()] << " ";
		}
	}
	cout << endl;
	return 0;
}
手搓数组
#include <iostream>
using namespace std;

#define maxn 1000005

int a[maxn];
int q[maxn];
int q2[maxn];

int main() {
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	int head = 0, tail = -1;
	q[0] = 0;
	for (int i = 1; i <= n; i++) {
		while (head <= tail && q[head] <= i - m)
			head++;//已经不在范围内要出队
		while (head <= tail && a[i] <= a[q[tail]])
			tail--;//维护单调递增的队列
		q[++tail] = i;
		if (i >= m)
			cout << a[q[head]] << " ";
	}
	cout << "\n";

	head = 0, tail = -1;
	q2[0] = 0;
	for (int i = 1; i <= n; i++) {
		while (head <= tail && q2[head] <= i - m)
			head++;//已经不在范围内要出队
		while (head <= tail && a[i] >= a[q2[tail]])
			tail--;//维护单调递增的队列
		q2[++tail] = i;
		if (i >= m)
			cout << a[q2[head]] << " ";
	}
	cout << "\n";
	return 0;
}

单调队列可以优化啥东西

求极值但可以转化成区间最大/最小元素

题目1(同维度)

切蛋糕 - 洛谷

思路(基础+优化)

抽象一下这个题,其实是在求:不定长度且长度有大小限制(1<=len<=maxL)的最大子段和


插播一段关于“前缀和”:可以简单理解成数列前n项的和,不妨记为sum数组。那么子序列闭区间[i,j]的和则为 sum[j]-sum[i-1] 

可参见题目:最大子段和 - 洛谷

 插播结束。


即使子段和我们采用前缀和来计算,仍需一个二重循环里面遍历j的取值。

进一步地我们注意到,对于每个i来说sum[i]都是固定的,于是我们可以做如下图的数学推导

至此,我们已把问题转化为找到sum数组指定区间中的最小值,单调队列可以闪亮登场了。

代码及注意事项说明

本题是不允许子段的长度为0的,应注意出队的边界。

#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;

#define maxn 500005
int a[maxn];
int sum[maxn];
int ans = -1 << 30;
deque<int>sq;

int main() {
	int n, m;
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
	}
	for (int i = 1 ; i <= n; i++) {
		sum[i] = sum[i - 1] + a[i];
	}
	sq.push_back(0);
	for (int i = 1; i <= n; i++) {
		while (!sq.empty() && sq.front() < i - m) {
			sq.pop_front();
		}

		ans = max(ans, sum[i] - sum[sq.front()]);

		while (!sq.empty() && sum[i] <= sum[sq.back()]) {
			//cout << "pop" << sq.back() << endl;
			sq.pop_back();
		}
		sq.push_back(i);
	}
	printf("%d\n", ans);
	return 0;
}

题目2(跨维度)

Watching Fireworks is Fun - 洛谷

思路(基础+优化)

f_{i,j}表示在放第i个烟花时,所处位置为j所能获得的最大值。

根据题意可以列出第一行的式子,但注意到b_i是常数可以提出去,于是可以得到第二行的式子。

对于固定的i和j来说,a_i和j都是常量,可以提出去,于是可以得到第三行的表达式。

根据第三行的式子不难看出,我们需要维护一个关于f_{i-1,k}的优先队列,来辅助更快地计算出f_{i,j}

代码及注意事项说明

由于计算第i维时,只需要用到第i-1维的数据,所以我们的dp只需要两行就够了,轮流当第i-1维和第i维。

又由于行数分别为0和1,可以通过异或的操作来互换。

#include <iostream>
#include <algorithm>
#include <cmath>
#include <deque>
using namespace std;

#define maxn 150005
#define maxm 305
typedef long long ll;
ll a[maxn], b[maxn], t[maxn];
//ll dp[maxm][maxn];
ll dp[2][maxn];int fl=1;
int q[maxn];
ll ans = -1 << 30;


int main() {
	int n, m, d;
	cin >> n >> m >> d;
	for (int i = 1; i <= m; i++) {
		cin >> a[i] >> b[i] >> t[i];
	}

	for (int i = 1; i <= m; i++) {
		int l=1,r=0,k=1;
		for (int j = 1; j <= n; j++) {
			for (; k <= min(n*1ll, j + (t[i] - t[i - 1])*d); k++) {
				while(l<=r && dp[fl^1][q[r]]<=dp[fl^1][k]) r--;
				q[++r]=k;	
			}	
			while(l<=r&&q[l]<max(1ll,j-(t[i]-t[i-1])*d)) l++;
			dp[fl][j]=dp[fl^1][q[l]]-abs(a[i]-j)+b[i];
				//dp[i][j] = max(dp[i - 1][k] - abs(a[i] - j) + b[i], dp[i][j]);
		}
		fl^=1;
	}
	for(int j = 1;j<=n;j++) ans=max(ans,dp[fl^1][j]);
	cout << ans << "\n";
	return 0;
}

参考资料:

OI-wiki

  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值