①核心要点与万能模板
| 知识板块 | 必须记住的核心 |
| deque 声明 | deque<int> d;,支持 d[i] 随机访问,迭代器可 sort |
| deque 双端操作 | push_front / push_back / pop_front / pop_back,全部 O(1) 均摊 |
| deque 禁忌 | 没有 capacity / reserve;内存不连续,别传裸指针给 C API |
| priority_queue 默认 | priority_queue<int> 是大根堆,top() 是最大值 |
| 小根堆写法 | priority_queue<int, vector<int>, greater<int>>,三参数缺一不可 |
| 比较器方向 | 返回 a.x > b.x → x 小的浮到堆顶;符号与直觉相反,务必手推 |
| pair 入堆 | 默认按 first 再按 second 的字典序;存 {-x, y} 可把大根堆当小根堆 |
| 懒惰删除 | 双堆互消(pq.top()==del.top() 同时弹)或 (值,编号) + alive 标记 |
| bitset 声明 | bitset<N> b; —— N 必须是编译期常量,运行时不可变 |
| bitset 统计 | count / any / none / all,全部 O(N/64) |
| bitset 位运算 | & | ^ ~ << >> 整块并行,比 for 循环快 64 倍 |
| bitset 三大场景 | 传递闭包 f[i] |= f[k];01 背包 f |= f << x;子集枚举 |
| 复杂度速览 | deque 两端 O(1);堆 push/pop O(log n)、top O(1);bitset 位运算 O(N/64) |
C++万能模板:三大容器核心操作速记卡
#include <bits/stdc++.h>
using namespace std;
// ============ 一、deque 模板 ============
void dequeTemplate() {
deque<int> d;
d.push_back(3); // 尾部进
d.push_front(1); // 头部进(vector 做不到)
d.pop_back(); // 尾部出
d.pop_front(); // 头部出
int a = d.front(), b = d.back();
int c = d[0]; // 随机访问 O(1)
sort(d.begin(), d.end()); // 迭代器是随机访问,可以排序
// d.reserve(1000); // ❌ 不存在这个接口
// ★ 单调队列(求滑动窗口最小值)核心四行
// while (!dq.empty() && a[dq.back()] >= a[i]) dq.pop_back();
// dq.push_back(i);
// while (dq.front() <= i - k) dq.pop_front();
// if (i >= k) ans = a[dq.front()];
}
// ============ 二、priority_queue 模板 ============
void pqTemplate() {
// ① 大根堆(默认)
priority_queue<int> big;
// ② 小根堆(三个参数写全)
priority_queue<int, vector<int>, greater<int>> small;
// ③ pair 大根堆(先比 first)
priority_queue<pair<int,int>> pp;
big.push(5);
int t = big.top(); // O(1) 取最值
big.pop(); // O(log n) 删堆顶
bool e = big.empty();
int s = big.size();
// big.clear(); // ❌ 没有 clear,只能循环 pop
// for (int x : big) ; // ❌ 不可遍历
}
// 自定义比较器:返回 true 表示 a 优先级更低(沉下去)
struct Node { int dis, id; };
struct Cmp {
bool operator()(const Node& a, const Node& b) const {
if (a.dis != b.dis) return a.dis > b.dis; // dis 小的优先
return a.id > b.id;
}
};
// priority_queue<Node, vector<Node>, Cmp> pq;
// ============ 三、bitset 模板 ============
const int MAXN = 100005;
void bitsetTemplate() {
bitset<MAXN> b;
b.set(); b.reset(); b.flip(); // 全 1 / 全 0 / 全取反
b.set(3); b.reset(3); b.flip(3); // 单点操作
cout << b[3] << b.count() << b.any() << b.none() << b.all();
bitset<MAXN> c;
b &= c; b |= c; b ^= c; b = ~c; // 位运算 O(N/64)
b <<= 3; b >>= 3; // 移位
string s = b.to_string();
// ★ 场景 A:01 背包可达性
// f[0] = 1;
// for (int x : a) f |= (f << x);
// if (f[S]) cout << "YES";
// ★ 场景 B:传递闭包(Floyd 位运算版)
// for (int k = 1; k <= n; k++)
// for (int i = 1; i <= n; i++)
// if (g[i][k]) g[i] |= g[k];
}
// ============ 四、手写小根堆(下标从 1 开始)============
long long h[100005]; int sz = 0;
void upHeap(int i) { while (i > 1 && h[i] < h[i>>1]) { swap(h[i], h[i>>1]); i >>= 1; } }
void downHeap(int i) { while ((i<<1) <= sz) { int s = i<<1;
if (s + 1 <= sz && h[s+1] < h[s]) s++;
if (h[i] <= h[s]) break; swap(h[i], h[s]); i = s; } }
void hpPush(long long v) { h[++sz] = v; upHeap(sz); }
void hpPop() { h[1] = h[sz--]; downHeap(1); }
🔍 代码逐段解析 · 三大容器万能模板
一、dequeTemplate —— 两端进出、d[0] 随机访问、整体 sort;注释标出 reserve 不存在;末尾附单调队列核心四行(压尾 / 去尾 / 判头过期 / 取头)。
二、pqTemplate —— 大根堆、小根堆、pair 堆三种声明;top O(1)、pop O(log n);注释标出没有 clear()、不可遍历——清空用 pq = {};。
比较器 Cmp —— operator()(const Node&, const Node&) const,返回 a.dis > b.dis 表示 dis 小的优先;dis 相同比 id,保证出堆顺序确定。
三、bitsetTemplate —— 全量/单点 set、reset、flip;count/any/none/all 统计;&=、|=、^=、移位;末尾附 01 背包与传递闭包两个「★ 场景」的骨架代码。
四、手写堆 —— upHeap 与父亲比、downHeap 选较小儿子;hpPush 放 h[++sz] 上浮,hpPop 用末元素盖顶下沉——与 q1 方法二完全一致,可直接当板子背。
易错点 —— ① 手写堆全局 sz 别忘初始化 0;② 模板里 b = ~c 是把 c 取反赋给 b,注意别和 b.flip() 混淆。
②拓展案例
拓1对顶堆求动态中位数
题目:依次读入 n 个数,每读入一个就输出当前所有数的中位数。n ≤ 10⁵。
思路:用一个大根堆 lo 存较小的一半、一个小根堆 hi 存较大的一半,并始终维持两者大小差 ≤ 1。中位数就是 lo.top()。每次插入后调整平衡,调整总次数是 O(n),所以总复杂度 O(n log n)。
C++对顶堆求中位数(P1168 模型)
#include <bits/stdc++.h>
using namespace std;
priority_queue<int> lo; // 大根堆:较小的一半
priority_queue<int, vector<int>, greater<int>> hi; // 小根堆:较大的一半
void add(int x) {
if (lo.empty() || x <= lo.top()) lo.push(x);
else hi.push(x);
// 保持 lo.size() == hi.size() 或 lo.size() == hi.size()+1
if ((int)lo.size() > (int)hi.size() + 1) { hi.push(lo.top()); lo.pop(); }
if ((int)hi.size() > (int)lo.size()) { lo.push(hi.top()); hi.pop(); }
}
int main() {
ios::sync_with_stdio(0);
int n; cin >> n;
for (int i = 0; i < n; i++) {
int x; cin >> x;
add(x);
if (i % 2 == 0) cout << lo.top() << "\n"; // P1168:奇数个时输出
}
return 0;
}
运行结果(P1168 只在奇数个时输出中位数)
输入: 7
3 1 5 2 8 7 4
输出:
3
3
3
4
与上一讲的联系:s2-04 里我们用两个 multiset 做了同样的事。堆版本的优势是常数更小、内存更省;multiset 版本的优势是可以顺便求第 k 小。同一思想、两种实现,这就是「一题多解」的典型对比。
🔍 代码逐段解析 · 对顶堆求动态中位数
两个堆(第 4~5 行) —— lo 大根堆存较小一半,hi 小根堆存较大一半;不变式:lo.size() == hi.size() 或 lo.size() == hi.size()+1。
add 分流(第 7~9 行) —— x <= lo.top() 进 lo,否则进 hi;lo 空时直接进 lo。
两次 if 调平(第 11~12 行) —— 单次插入只会让大小差越界 1 个单位,所以各用一次 if 搬一个堆顶即可恢复平衡。
输出时机(第 19 行) —— i % 2 == 0(i 从 0 起)即每读入奇数个数时输出 lo.top(),样例输出 3、3、3、4。
易错点 —— ① 两个 if 的顺序(先 lo 超、后 hi 超)不能颠倒;② size() 是无符号数,比较前要 (int) 强转。
拓2多路归并:求第 k 小的和
题目:两个长度均为 n 的递增数组 A、B(n ≤ 10⁵),求 A[i]+B[j] 的所有 n² 个和中最小的 n 个。
思路一:暴力全算再排序O(n²log n),n=10⁵ 时 10¹⁰ 超时
思路二:小根堆多路归并O(n log n) 把问题看成 n 条有序链:A[0]+B[0] ≤ A[0]+B[1] ≤ … 堆里先放入 (A[i]+B[0], i, 0) 共 n 项,每次弹出最小值后把同一行的下一个推进堆,弹出 n 次即得答案。
C++小根堆多路归并(P1631 模型)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(0);
int n; cin >> n;
vector<int> A(n), B(n);
for (int& x : A) cin >> x;
for (int& x : B) cin >> x;
sort(A.begin(), A.end());
sort(B.begin(), B.end());
// 小根堆:存 (和, A 的下标, B 的下标)
priority_queue<tuple<int,int,int>, vector<tuple<int,int,int>>,
greater<tuple<int,int,int>>> pq;
for (int i = 0; i < n; i++) pq.push({A[i] + B[0], i, 0});
for (int c = 0; c < n; c++) {
auto [sum, i, j] = pq.top(); pq.pop();
cout << sum << (c == n - 1 ? '\n' : ' ');
if (j + 1 < n) pq.push({A[i] + B[j + 1], i, j + 1});
}
return 0;
}
运行结果
输入: 3
1 3 5
2 4 6
输出: 3 5 5
易错:忘记先对 A、B 排序,「每行有序」的前提就不成立,堆里推出的下一个不一定是该行最小值,答案失效。
拓3bitset 做子集存在性判定
题目:给定 n 个不超过 1000 的正整数(n ≤ 10⁶,有大量重复),求这些数的子集和能否组成 [0, S] 中的全部整数?只问「能组成哪些和」,输出个数。
思路:这就是 01 背包的可达性问题,但 n 大到 10⁶。用 bitset 可以在 O(nS/64) 内完成:f |= f << x。更妙的优化:因为值域只有 1000,可以把相同的数按「多重背包二进制拆分」合并,n 秒降到 O(值域 × log n × S/64)。
C++bitset 可达和统计 + 计数数组优化
#include <bits/stdc++.h>
using namespace std;
const int MAXS = 2000005;
bitset<MAXS> f;
int main() {
ios::sync_with_stdio(0);
int n, S; cin >> n >> S;
vector<int> cnt(1005, 0);
for (int i = 0; i < n; i++) { int x; cin >> x; cnt[x]++; }
f[0] = 1;
for (int v = 1; v <= 1000; v++) {
if (!cnt[v]) continue;
// 二进制拆分:把 cnt[v] 个 v 拆成 1,2,4,8,... 组,转成 01 背包
int c = cnt[v];
for (int p = 1; c > 0; p <<= 1) {
int take = min(p, c);
f |= (f << (v * take));
c -= take;
}
}
cout << f.count() << " 种不同的和可以被凑出\n";
return 0;
}
运行结果
输入: 6 12
2 2 3 3 3 5
输出: 17 种不同的和可以被凑出
(总和为 18,可达集合为 0~18 中除 1 和 17 外的全部整数,共 17 个)
🔍 代码逐段解析 · bitset 可达和 + 计数数组优化
计数(第 8~9 行) —— 值域只有 1000,用 cnt[v] 统计每个值出现次数,n = 10⁶ 的输入被压成 1000 个 (值, 个数) 对。
二进制拆分(第 12~18 行) —— 把 c 个 v 拆成 1、2、4、…、take 组(take = min(p, c)),每组做一次 f |= (f << v·take):log c 次位运算覆盖 c 个副本。
统计(第 19 行) —— f.count() 一次数出所有可达和的个数,样例得 17。
易错点 —— ① 拆分循环 for (p=1; c>0; p<<=1) 里 take 用完要 c -= take,忘减会死循环;② 读入的 S 若只是「上限」而不参与统计,别把它当值域开 bitset(本题按总和 18 即可覆盖)。
要点:二进制拆分把「每种物品有 c 个」的多重背包转化成 O(log c) 个 01 背包物品,配合 bitset 让整体复杂度降到可接受范围。这是「多重背包 + bitset」的组合套路,在提高组及以上非常常见。