单调队列

· 浏览次数 : 0

小编点评

The provided code is a C++ program that generates a sequence of numbers based on certain conditions. The code uses dynamic programming to efficiently calculate the sequence. **Key Concepts:** * **Dynamic Programming:** A technique for solving problems by breaking them down into smaller subproblems and storing the results of previous subproblems. * **Arrays:** A data structure that allows for efficient random access to elements in a specific order. * **Optimization:** Finding the optimal solution to a problem by iteratively improving upon subproblems. * **Conditionals:** Controlling the execution of code based on specific conditions. * **Looping:** Performing a set of operations a specific number of times. **Algorithm:** 1. **Input:** Read the input values, including the size of the array `n`, the array elements `a` and `b`, and the time limit `d`. 2. **Initialization:** Initialize a 2D array `dp` with dimensions `2` and `n`. Initialize `dp[1][i]` to `b[1]` for all `i` in the array. 3. **Recursive Steps:** - For `i` from 2 to `n`: - Calculate `dp[now][j]` by finding the maximum of the following two values: - `dp[now][j]` (from the previous row) and `dp[last][num[fst]] + b[i]` (from the previous column). - Update `dp[now][j]` with the maximum value. 4. **Output:** Print the maximum value in the `dp` array, which represents the maximum achievable sum for the given conditions. **Optimization:** * Using dynamic programming, the code efficiently calculates the optimal solution. * Rolling arrays allow for efficient access to elements in the `dp` array. * The code considers two scenarios for calculating `dp[now][j]`, providing an optimal solution. **Additional Notes:** * The code assumes that `n` is a power of 2. This is a optimization to reduce the time complexity. * The constants `M` and `d` are defined within the code and may need to be adjusted based on the problem's specific parameters.

正文

单调队列

考虑在一个序列中维护一个类似于窗口的东西。

以下不妨设求得是窗口最大值。

首先根据贪心,如果当前数整个窗口中最大的,并且是最靠前的,那么这个数前面的所有数都不会对答案产生一点贡献。于是考虑维护一个单调递增的序列,需要从中找出答案。设置一个首指针,未指针代表这个窗口的开始和结束。

然后,考虑一个和莫队类似的操作:

Remove 操作:将比当前值小的数扔出维护序列即可。

Add 操作:直接将当前数加入到队伍的最后段(这里可以仔细想一想,为什么)。

然后按照这样的操作模拟即可。

时间复杂度分析

这是很简单的,因为每一个元素最多只被出队和入队各 \(1\) 次,所以时间复杂度是 \(O(n)\)

模板

模板题目:P1886 滑动窗口

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+1000;
int n,k;
int number[N];
int num[N];

