队列(带例题以及详细实现代码)

什么是队列

队列的存储方式是“先进先出”,只能向队尾插入数据,从队头移出数据。队列的原型在生活中也很常见,比如排队打饭,先来的先打。
队列的实现有两种方式:链队列和循环队列,像下面这个图一样

在这里插入图片描述
链队列可以看做是单链表的一种特殊情况,用指针连接各个节点
在这里插入图片描述
循环队列是一种顺序表,使用一组连续的存储单元依次存放队列元素,用两个指针front和rear分别指向队首和队尾元素,当head和rear走到底时,下一步回到开始的位置,从而在这组连续空间内循环,循环队列能够解决溢出问题,如果不循环的话两个指针都是一直往前的,可能会导致溢出。
在这里插入图片描述
队列的缺点是查找较慢,要从头开始一个一个查找,在某些情况下可以使用优先队列,让优先级高的先出队
队列的简要实现代码很容易,最简单的手写队列如下
在这里插入图片描述
请务必注意这个队列不是循环队列,rear可能大于N从而可能会有溢出问题。在竞赛中一般采用STL中的queue或者手写静态数组实现队列。

STL queue

STL queue的主要操作如下

  • queue<int> que: 定义一个名为que的队列,数据类型为int,可以更具实际需求换成其它的类型
  • que.push(i):将i这个元素入队
  • que.front():返回队首元素
  • que.pop(): 删除队首元素
  • que.back():返回队尾元素
  • que.size():返回队列中元素的个数
  • que.empty(): 判断队列是否为空
    在这里插入图片描述
#include <bits/stdc++.h>

using namespace std;

using ll = long long;

