队列学习笔记

队列

认识一下

队列,英文名是 queue , 先进入队列的元素一定先出队列,因此队列通常也被称为先进先出(first in first out)表,简称 FIFO 表

基操这里(写栈的时候顺便了)


双端队列

双端队列是指一个可以在 队首/队尾 插入或删除元素的队列。相当于是栈与队列功能的结合。具体地,双端队列支持的操作有 4 个

  1. 在队首插入一个元素
  2. 在队尾插入一个元素
  3. 在队首删除一个元素
  4. 在队尾删除一个元素

用法 ↓

#include<deque>
deque<Type> d;
deque<Type> d(d1); 复制一个deque
d[i] : 返回d中下标为i的元素
d.front() : 返回第一个元素
d.back() : 返回最后一个元素
d.pop_back() : 删除尾部的元素 不返回值
d.pop_front() :删除头部元素 不返回值
d.push_back(e) : 在队尾添加一个元素e
d.push_front(e) : 在队头添加一个元素e
d.clear() : 清空所有元素
d.empty() : 空则返回1 不空返回0
d.insert(pos, num) : 在pos位置插入元素num
d.erase(pos) : 删除pos位置的元素 
  ...

deque是双向开口的连续性存储空间,与vector类似,不同的是deque支持在队首插入元素,而且在空间存储上也比vector更优,,,

就是常数大 还有不少函数就不举出了,想看更多用法及代码的点这里


数组模拟队列

我们可以用 数组 方便地模拟个队列 ↓

  • int q[SIZE], ql = 1, qr; 用两个变量标记队尾队首
  • q[++qr] = x; 插入元素
  • ++ql; 删除元素
  • q[ql] / q[qr] 访问队首队尾
  • ql = 1, qr = 0; 清空队列

数组模拟双端队列同理


循环队列

随时间推移,会出现 假溢出 (实际上有空闲位置但发生了上溢),

解决方法 : 将数组下标为 \(0\) 的位置看作是最后一个位置的后继 (x 的后继为 (x + 1) % size)


双栈模拟队列

有点冷门

使用两个栈 F,S 模拟一个队列, 其中 F 是队尾的栈, S 代表队首的栈, 支持 push (在队尾插入),pop (在队首弹出)

  1. Push : 插入到栈 F
  2. Pop : 如果 S 非空,让 S 弹栈;否则把 F 的元素倒过来圧到 S 中(其实就是一个一个弹出插入,做完后是首位颠倒的),然后再让 S 弹栈

易证,每个元素只会进入/转移/弹出一次,均摊复杂度 \(O(1)\)

双栈模拟双端队列的栗题

一个双端队列(deque),m 个事件:

  1. 在前端插入 (w,v)
  2. 在后端插入 (w,v)
  3. 删除前端的二元组
  4. 删除后端的二元组
  5. 给定 l,r,在当前 deque 中选择一个子集 S 使得 \(\sum\limits_{(w,v)\in S} w~mod~p\in[l,r]\) ,且最大化 \(\sum\limits_{(w,v)\in S}v\)

因为本人又菜又蒻所以就没有写..

单调队列

概念

元素递增或递减,只能从队首或队尾进行操作

一个栗题说明问题 ↓

滑稽窗口

维护两次队列,分别求最大值和最小值

#include <bits/stdc++.h>
using namespace std;
long long n, k, o[1000009], q[1000009], qq[1000009];

void miin(){
    int h = 1, t = 0;
    for(int i = 1; i <= n; i++){
        while(h <= t && q[h] + k <= i) h++;
        while(h <= t && o[i] < o[q[t]]) t--;
        q[++t] = i;
        if(i >= k) cout << o[q[h]] << " ";
    }
}

void maax(){
    int h = 1, t = 0; 
    for(int i = 1; i <= n; i++){
        while(h <= t && qq[h] + k <= i) h++;
        while(h <= t && o[i] > o[qq[t]]) t--;
        qq[++t] = i;
        if(i >= k) cout << o[qq[h]] << " ";
    }
}