int main(){
	ios::sync_with_stdio(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>number[i];
	int fst=0,ed=-1;
	for(int i=1;i<k;i++){
		while(fst<=ed&&number[num[ed]]>=number[i])ed--;
		num[++ed]=i;
	}
	for(int i=k;i<=n;i++){
		while(fst<=ed&&number[num[ed]]>=number[i])ed--;
		num[++ed]=i;
		while(num[fst]<=i-k)fst++;
		cout<<number[num[fst]]<<' ';
	}
	for(int i=0;i<=ed;i++)num[i]=0;
	fst=0,ed=-1;
	cout<<'\n';
	for(int i=1;i<k;i++){
		while(fst<=ed&&number[num[ed]]<=number[i])ed--;
		num[++ed]=i;
	}
	for(int i=k;i<=n;i++){
		while(fst<=ed&&number[num[ed]]<=number[i])ed--;
		num[++ed]=i;
		while(num[fst]<=i-k)fst++;
		cout<<number[num[fst]]<<' ';
	}
	return 0;
}	

优点分析

单调队列可以在 \(O(n)\) 的时间复杂度之内求出一个给定区间长度的整个序列中的区间最大值,在这一点上,它比 线段树ST表 优化了一个 \(O(\log n)\) 的时间。

例题

P2698

题目形式化:

  • 给出一条线段上的 \(n\) 个点,每个点有一个权值 \(𝑦 _𝑖\),你要找出一段区间,使区间中的 \(𝑦_{𝑚𝑎𝑥}−𝑦_{𝑚𝑖𝑛}\) 大于 \(d\)

  • 输出这个区间的长度

考虑二分区间长度。

开两个单调队列记录最大值和最小值吗,每次用 \(O(n)\) 进行判断即可。总时间复杂度 \(O(n\log n)\)

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
struct node{
	int x;
	int height;
}rain[N];
bool cmp(node a,node b){
	return a.x==b.x?a.height<b.height:a.x<b.x;
}
#define L(i,j,k) for(int i=j;i<=k;i++)
int n,d;
int fst_Max,ed_Max;
int fst_Min,ed_Min;
int num_Max[N];
int num_Min[N];
bool check(int len){
	L(i,0,ed_Max)num_Max[i]=0;
	L(i,0,ed_Min)num_Min[i]=0;
	fst_Max=0,ed_Max=-1;
	fst_Min=0,ed_Min=-1;
	L(i,1,n){
		while(rain[num_Max[fst_Max]].x<rain[i].x-len&&fst_Max<=ed_Max)fst_Max++;
		while(fst_Max<=ed_Max&&rain[num_Max[ed_Max]].height<=rain[i].height)ed_Max--;
		num_Max[++ed_Max]=i;
		
		
		while(rain[num_Min[fst_Min]].x<rain[i].x-len&&fst_Min<=ed_Min)fst_Min++;
		while(fst_Min<=ed_Min&&rain[num_Min[ed_Min]].height>=rain[i].height)ed_Min--;
		num_Min[++ed_Min]=i;
		
//		cout<<fst_Max<<' '<<fst_Min<<'\n';
//		cout<<"Max:"<<rain[num_Max[fst_Max]].height<<'\n';
//		cout<<"Min:"<<rain[num_Min[fst_Min]].height<<'\n';
//		cout<<rain[num_Max[fst_Max]].height-rain[num_Min[fst_Min]].height<<' '<<d<<'\n';
		if(rain[num_Max[fst_Max]].height-rain[num_Min[fst_Min]].height>=d)return true;
	}
	return false;
}
int l,r;
int mid,ans=-1;
int main(){
	scanf("%d %d",&n,&d);
	L(i,1,n)scanf("%d %d",&rain[i].x,&rain[i].height);
	sort(rain+1,rain+n+1,cmp);
//	L(i,1,n)cout<<rain[i].x<<' '<<rain[i].height<<'\n';
	l=0,r=1e6+10;
//	check(2);
	while(l<=r){
		mid=(l+r)>>1;
		if(check(mid)){
			r=mid-1;
			ans=mid;
		}else l=mid+1;
	}
	printf("%d\n",ans);
	return 0;
}

P2216

考虑使用多次单调队列。

可以先将整个矩阵按照行的顺序找出每个 \(1\times n\) 中的最大值,然后再在刚刚得出的这个矩阵中按照列的顺序找出每个 \(n\times 1\) 的矩阵的最大值。

这时候,你就得到了一个 \((a-n+1)\times (b-n+1)\) 的新矩阵,直接找出 \(\displaystyle \min_{1\le i\le a-n+1,1\le j\le b-n+1}(Max_{i,j}-Min_{i,j})\) 就是答案,证明是显然的。

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int a,b,n;
#define L(i,j,k) for(int i=j;i<=k;i++)
int number[N][N];
vector<int> Max[N],Min[N];
vector<int> final_Max[N],final_Min[N];
int num[N];
int h_Max[N],l_Max[N];
int h_Min[N],l_Min[N];
int fst,ed;
int main(){
	scanf("%d %d %d",&a,&b,&n);
	memset(h_Min,0x3f3f3f3f,sizeof(h_Min));
	memset(l_Min,0x3f3f3f3f,sizeof(l_Min));
	L(i,1,a)L(j,1,b)scanf("%d",&number[i][j]);
	
	L(i,1,a){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&number[i][num[ed]]<=number[i][j])ed--;
			num[++ed]=j;
		}
		L(j,n,b){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&number[i][num[ed]]<=number[i][j])ed--;
			num[++ed]=j;
//			cout<<i<<' '<<j<<' '<<num[fst]<<'\n';
			Max[i].push_back(number[i][num[fst]]);
		}
	}
	
//	cout<<"finish!\n";
//	cout<<"Max:\n";
//	L(i,1,a){
//		int len=Max[i].size()-1;
//		L(j,0,len)cout<<Max[i][j]<<' ';
//		cout<<'\n';
//	}

	L(i,1,a){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&number[i][num[ed]]>=number[i][j])ed--;
			num[++ed]=j;
		}
		L(j,n,b){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&number[i][num[ed]]>=number[i][j])ed--;
			num[++ed]=j;
			Min[i].push_back(number[i][num[fst]]);
		}
	}
	