void solve() {
	queue<int> que; // 定义一个名为que的队列
	que.push(1); // 1入队
	cout << que.front() << "\n"; // 1
	que.pop(); // 删除队首元素 此时队列为空
	for (int i = 1; i <= 10; ++i) {
		que.push(i);
	}
	cout << que.back() << "\n"; // 队尾为10
	cout << que.size() << "\n"; // 共有十个元素
	// 遍历队列
	while (!que.empty()) {
		cout << que.front() << " ";
		que.pop();
	}
	cout << "\n";

	
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int tt = 1; // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}

下面这个题是对queue的一个应用,后面还有手动实现队列的代码

机器翻译(洛谷P1540)

题目传送门: 机器翻译

[NOIP2010 提高组] 机器翻译

题目背景

NOIP2010 提高组 T1

题目描述

小晨的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章。

这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义来替换。对于每个英文单词,软件会先在内存中查找这个单词的中文含义,如果内存中有,软件就会用它进行翻译;如果内存中没有,软件就会在外存中的词典内查找,查出单词的中文含义然后翻译,并将这个单词和译义放入内存,以备后续的查找和翻译。

假设内存中有 M M M 个单元,每单元能存放一个单词和译义。每当软件将一个新单词存入内存前,如果当前内存中已存入的单词数不超过 M − 1 M-1 M1,软件会将新单词存入一个未使用的内存单元;若内存中已存入 M M M 个单词,软件会清空最早进入内存的那个单词,腾出单元来,存放新单词。

假设一篇英语文章的长度为 N N N 个单词。给定这篇待译文章,翻译软件需要去外存查找多少次词典?假设在翻译开始前,内存中没有任何单词。

输入格式

2 2 2 行。每行中两个数之间用一个空格隔开。

第一行为两个正整数 M , N M,N M,N,代表内存容量和文章的长度。

第二行为 N N N 个非负整数,按照文章的顺序,每个数(大小不超过 1000 1000 1000)代表一个英文单词。文章中两个单词是同一个单词,当且仅当它们对应的非负整数相同。

输出格式

一个整数,为软件需要查词典的次数。

样例 #1

样例输入 #1

3 7
1 2 1 5 4 4 1

样例输出 #1

5

提示

样例解释

整个查字典过程如下:每行表示一个单词的翻译,冒号前为本次翻译后的内存状况:

  1. 1:查找单词 1 并调入内存。
  2. 1 2:查找单词 2 并调入内存。
  3. 1 2:在内存中找到单词 1。
  4. 1 2 5:查找单词 5 并调入内存。
  5. 2 5 4:查找单词 4 并调入内存替代单词 1。
  6. 2 5 4:在内存中找到单词 4。
  7. 5 4 1:查找单词 1 并调入内存替代单词 2。

共计查了 5 5 5 次词典。

数据范围

  • 对于 10 % 10\% 10% 的数据有 M = 1 M=1 M=1 N ≤ 5 N \leq 5 N5
  • 对于 100 % 100\% 100% 的数据有 1 ≤ M ≤ 100 1 \leq M \leq 100 1M100 1 ≤ N ≤ 1000 1 \leq N \leq 1000 1N1000

这个题题意不难,拿队列模拟即可,代码如下

#include <bits/stdc++.h>

using namespace std;

using ll = long long;

int vis[1500];
queue<int> q;

void solve() {
	memset(vis, 0, sizeof(vis));
	int m, n; cin >> m >> n;
	int cnt = 0;
	for (int i = 1; i <= n; ++i) {
		int x; cin >> x;
		if (!vis[x]) {
			++cnt;
			q.push(x);
			vis[x] = 1;
			while (q.size() > m) {
				vis[q.front()] = 0;
				q.pop();
			}
		}
	}
	cout << cnt << "\n";

	
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int tt = 1; // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}

手写循环队列

我们来手写一下循环队列然后用我们手写的循环队列解决该题,竞赛里一般使用静态的,动态的用起来不方便

#include <bits/stdc++.h>

using namespace std;

using ll = long long;

const int N = 1500;
int vis[N];

struct myqueue {
	int data[N];
	// 如果是动态分配 int *data;
	int head, rear; // 队头 队尾

	bool init() {
		// 动态分配这样写
		// q.data = (int *)malloc(N * sizeof(int));
		// if (!q.data) return false;
		head = rear = 0;
		return true;
	}

	int size() {
		return (rear - head + N) % N;  //队列长度
	}

	bool empty() {
		if (size() == 0) return true;
		else return false;
	}

	bool push(int x) {
		if ((rear + 1) % N == head) return false; // 队列满了
		data[rear] = x;
		rear = (rear + 1) % N;
		return true;
	}

	bool pop(int &x) {
		if (head == rear) return false;  // 队列为空
		x = data[head];
		head = (head + 1) % N;
		return true;
	}

	int front() {
		return data[head];
	}

}q;

void solve() {
	int m, n; cin >> m >> n;
	memset(vis, 0, sizeof(vis));
	q.init();
	int cnt = 0;
	for (int i = 1; i <= n; ++i) {
		int x; cin >> x;
		if (!vis[x]) {
			++cnt;
			q.push(x);
			vis[x] = 1;
			while (q.size() > m) {
				int tmp;
				q.pop(tmp);
				vis[tmp] = 0;
			}
		}
	}
	cout << cnt << "\n";


	
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int tt = 1; // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}

双端队列和单调队列

双端队列和单调队列的概念

双端队列是两头都可进可出的队列,同时具有队列和栈 (先进后出)性质,它能在两端进行插入和删除,而且也只能在两端插入和删除
简单手写的双端队列如下 但是请务必追不能让队头和队尾溢出

const int N = 1e5 + 5;
int que[N], head, tail; // 队头队尾指针 队列大小为 tail - head + 1
head++; // 出队
que[--head] = data; // 数据入队头
que[head]; // 读取队头数据
tail--; // 弹走队尾
que[tail++] = data; // 从队尾入队

也可以使用STL中的deque,用法如下

  • dq[i]:返回队列中下标为i的元素
  • dq.front():返回队头
  • dq.back():返回队尾
  • dq.pop_back():删除队尾 不返回值
  • dq.pop_front():删除队头 不返回值
  • dq.push_back(x):队尾入队
  • dq.push_front(x):队首入队
#include <bits/stdc++.h>

using namespace std;

using ll = long long;

void solve() {
	deque<int> dq;
	for (int i = 1; i <= 10; ++i) {
		if (i & 1) dq.push_back(i);
		else dq.push_front(i);
	}
	// 10 8 6 4 2 1 3 5 7 9
	for (auto it : dq) cout << it << " ";
	cout << "\n";
	cout << dq[0] << " " << dq.front() << "\n"; // 10 10
	cout << dq[9] << " " << dq.back() << "\n"; // 9 9
	dq.pop_back();
	// 10 8 6 4 2 1 3 5 7
	dq.pop_front();
	// 8 6 4 2 1 3 5 7
	for (auto it : dq) cout << it << " ";
	cout << "\n";
	dq.push_front(15);
	dq.push_back(20);
	// 15 8 6 4 2 1 3 5 7 20
	for (auto it : dq) cout << it << " ";
	cout << "\n";
	
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int tt = 1; // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}

双端队列的经典应用是单调队列。单调队列中的元素是单调有序的,且元素在队列中的顺序和原来在队列中的顺序一致,单调队列的队头和队尾都能入队和出队。用单调队列处理时,每个元素只需要进出队一次,复杂度为 O ( n ) O(n) O(n)

单调队列与滑动窗口

来介绍一下单调队列的基本应用,了解如何通过单调队列获得优化,注意队列中的删头,去尾,窗口操作。

滑动窗口/单调队列(洛谷P1886)

题目传送门: 滑动窗口 /【模板】单调队列

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

题目描述

有一个长为 n n n 的序列 a a a,以及一个大小为 k k k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

例如,对于序列 [ 1 , 3 , − 1 , − 3 , 5 , 3 , 6 , 7 ] [1,3,-1,-3,5,3,6,7] [1,3,1,3,5,3,6,7] 以及 k = 3 k = 3 k=3,有如下过程:

窗口位置 最小值 最大值 [1   3  -1] -3   5   3   6   7  − 1 3  1  [3  -1  -3]  5   3   6   7  − 3 3  1   3 [-1  -3   5]  3   6   7  − 3 5  1   3  -1 [-3   5   3]  6   7  − 3 5  1   3  -1  -3  [5   3   6]  7  3 6  1   3  -1  -3   5  [3   6   7] 3 7 \def\arraystretch{1.2} \begin{array}{|c|c|c|}\hline \textsf{窗口位置} & \textsf{最小值} & \textsf{最大值} \\ \hline \verb![1 3 -1] -3 5 3 6 7 ! & -1 & 3 \\ \hline \verb! 1 [3 -1 -3] 5 3 6 7 ! & -3 & 3 \\ \hline \verb! 1 3 [-1 -3 5] 3 6 7 ! & -3 & 5 \\ \hline \verb! 1 3 -1 [-3 5 3] 6 7 ! & -3 & 5 \\ \hline \verb! 1 3 -1 -3 [5 3 6] 7 ! & 3 & 6 \\ \hline \verb! 1 3 -1 -3 5 [3 6 7]! & 3 & 7 \\ \hline \end{array} 窗口位置[1   3  -1] -3   5   3   6   7  1  [3  -1  -3]  5   3   6   7  1   3 [-1  -3   5]  3   6   7  1   3  -1 [-3   5   3]  6   7  1   3  -1  -3  [5   3   6]  7  1   3  -1  -3   5  [3   6   7]最小值133333最大值335567

输入格式

输入一共有两行,第一行有两个正整数 n , k n,k n,k
第二行 n n n 个整数,表示序列 a a a

输出格式

输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值

样例 #1

样例输入 #1

8 3
1 3 -1 -3 5 3 6 7

样例输出 #1

-1 -3 -3 -3 3 3
3 3 5 5 6 7

提示

【数据范围】
对于 50 % 50\% 50% 的数据, 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105
对于 100 % 100\% 100% 的数据, 1 ≤ k ≤ n ≤ 1 0 6 1\le k \le n \le 10^6 1kn106 a i ∈ [ − 2 31 , 2 31 ) a_i \in [-2^{31},2^{31}) ai[231,231)

这个题用暴力肯定是很容易想到的,从头到尾扫描,每次检查k个数,一共检查$O(nk)$次。暴力显然是会超时的,下面是用单调队列解决该题的思路,复杂度为$O(n)$。
在这个题中单调队列有如下特征
  • 队头元素始终是队列中最小的。根据需要输出队头 但是不一定弹出

  • 元素只能从头到尾进入队列 从队头 队尾都可以弹出

  • 序列中的每个元素都必须进入队列,比如 x x x进入队尾时,和原队尾y比较,如果 x ≤ y x \le y xy就从队尾弹出 y y y,一值弹到队尾所有比x大的元素,最后x进入队尾,这个入队操作就保证了队头元素是队列中最小的。

    我这样说可能有点抽象,举个实际点的例子。

    • 到食堂打饭排队时大家都有一个心理,在打饭之前要看看有什么菜,如果不好吃就走了。不过,能不能看到和身高有关,站在队尾的人如果个子高,眼光就能越过前面的人看到里面的菜,如果个子矮,会被挡住看不见。
    • 一个矮个子来排队,他希望队伍前面的人都比他更矮。如果他会魔法,他来排队时,队尾比他高的人就会自动从队尾离开,新的队尾如果仍然比他高,也会离开,最后新来的矮个子成了新的队尾,而且是最高的。他终于可以看到菜了,让人兴奋的是,菜很好吃,所以他肯定不想走。
    • 假设每个新来的人的魔法本领都比队列中的人更厉害,这样队伍就会变成这样:每个新来的人都能排到队尾,但是都会被后来的矮个子赶走,这样一来,整个队伍会始终满足单调性,由队头到队尾,由矮到高。
    • 但是,让这个魔法队伍郁闷的是,打饭阿姨一直忙自己的,顾不上打饭。所以排头的人等了一会就走了,等待时间就是题目中的 k k k。这里附带一个现象:队伍的长度不会超过 k k k
    • 输出是什么?每新来一个排队的人,排头如果还没走,他就向阿姨喊一声,这就是输出。
    • 以上就是这个题的现实模型。

下面我们举例来描述一下该算法的流程,队列为 { 1 , 3 , − 1 , − 3 , 5 , 3 , 6 , 7 } \{1, 3, -1, -3, 5, 3, 6, 7\} {1,3,1,3,5,3,6,7},你们可以理解这些数字为身高,想象一下负的身高哈哈哈,便于理解啦。下表中的“输出队首”就是本题的结果。
元素进入队尾 元素进队顺序 队列 窗口范围 队首是否在窗口内 输出队首 弹出队尾 弹出队首 1 1 { 1 }   [ 1   ] 是 3 2 { 1 , 3 }   [ 1 , 2   ] 是 − 1 3 { − 1 }   [ 1 , 2 , 3   ] 是 − 1 3 , 1 − 3 4 { − 3 }   [ 2 , 3 , 4   ] 是 − 3 − 1 5 5 { − 3 , 5 }   [ 3 , 4 , 5   ] 是 − 3 3 6 { − 3 , 3 }   [ 4 , 5 , 6   ] 是 − 3 5 6 7 { 3 , 6 }   [ 5 , 6 , 7   ] − 3 否, 3 是 3 − 3 7 8 { 3 , 6 , 7 }   [ 6 , 7 , 8   ] 是 3 \def\arraystretch{1.2} \begin{array}{|c|c|c|c|c|c|c|c|}\hline \textsf{元素进入队尾} & \textsf{元素进队顺序} & \textsf{队列} & \textsf{窗口范围}& \textsf{队首是否在窗口内}& \textsf{输出队首}& \textsf{弹出队尾}& \textsf{弹出队首}\\ \hline 1& 1& \{1\} & \ [1 \ ]& 是 & & & \\ \hline 3& 2& \{1, 3\} & \ [1, 2 \ ]& 是 & & &\\ \hline -1& 3& \{-1\} & \ [1, 2,3 \ ]& 是 &-1 &3,1 &\\ \hline -3& 4& \{-3\} & \ [2,3,4\ ]& 是 &-3 &-1 &\\ \hline 5& 5& \{-3, 5\} & \ [3,4,5 \ ]& 是 & -3& &\\ \hline 3& 6& \{-3, 3\} & \ [4,5,6\ ]& 是 & -3& 5 &\\ \hline 6& 7& \{3, 6\} & \ [5,6,7 \ ]& -3否,3是 & 3 & &-3\\ \hline 7& 8& \{3, 6, 7\} & \ [6,7,8 \ ]& 是 & 3& &\\ \hline \end{array} 元素进入队尾13135367元素进队顺序12345678队列{1}{1,3}{1}{3}{3,5}{3,3}{3,6}{3,6,7}窗口范围 [1 ] [1,2 ] [1,2,3 ] [2,3,4 ] [3,4,5 ] [4,5,6 ] [5,6,7 ] [6,7,8 ]队首是否在窗口内3否,3输出队首133333弹出队尾3,115弹出队首3

下面是这个题的具体实现代码

#include <bits/stdc++.h>

using namespace std;

using ll = long long;

const int N = 1e6 + 5;
int a[N];
deque<int> q;

void solve() {
	int n, k; cin >> n >> k;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	} 
	for (int i = 1; i <= n; ++i) {
		while (!q.empty() && a[q.back()] > a[i]) q.pop_back(); // 去尾
		q.push_back(i);
		if (i >= k) { // 每个窗口输出一次
			while (!q.empty() && q.front() <= i - k) q.pop_front(); // 删头
			cout << a[q.front()] << " ";
		}
	}
	cout << "\n";
	while (!q.empty()) q.pop_back(); // 清空队列 输出最大值
	for (int i = 1; i <= n; ++i) {
		while (!q.empty() && a[q.back()] < a[i]) q.pop_back(); // 去尾
		q.push_back(i);
		if (i >= k) { // 每个窗口输出一次
			while (!q.empty() && q.front() <= i - k) q.pop_front(); // 删头
			cout << a[q.front()] << " ";
		}
	}
	cout << "\n";
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int tt = 1; // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}

单调队列与最大子序和问题

下面再介绍一下单调队列的典型应用–最大子序和问题
子序和:在给定长度为n的整数序列A,它的子序列定义为A中非空的一段连续的元素,例如序列(6, -1, 5, 4, -7)前4个子序和为6 + -1 + 5 + 4 = 14
最大子序和问题按子序列有无长度限制为两种

  • 问题一,不限制子序列的长度,在所有可能的子序列中找到一个子序列,该子序列和最大。
  • 问题二,限制了子序列的长度m,找出一段长度不超过m的连续子序列,使得它的子序和最大。
    问题一比较easy,可以用贪心或者DP做,复杂度都是 O ( n ) O(n) O(n)
    问题二用单调队列,复杂度也是 O ( n ) O(n) O(n)。通过这个例子,我想你应该会明白为什么单调队列能用于DP优化
    问题一不是本节要写的,不过也可以先给出来作为对比,后面我会写DP专题。
    用贪心或者DP, O ( n ) O(n) O(n)时间内解决,下面是这个题
问题一 Max Sum(hdu 1003)

传送门: hdu1003

中文题意

给定一个序列,求最大的子序和。
INPUT: 第一行输入整数T,表示测试用例的个数 T ∈ [ 1 , 20 ] T \in[1, 20] T[1,20],每行第一个数输入 N N N,后面输入 N N N个数, N ∈ [ 1 , 100000 ] N \in [1, 100000] N[1,100000],每个数在 [ − 1000 , 1000 ] [-1000, 1000] [1000,1000]区间内
OUTPUT: 每个测试输出两行。第一行是"Case #:“其中”#"表示测试序号
第二行输出三个数,第一个数表示最大子序和,第二个和第三个是开始和终止位置。两个测试用例之间输出一个空行。

贪心

思路: 逐个扫描序列中的元素并累加,加上一个正数时,子序和会增加,加负数会减小。如果当前得到的和变成了负数,这个负数在接下来的累加中会减少后面的求和,所以不取它,从下一个位置重新开始求和。
代码

#include <bits/stdc++.h>

using namespace std;

using ll = long long;

void solve() {
	int t; cin >> t;
	for (int tt = 1; tt <= t; ++tt) {
		int n; cin >> n;
		int maxsum = INT_MIN;
		int l = 1, r = 1, p = 1; // 起点 终点 扫描位置
		int sum = 0; // 子序和
		for (int i = 1; i <= n; ++i) {
			int x; cin >> x;
			sum += x;
			if (sum > maxsum) {
				maxsum = sum;
				l = p;
				r = i;
			}
			if (sum < 0) {
				// 扫描到i时,若前面的最大子序和是负数 从下一个数开始求和
				sum = 0;
				p = i + 1;
			}
		}
		printf("Case %d:\n", tt);
		printf("%d %d %d\n", maxsum, l, r);
		if (tt != t) printf("\n");
	}




}

int main() {
	//ios::sync_with_stdio(false);
	// cin.tie(nullptr);
	int tt = 1;  // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}
动态规划做法

定义状态dp[i]表示以a[i]为结尾的最大子序和。转移有两种情况
dp[i]只包括一个情况 就是a[i]
dp[i]包括多个元素,从前面某个a[v]开始, v < i, 到a[i]结束
dp[i] = max(dp[i - 1] + a[i], a[i])

代码

#include <bits/stdc++.h>

using namespace std;

using ll = long long;

int dp[100005];

void solve() {
	int t; cin >> t;
	for (int tt = 1; tt <= t; ++tt) {
		int n; cin >> n;
		for (int i = 1; i <= n; ++i) {
			cin >> dp[i];
		}
		int l = 1, r = 1, p = 1;
		int maxsum = dp[1];
		for (int i = 2; i <= n; ++i) {
			if (dp[i - 1] + dp[i] >= dp[i]) 
				dp[i] = dp[i - 1] + dp[i];
			else p = i;
			if (dp[i] > maxsum) {
				maxsum = dp[i];
				l = p;
				r = i;
			}
		}
		printf("Case %d:\n", tt);
		printf("%d %d %d\n", maxsum, l, r);
		if (tt != t) printf("\n");
	}




}

int main() {
	//ios::sync_with_stdio(false);
	// cin.tie(nullptr);
	int tt = 1;  // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}
问题二的求解

这个要用到前缀和,还是先普及一下前缀和这个知识吧。
顾名思义前缀和sum[i]表示数组a第一个元素到第i个元素的和
即sum[i] = a[1] + a[2] + … + a[i]
利用递推可以 O ( n ) O(n) O(n)时间复杂度求前缀和
sum[i] = sum[i - 1] + a[i]
ok下面进入正题
和前面例题的"滑动窗口"类似,可以利用单调队列的“窗口,删头,去尾”解决问题二
首先求前缀和s[i],问题转换为找出两个位置l, r使得s[r]-s[l]最大,r-l<=m(m是限制的最大子序列的长度)
首先考虑用DP来求解,把问题进一步转化为:首先固定一个l,找到它左边的一个端点r,r-l<=m,使得s[r] - s[l]最大 定义这个最大值是dp[l],逐渐扩大l,求得所有的dp[l],其中的最大值就是该问题的解。

如果简单地暴力,对每个i检查比它小的m个s[k],那么总时间复杂度为 O ( n m ) O(nm) O(nm),这是会TLE(超时)的。
暴力的方法不可行,那么我们可以改用一个大小为m的窗口寻找最大子序和ans。从头到尾依次把s的元素放入这个窗口。

  1. 首先把s[1]放入窗口,并且记录ans的初始值s[1]
  2. 接着把s[2]放入窗口(假设窗口长度大于2),有两种情况,如果 s [ 1 ] ≤ s [ 2 ] s[1] \le s[2] s[1]s[2],那么更新 a n s = m a x { s [ 1 ] , s [ 2 ] , s [ 2 ] − s [ 1 ] } ans = max\{s[1], s[2],s[2]-s[1]\} ans=max{s[1],s[2],s[2]s[1]},如果 s [ 1 ] > s [ 2 ] s[1] >s[2] s[1]>s[2],那么保持 a n s = s [ 1 ] ans=s[1] ans=s[1]不变,从队列中弹出 s [ 1 ] s[1] s[1],只留下 s [ 2 ] s[2] s[2],类似于上面那个打饭的例子,前面比他高的就要出队,这是因为后面如果再将新的 s [ k ] s[k] s[k]放入窗口时, s [ k ] − s [ 2 ] s[k]-s[2] s[k]s[2] s [ k ] − s [ 1 ] s[k]-s[1] s[k]s[1]更大,这样就不满足单调性了。
  3. 继续这个过程,直到所有的 s s s处理结束

总结上面的思路,把新的 s [ i ] s[i] s[i]放入窗口时:

  • 把窗口内比 s [ i ] s[i] s[i]大的所有 s [ j ] s[j] s[j]都出队, i − j ≤ m i-j\le m ijm,因为这些 s [ j ] s[j] s[j]在处理 s [ i ] s[i] s[i]后面的 s [ k ] s[k] s[k]时用不到了, s [ k ] − s [ i ] s[k]-s[i] s[k]s[i]优于 s [ k ] − s [ j ] s[k]-s[j] s[k]s[j],于是保留 s [ i ] s[i] s[i]就可以了。
  • 若窗口内最小的是 s [ k ] s[k] s[k],此时肯定有 s [ k ] ≤ s [ i ] s[k]\le s[i] s[k]s[i],检查 s [ i ] − s [ k ] s[i]-s[k] s[i]s[k]是否为当前的最大子序和,若是,就更新 a n s ans ans
  • 每个 s [ i ] s[i] s[i]都会进入队列

此时最优的策略是一个“位置递增,前缀和也递增”的序列,用单调队列是最合适的。 s [ i ] s[i] s[i]进入队尾时,如果原队尾比 s [ i ] s[i] s[i]大,则去尾;如果队头超过窗口范围 m m m,则删头,而最小的 s [ k ] s[k] s[k]就是队头。该做法的原理和“滑动窗口”这个题差不多。在这个单调队列中,每个 s [ i ] s[i] s[i]只进出队列一次,时间复杂度为 O ( n ) O(n) O(n)

c++代码如下所示

#include <bits/stdc++.h>

using namespace std;

using ll = long long;

deque<int> dq;

ll s[100005], a[100005]; //前缀和数组

void solve() {
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= m; ++i) {
		cin >> a[i];
		s[i] = s[i - 1] + a[i];
	}
	int ans = -1e8;
	dq.push_back(0);
	for (int i = 1; i <= n; ++i) {
		while (!dq.empty() && dq.front() < i - m) dq.pop_front(); //队头超过m范围,删头
		if (dq.empty()) ans = max(ans, s[i]);
		else ans = max(ans, s[i] - s[dq.front()]); // 队头就是最小的s[k]
		while (!dq.empty() && s[dq.back()] >= s[i]) dq.pop_back(); // 队尾大于s[i] 去尾 
		dq.push_back(i);
	}
	cout << ans << "\n";

}

int main() {
	//ios::sync_with_stdio(false);
	// cin.tie(nullptr);
	int tt = 1;  // cin >> tt;
	while (tt--) {
		solve();
	}
	

	return 0;
}

在这个题中,用到了DP+单调队列,关于单调队列优化DP后面我会写个专题慢慢说。

优先队列

优先队列的特点是每次让优先级最高的先出队,优先队列的底层实现是堆这种数据结构,代码写起来复杂点,所以一般都不手写,而是使用STL中的priority_queu来实现,下面介绍一下使用。

STL priority_queue

priority_queue 是 C++ 标准模板库(STL)中的一个容器适配器,用于管理元素的优先级。它通常实现为一个堆(heap),默认情况下是一个最大堆,即最大的元素位于顶部。你可以通过自定义比较器将其转换为最小堆或实现其他优先级策略。

下面是 priority_queue 的详细使用指南,包括基本操作、不同的底层容器、如何自定义比较器等内容。

基本介绍
1. 定义
#include <queue>
#include <vector>
#include <functional>
2. 基本操作
  • 构造函数:创建一个空的优先队列或从其他容器复制。
  • push(const T& value):向队列中添加元素。
  • pop():移除队列顶部的元素。
  • top():访问队列顶部的元素。
  • empty()检查队列是否为空。
  • size():返回队列中元素的数量。
3.创建最大堆(默认)

默认情况下,priority_queue 是一个最大堆,最大的元素位于顶部。

#include <iostream>
#include <queue>
#include <vector>

int main() {
    std::priority_queue<int> pq;

    // 插入元素
    pq.push(10);
    pq.push(30);
    pq.push(20);
    pq.push(5);
    pq.push(1);

    // 输出并移除元素
    while (!pq.empty()) {
        std::cout << pq.top() << " "; // 输出最大元素
        pq.pop();
    }
    // 输出: 30 20 10 5 1
    return 0;
}

4. 创建最小堆

要创建一个最小堆,可以使用 std::greater 作为比较器。

#include <iostream>
#include <queue>
#include <vector>
#include <functional>

int main() {
    // 最小堆
    std::priority_queue<int, std::vector<int>, std::greater<int>> pq;

    pq.push(10);
    pq.push(30);
    pq.push(20);
    pq.push(5);
    pq.push(1);

    while (!pq.empty()) {
        std::cout << pq.top() << " "; // 输出最小元素
        pq.pop();
    }
    // 输出: 1 5 10 20 30
    return 0;
}

5. 自定义比较器为最小堆

假设你有一个自定义的 struct,例如学生结构体,你希望根据成绩来排序。

#include <iostream>
#include <queue>
#include <vector>
#include <string>

// 定义学生结构体
struct Student {
    std::string name;
    int score;

    // 构造函数
    Student(const std::string& n, int s) : name(n), score(s) {}
};

// 自定义比较器(最大堆,根据成绩排序)
struct CompareStudent {
    bool operator()(const Student& a, const Student& b) {
        // 如果 a 的成绩小于 b,则 a 在 b 之后
        return a.score < b.score;
    }
};

int main() {
    // 使用自定义比较器
    std::priority_queue<Student, std::vector<Student>, CompareStudent> pq;

    pq.emplace("Alice", 85);
    pq.emplace("Bob", 95);
    pq.emplace("Charlie", 75);
    pq.emplace("Diana", 90);

    while (!pq.empty()) {
        Student top = pq.top();
        std::cout << top.name << ": " << top.score << "\n";
        pq.pop();
    }
    // 输出:
    // Bob: 95
    // Diana: 90
    // Alice: 85
    // Charlie: 75
    return 0;
}

6. 自定义比较器为最小堆
#include <iostream>
#include <queue>
#include <vector>
#include <string>

struct Student {
    std::string name;
    int score;

    Student(const std::string& n, int s) : name(n), score(s) {}
};

// 自定义比较器(最小堆,根据成绩排序)
struct CompareStudentMinHeap {
    bool operator()(const Student& a, const Student& b) {
        // 如果 a 的成绩大于 b,则 a 在 b 之后
        return a.score > b.score;
    }
};

int main() {
    // 最小堆
    std::priority_queue<Student, std::vector<Student>, CompareStudentMinHeap> pq;

    pq.emplace("Alice", 85);
    pq.emplace("Bob", 95);
    pq.emplace("Charlie", 75);
    pq.emplace("Diana", 90);

    while (!pq.empty()) {
        Student top = pq.top();
        std::cout << top.name << ": " << top.score << "\n";
        pq.pop();
    }
    // 输出:
    // Charlie: 75
    // Alice: 85
    // Diana: 90
    // Bob: 95
    return 0;
}

7. 使用 Lambda 表达式作为比较器

自 C++11 起,可以使用 Lambda 表达式作为比较器,配合 std::function·或其他方式。不过,由于 priority_queue 需要比较器类型在编译时已知,因此直接在模板参数中使用 Lambda 是不太方便的。一个常见的解决方案是使用 std::vectorstd::make_heap 等函数,或定义一个单独的比较器结构。

不过,如果你仍然希望使用 Lambda,可以使用std::function,但这可能带来性能开销。

#include <iostream>
#include <queue>
#include <vector>
#include <functional>
#include <string>

struct Student {
    std::string name;
    int score;

    Student(const std::string& n, int s) : name(n), score(s) {}
};

int main() {
    // 使用 std::function 包装 Lambda
    auto cmp = [](const Student& a, const Student& b) -> bool {
        return a.score < b.score; // 最大堆
    };

    std::priority_queue<Student, std::vector<Student>, decltype(cmp)> pq(cmp);

    pq.emplace("Alice", 85);
    pq.emplace("Bob", 95);
    pq.emplace("Charlie", 75);
    pq.emplace("Diana", 90);

    while (!pq.empty()) {
        Student top = pq.top();
        std::cout << top.name << ": " << top.score << "\n";
        pq.pop();
    }
    // 输出:
    // Bob: 95
    // Diana: 90
    // Alice: 85
    // Charlie: 75
    return 0;
}

常见用法

  1. 优先级调度:任务调度系统中,可以根据任务的优先级执行任务。
  2. 图算法:如 Dijkstra 算法中,用于选择当前最短路径的节点。
  3. 合并多个有序序列:例如,合并 K 个有序链表。
  4. 实时数据流的 Top K 问题:维护实时数据流中的前 K 大元素。
    注意事项
  5. 效率:priority_queue 的插入和删除操作的时间复杂度为 O ( l o g n ) O(log n) O(logn)
  6. 访问限制:priority_queue 仅允许访问顶部元素,不支持随机访问或遍历。
  7. 底层容器:默认使用 std::vector,但也可以使用其他容器,如 std::deque,前提是满足 RandomAccessIterator 要求。
7. 完整示例

以下是一个综合示例,展示如何使用 priority_queue 进行任务调度,根据任务优先级执行任务

#include <iostream>
#include <queue>
#include <vector>
#include <string>

// 定义任务结构体
struct Task {
    std::string name;
    int priority;

    Task(const std::string& n, int p) : name(n), priority(p) {}
};

// 自定义比较器(优先级高的任务先执行)
struct CompareTask {
    bool operator()(const Task& a, const Task& b) {
        // 如果 a 的优先级低于 b,则 a 在 b 之后
        return a.priority < b.priority;
    }
};

int main() {
    // 创建优先队列
    std::priority_queue<Task, std::vector<Task>, CompareTask> taskQueue;

    // 添加任务
    taskQueue.emplace("Task1", 3);
    taskQueue.emplace("Task2", 1);
    taskQueue.emplace("Task3", 4);
    taskQueue.emplace("Task4", 2);

    // 执行任务
    while (!taskQueue.empty()) {
        Task current = taskQueue.top();
        std::cout << "Executing " << current.name << " with priority " << current.priority << "\n";
        taskQueue.pop();
    }
    // 输出:
    // Executing Task3 with priority 4
    // Executing Task1 with priority 3
    // Executing Task4 with priority 2
    // Executing Task2 with priority 1
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只微

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值