int main(){
    cin >> n >> k;
    for(int i = 1; i <= n; i++) cin >> o[i];
    miin();
    cout << "\n";
    maax();
    return 0;
}

P1714 切蛋糕

板子?,,维护一个单调队列,及时把过期的蛋糕和没用的蛋糕清除(负的还不如不吃

#include <bits/stdc++.h>
using namespace std;
int n, m, a, ans, o[500009];
deque<int> q;

int Max(int x, int y){ return x > y ? x : y; }

int main(){
	std::ios::sync_with_stdio(0); 
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> a;
        o[i] = a + o[i - 1];
    } 
    for(int i = 1; i <= n; i++){
        while(q.size() and o[q.back()] > o[i]) q.pop_back();//确保队首最大
	q.push_back(i); 
        while(q.front() < i - m) q.pop_front();//过期清理
        ans = max(ans, o[q.back()] - o[q.front()]);     
    }
    cout << ans;
    return 0;
}

栗题

为了更好地理解生日礼物这道题,先来一道逛画展

P1638 逛画展

题目大意 : 给出一排画,找到一个最短区间,包含所有画师画的画

看到数据范围 N<=1000000,想到用 \(O(n)\) 的做法,遍历这 n 个点,遇到的点都放到队列中,若和队首相同则把队首弹出,更新 ans

#include <bits/stdc++.h>
using namespace std;

int n, m, tot, l, r, len =  1000009, o[1000009], a[2009];
deque<int> q;

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> o[i];
    for(int i = 1; i <= n; i++) {
        if(!a[o[i]]) tot ++;
        a[o[i]] ++;
        q.push_back(i);
        while(q.size() and a[o[q.front()]] > 1){
            a[o[q.front()]] --;
            q.pop_front();
        }
        if(tot == m){
            if(len > q.size()){
                len = q.size();
                l = q.front();
                r = q.back();
            }
        }
    }
    cout << l << " " << r;
    return 0;
}

P2564 [SCOI2009]生日礼物

题目大意 :一条彩带,找到一个最短区间,包含所有彩珠种类,一个位置可以放多个彩球

看到数据范围 N <= 1000000,我们考虑单调队列。因为彩珠分种类还得记录位置,考虑开个结构体,存颜色和位置,按位置排序。因为截取的是连续一段,所以队尾不弹出,而队首的弹出条件是:当这个颜色出现次数大于等于2。这个开桶记录一下就可以,然后用一个变量 tot 记录当前队列有几种颜色,当 tot = k 时,更新答案(队尾位置-队首位置)

//STL版
#include <bits/stdc++.h>
using namespace std;

int n, k, tot, ans = 2147483647, cnt, _, __, a[70];
struct hh{
    int sp, pos;
} o[1000009];
deque<hh> q;

bool cmp(hh a, hh b){ return a.pos < b.pos; }

int main(){
    std::ios::sync_with_stdio(0);
    cin >> n >> k;
    for(int i = 1; i <= k; i++){
        cin >> _;
        for(int j = 1; j <= _; j++){
            cin >> __;
            o[++cnt].pos = __, o[cnt].sp = i;
        }
    }
    sort(o + 1, o + n + 1, cmp);
    for(int i = 1; i <= n; i++){
        q.push_back(o[i]);
        a[o[i].sp] ++;
        if(a[o[i].sp] == 1) tot ++;
        while(tot == k){
            hh l = q.front(), r = q.back();
            if(ans > r.pos - l.pos) ans = r.pos - l.pos;
            q.pop_front();
            a[l.sp] --;
            if(!a[l.sp]) tot --;
        }
    }
    cout << ans;
    return 0;
}
#include <bits/stdc++.h>
#define N 1000010
using namespace std;
typedef long long ll;
inline int in() {
    int x = 0, f = 1; char C = getchar();
    while(C < '0' or C > '9') { if(C == '-') f = -1; C = getchar(); }
    while(C >= '0' and C <= '9') x = (x << 3) + (x << 1) + (C ^ 48), C = getchar();
    return x * f;
}
int n, k, h = 1, t, q[N], orz[100], ans = 0x7fffffff;
struct hh { int co, pos; } o[N];
bool cmp(hh x, hh y) { return x.pos < y.pos; }