//	cout<<"Min:\n";
//	L(i,1,a){
//		int len=Max[i].size()-1;
//		L(j,0,len)cout<<Min[i][j]<<' ';
//		cout<<'\n';
//	}

	L(i,0,b-n){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&Max[num[ed]][i]<=Max[j][i])ed--;
			num[++ed]=j;
		}
		L(j,n,a){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&Max[num[ed]][i]<=Max[j][i])ed--;
			num[++ed]=j;
			final_Max[i].push_back(Max[num[fst]][i]);
		}
	}
	
	L(i,0,b-n){
		L(j,0,ed)num[j]=0;
		fst=0,ed=-1;
		L(j,1,n-1){
			while(fst<=ed&&Min[num[ed]][i]>=Min[j][i])ed--;
			num[++ed]=j;
		}
		L(j,n,a){
			while(num[fst]<=j-n)fst++;
			while(fst<=ed&&Min[num[ed]][i]>=Min[j][i])ed--;
			num[++ed]=j;
			final_Min[i].push_back(Min[num[fst]][i]);
		}
	}
	
//	cout<<"final_Max:\n";
//	L(i,0,b-n){
//		int len=final_Max[i].size()-1;
//		L(j,0,len)cout<<final_Max[i][j]<<' ';
//		cout<<'\n';
//	}
//	
//	cout<<"final_Min:\n";
//	L(i,0,b-n){
//		int len=final_Min[i].size()-1;
//		L(j,0,len)cout<<final_Min[i][j]<<' ';
//		cout<<'\n';
//	}
	
	
	int ans=2e9;
	L(i,0,b-n){
		L(j,0,a-n){
			ans=min(ans,final_Max[i][j]-final_Min[i][j]);
		}
	}
	printf("%d",ans);
	return 0;
}

单调队列优化 DP

首先看看为什么单调队列可以优化 DP

DP 中,如果需要一个固定区间长度 \(k_0\) 的最小值:

\(dp_i=\displaystyle \min_{i-k_0-1\le k\le i-1}dp_k+value_i\)

那么这时候,就可以使用单调队列优化 DP,只是每次加入元素值的时候,需要先算出当前的 DP 值,然后在加入单调队列中即可。

时间复杂度分析

在以上的例子中,将原来 \(dp\)\(O(nk_0)/O(n\log k_0)\)(暴力/线段树) 的时间复杂度优化为了 \(O(n)\)。所以,单调队列的时间优化是十分优秀的。

例题

P2034

首先,将问题转化为反面,即为最小的删数大小。

设立 \(dp_i\) 表示删除第 \(i\) 个数所能得到的最小贡献值。

考虑 DP

When i≤k+1\(dp_i=\displaystyle number_i\)

When i≥k+2\(dp_i=\displaystyle \min_{i-k-1\le j\le i-1} dp_j+number_i\)

发现当 \(i\ge k+2\) 时,正好符合对于单调队列优化 DP 的要求。

于是直接写即可。

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
#define L(i,j,k) for(int i=j;i<=k;i++)
#define ll long long
ll dp[N],tot;
int n,k;
int number[N];
int fst,ed=-1;
int num[N];
int main(){
	scanf("%d %d",&n,&k);k++;
	L(i,1,n)scanf("%d",&number[i]),tot+=number[i];
	L(i,1,k){
		dp[i]=number[i];
		while(fst<=ed&&dp[num[ed]]>=dp[i])ed--;
		num[++ed]=i;
	}
	L(i,k+1,n){
		dp[i]=dp[num[fst]]+number[i];
		while(fst<=ed&&dp[num[ed]]>=dp[i])ed--;
		num[++ed]=i;
		while(num[fst]<=i-k)fst++;
	}
	ll Min=1e18;
	L(i,n-k+1,n)Min=min(dp[i],Min);
	printf("%lld\n",tot-Min);
	return 0;
}

CF372C

注意到 \(m\le 300\),于是考虑一个 \(O(n^2m)\)DP

设定 \(dp_{i,j}\) 表示当第 \(i\) 个烟花发射时,在位置 \(j\) 所能得到的最大收益。

然后可以得出如下转移方程:

定义 \(T=t_i-t_{i-1}\)

\(dp_{1,j}=b_1-|a_1-j|\)

\(dp_{i,j}=\displaystyle \max_{\max(1,j-d\times T)\le k\le \min(n,j+d\times T)}dp_{i-1,k}+b_i-|a_i-j|\)

然后突然发现 \(\displaystyle \max_{\max(1,j-d\times T)\le k\le \min(n,j+d\times T)}dp_{i-1,k}\) 可以单调队列,于是直接写。

可以将 \(\displaystyle \max_{\max(1,j-d\times T)\le k\le \min(n,j+d\times T)}dp_{i-1,k}\) 分成比 \(j\) 小的和比 \(j\) 大的两部分来计算。

然后突然发现 \(O(nm)\) 的空间根本过不了,于是滚动数组优化即可。