int main() {
    n = in(), k = in();
    for(int i = 1, cnt, id = 0; i <= k; i ++) {
        cnt = in();
        for(int j = 1; j <= cnt; j ++) 
            o[++ id].co = i, o[id].pos = in();
    }
    sort(o + 1, o + n + 1, cmp);
    for(int i = 1, tot = 0; i <= n; i ++) {
        if(!orz[o[i].co]) tot ++;
        q[++ t] = i; orz[o[i].co] ++;
        while(h <= t and orz[o[q[h]].co] > 1) orz[o[q[h]].co] --, h ++;
        if(tot == k) ans = min(ans, o[q[t]].pos - o[q[h]].pos);
    }
    cout << ans;
    return 0;
}

P2569 [SCOI2010]股票交易

题目大意 : zcl 通过肉眼不可见的观察,预测到了某支股票未来T天的走势,在第 i 天,买入每股花 \(AP_i\) ,一次最多买 \(AS_i\) 股,卖出每股得 \(BP_i\) ,(\(AP_i ≥ BP_i\)) ,一次最多卖 \(BS_i\) 股,由于某种原♂因,两次交易至少间隔 \(W\) 天(第 \(i\) 天交易,第 \(i+W+1\) 天才能再次交易,买入/卖出均算一次交易, 由于 zcl 还要找妹子, 他最多把精力放在 \(MaxP\) 支股票上,,在第 \(0\) 天,zcl 幻想手里有一笔巨款,想要赚到最多的钱,他给自己设计了一个程序,但是忘了保存,他想让你帮他还原这个程序...

不难想到本题可以用动态规划来解,队列&DP

数据范围 \(0≤T≤2000\) ,二维数组开得起

第一个维度,用来表示是第几天

第二个维度,记录拥有多少张股票

得:\(f[i][j]\) 表示第 $i $ 天后拥有 \(j\) 张股票可以赚到的最多钱数

分情况讨论 :

\(i\) 天,

  1. 买入 a. 从0开始 b.前一状态转移而来
  2. 卖出
  3. 不买卖

不难发现,一共4种情况,其中1a的情况是独立的(没有转移)

1 . 买入,0基础

仅本情况下的状态可以直接赋值,其他状态初值为 -inf,即:

\[f[i,j] = -ap_i \times j \]

2 . 不买卖

直接由上一天转移,且拥有股票数量不变

\[f[i][j]=max(f[i][j],~f[i-1][j]) \]

3 . 买入,在之前的基础上

假设在第 \(i\) 天交易了,下次交易最早在 \(i+W+1\) 天,但是也可以在第 \(i+W+2\) 天交易,,所以 \(f[i][j]\) 不一定是哪天转移来的(鸽了不止 \(W\) 天的情况),但是情况2已经考虑到了所以不用担心(。-ω-)

现在要求 \(f[i][j]\),若第 \(i-W-1\) 天有 \(k\) 张股票,因为现在要买入,所以 \(j\) 是大于 \(k\) 的,考虑到 zcl 单次购买是有上限的,所以 \(k\) 最小等于 \(j-as_i\) ,→ \((j-as_i⩽k<j)\) ,然后本次交易买了 \(j-k\) 张股票,花费 \((j-k)*ap_i\) ,所以转移方程为

\[f[i][j]=max[f[i][j],~f[i-W-1][k]-(j-k)\times ap_i] \]

4 .卖出

卖完之后股票变少了,所以 \(j<k⩽j+bp_i\) ,卖出所得为 \((k-j)\times bp_i\),所以转移方程为

\[f[i][j]=max(f[i][j],f[i-W-1][k]+(k-j)\times bp_i) \]

dp部分结束 ❀❀✿

对于3,4,我们发现时间复杂度是 \(O(n^3)\) qaq,

img

全剧终❀❀

正题来了 ↓ ↓ ↓

队列优化dp

利用队列的单调性,我们可以把ta优化成时间复杂度为 \(O(n^2)\)

转移方程为

\[f[i][j]=max(f[i][j],~f[i-W-1][k]-(j-k)\times ap_i)(j-as_i⩽k<j) \]

乘法分配率

\[f[i][j]=max(f[i][j],f[i-W-1][k]-j\times ap_i+k\times ap_i)(j-as_i⩽k<j) \]

现要转移给 \(f[i][j]\),因为我们要找的是区间 \(j-as_i⩽k<j\) 内的最大值,而变量是 \(k\),所以可以把 \(j\times ap_i\) 提出来,发现 \(f[i-W-1][k]+k\times ap_i\) 可以单调性优化,也就是求一个区间中的最大值,这不就是滑稽♂窗口嘛

然后对于情况3,正序,对于情况4,逆序, 。

#include<bits/stdc++.h>
using namespace std;
int n, m, w, ap, as, bp, bs, ans, q[2010], f[2010][2010];

int main() {
	cin >> n >> m >> w;
	memset(f, 128, sizeof f);
	for(int i = 1; i <= n; i ++) {
		cin >> ap >> bp >> as >> bs;
		for(int j = 0; j <= as; j ++) f[i][j] = -1 * j * ap;
		for(int j = 0; j <= m; j ++) f[i][j] = max(f[i][j], f[i - 1][j]);
		if(i <= w) continue;
		
		for(int j = 0, h = 1, t = 0; j <= m; j ++) {
			while(h <= t and q[h] < j - as) h ++;
			while(h <= t and f[i - w - 1][q[t]] + q[t] * ap <= f[i - w - 1][j] + j * ap) t --;
			q[++ t] = j;
			if(h <= t) f[i][j] = max(f[i][j], f[i - w - 1][q[h]] + q[h] * ap - j * ap);
		}
		for(int j = m, h = 1, t = 0; j >= 0; j --) {
			while(h <= t and q[h] > j + bs) h ++;
			while(h <= t and f[i - w - 1][q[t]] + q[t] * bp <= f[i - w - 1][j] + j * bp) t --;
			q[++ t] = j;
			if(h <= t) f[i][j] = max(f[i][j], f[i - w - 1][q[h]] + q[h] * bp - j * bp);
		}
	}
	for(int i = 0; i <= m; i ++) ans = max(ans, f[n][i]);
	cout << ans;
	return 0;
}

优先队列

认识一下

\(priority\_queue\) ←就是这东西,搁 \(STL\) 里的 ,你还在手打堆吗 ,包含了队列所拥有的所有特性和基操, 只是在队列的基础上添加了内部的一个排序,本质是由二叉堆来实现的,每次插入一个数据都是插入到数据数组的最后一个位置上,然后再做上浮操作,,,

基操

#include<queue>
priority_queue<Type, Container, Functional>
//Type为数据类型,Container为容器类型 比如vector deque(list除外),Functional为比较的方式
priority_queue<int, vector<int>, greater<int> > q;
priotity_queue<int, vector<int>, less<int> > q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)→此为引用,我也没看懂(划掉)
q.top(); //访问队头元素
q.empty(); //队列是否为空
q.size(); //返回元素个数
q.push(); //插入元素到队尾并排序
q.emplace(); //原地构造一个元素并插入队列
q.pop(); //弹出队头元素
q.swap(); //交换内容

行了行了认识一下得了
全剧终❀✿✿

posted @ 2020-07-19 17:54  hulne  阅读(357)  评论(1编辑  收藏  举报