AC code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define L(i,j,k) for(register int i=j;i<=k;i++)
#define R(i,j,k) for(register int i=j;i>=k;i--)
const int N=1e5+5e4+100;
const int M=310;
ll dp[2][N];
ll a[M],b[M],t[M];
int n,m,d;
int num[N];
int abs(int x){if(x<0)return -x;return x;}
int main(){
	scanf("%d %d %d",&n,&m,&d);
	memset(dp,0xFE,sizeof(dp));
	L(i,1,m)scanf("%lld %lld %lld",&a[i],&b[i],&t[i]);
	L(i,1,n)dp[1][i]=b[1]-abs(a[1]-i);
	L(i,2,m){
		int now=i&1,last=now^1;
		ll T=t[i]-t[i-1];
		int fst=0,ed=-1;
		memset(dp[now],0xFE,sizeof(dp[now]));
		L(j,1,n){
			while(fst<=ed&&num[fst]<j-T*d)fst++;
			while(fst<=ed&&dp[last][num[ed]]<=dp[last][j])ed--;
			num[++ed]=j;
			dp[now][j]=max(dp[now][j],dp[last][num[fst]]+b[i]-abs(a[i]-j));
		}
		fst=0,ed=-1;
		R(j,n,1){
			while(fst<=ed&&num[fst]>j+T*d)fst++;
			while(fst<=ed&&dp[last][num[ed]]<=dp[last][j])ed--;
			num[++ed]=j;
			dp[now][j]=max(dp[now][j],dp[last][num[fst]]+b[i]-abs(a[i]-j));
		}
	}
	ll ans=-1e18;
	L(i,1,n)ans=max(ans,dp[m&1][i]);
	printf("%lld\n",ans);
	return 0;
}

与单调队列相似的内容:

单调队列

单调队列 考虑在一个序列中维护一个类似于窗口的东西。 以下不妨设求得是窗口最大值。 首先根据贪心,如果当前数整个窗口中最大的,并且是最靠前的,那么这个数前面的所有数都不会对答案产生一点贡献。于是考虑维护一个单调递增的序列,需要从中找出答案。设置一个首指针,未指针代表这个窗口的开始和结束。 然后,考虑

使用单调队列解决 “滑动窗口最大值” 问题

本文已收录到 GitHub · AndroidFamily,有 Android 进阶知识体系,欢迎 Star。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。 前言 大家好,我是小彭。 在上一篇文章中,我们介绍了单调栈这种特殊的栈结构,单调栈是一种非常适合处理 “下一个更大元素问题” 的数据结

数组模拟单向队列的思路及代码

JAVA实现数组模拟单向队列的思路及代码 一、什么是队列? 队列是一种特殊的线性表 ,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。 进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为

[转帖]Linux中常见IO调度器

https://www.jianshu.com/p/3c16e39a005a 单队列调度算法多队列调度算法 deadline mqdeadlines cfq bfq noop none kyber 对于磁盘I/O,Linux提供了cfq, deadline和noop三种调度策略 cfq: 这个名字是

数组模拟环形队列的思路及代码

JAVA实现数组模拟环形队列的思路及代码 前言 在对Java实现数组模拟队列零了解的情况下,建议先去阅读《JAVA实现数组模拟单向队列的思路及代码》一文,可以辅助理解本文核心思想。 一、环形数组队列 实现:让数组达到复用的效果,即:当我们从数组队列中取出了数据,那取出数据后后这个空间可以再次使用。

文心一言 VS 讯飞星火 VS chatgpt (112)-- 算法导论10.2 3题

三、用go语言,用一个单链表L实现一个队列。要求操作 ENQUEUE 和 DEQUEUE 的运行时间仍为O(1)。 文心一言: 在 Go 语言中,使用单链表实现队列的操作,包括入队(ENQUEUE)和出队(DEQUEUE),并保持操作的时间复杂度为 O(1),需要利用两个指针,一个指向队头,另一个指

Linux多线程

Linux多线程,线程的基本概念,线程库的基本原理,线程私有栈的由来,互斥与同步,互斥锁,信号量,条件变量,线程池,生产者消费者模式,基于阻塞队列/阻塞环形队列的生产者消费者模模型,单例模式,饿汉懒汉方式

微服务项目搭建之技术选型

1、什么是微服务 Java微服务是一种架构风格,通过将单个Spring Boot应用程序拆分为一组小型、独立的Spring Boot服务来构建分布式系统。每个微服务都运行在自己的进程中,并使用轻量级通信机制(如HTTP或消息队列)来进行相互之间的通信。微服务的设计目标是提高系统的灵活性、可伸缩性和可

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解

【RocketMQ】消息的拉取总结

在上一讲中,介绍了消息的存储,生产者向Broker发送消息之后,数据会写入到CommitLog中,这一讲,就来看一下消费者是如何从Broker拉取消息的。 RocketMQ消息的消费以组为单位,有两种消费模式: 广播模式:同一个消息队列可以分配给组内的每个消费者,每条消息可以被组内的消费者进行消费。