第 26 节的 BFS 只有一条规矩:所有点从队尾进、从队头出。这条规矩之所以对,是因为每条边的长度都等于 1 —— 先被发现的点一定不比后发现的远。可一旦边权出现 0,这个前提就断了:一个点是绕了三步才发现的,距离却可能和第一步就发现的点一样。“第几层”不再等于“多远”,所以要换容器。
本节的另一半内容方向刚好相反:有些问题根本不关心先后顺序,只关心“当前最小的是谁”。这时用 priority_queue(二叉堆),每次取最值 O(log n),付出的代价是彻底放弃顺序。
| 容器 | 元素怎么进出 | 能拿到什么 | 单次代价 | 用在哪种题上 |
|---|---|---|---|---|
| queue<int> | 队尾进、队头出 | 最旧的元素 | O(1) | 边权全为 1 的 BFS、按层扩散 |
| deque<int> | 两端都能进、两端都能出 | 最旧的和最新的 | O(1) | 0-1 BFS、滑动窗口最值(第 19 节) |
| priority_queue<int> | 随便进、只能从堆顶出 | 当前最大/最小 | O(log n) | 每步取当前最优、多路归并、Top-K |
表里只有 deque 那一行是新的容器,另外两个前面都见过。但“见过”和“能说清它为什么能两端 O(1) 插入”是两件事 —— deque 的内存是分段的:一段一段申请,用一张表记录各段的首地址,头插就往前一段挂,尾插就往后一段挂,所以不像 vector 那样要把整块数据搬家。代价是元素不保证连续,&dq[0] + n 这种指针算术在 deque 上是危险的。
| 组成 | 含义 / 格式要求 |
|---|---|
deque<int> | 头文件 <deque>(bits 已包含)。带下标的双端队列,两端插入删除都是 O(1)。 |
push_back(x) / push_front(x) | 在队尾 / 队头插入一个元素。这是 deque 与 vector 的实质区别:vector 的 push_front 是不存在的,insert(begin(), x) 是 O(n)。 |
pop_back() / pop_front() | 删除队尾 / 队头元素,O(1)。返回 void,不返回值;要取值必须先 back() / front()。 |
front() / back() | 读队头 / 队尾,O(1),不删除。容器为空时调用是未定义行为,通常表现为 RE。 |
empty() / size() | 判空 / 元素个数。pop 之前先判空,是本节所有代码的固定动作。 |
dq[i] | 支持下标访问,O(1)。但一旦用下标,说明你只需要随机访问,那就该用 vector 了。 |
deque<int> dq;
dq.push_back(1); // [1]
dq.push_back(2); // [1, 2]
dq.push_front(0); // [0, 1, 2] 只有 deque 能这么干
cout << dq.front() << dq.back(); // 02
dq.pop_front(); // [1, 2]
dq.pop_back(); // [1]
deque<int> d2;
// d2.front(); // 空容器上取 front:未定义行为,现场 RE
if (!d2.empty()) d2.pop_front(); // 正确写法:先判空deque 没有 reserve(),也不能用 memcpy / 指针遍历 —— 内存是分段的。
0-1 BFS 里只有边权为 0 才允许 push_front。把边权为 1 的也插到队头,队列的距离序列不再递增,同一个点会被反复松弛,答案虽然还对,但最坏退化成 SPFA 量级 → TLE。
| 组成 | 含义 / 格式要求 |
|---|---|
T(第 1 个参数) | 元素类型。可以是 int、long long、pair<int,int>,或自己写的 struct。 |
Container(第 2 个) | 底层容器,默认 vector<T>。要求随机访问 + push_back + pop_back,所以只能是 vector 或 deque,写 list 会编译失败。平时照抄 vector<T>。 |
Compare(第 3 个) | 比较器,回答“谁排在后面”。默认 less<T>,于是 top() 是最大值;写 greater<T> 则 top() 是最小值(第 17 节用过)。 |
三个参数必须写全 | 想换比较器,前两个也得一起写出来。只写 priority_queue<int, greater<int>> 会得到一屏指向 <bits/stl_queue.h> 的模板报错 —— 因为第二个位置固定是容器。 |
自定义比较器 | 写函数对象:struct Cmp { bool operator()(const Node &x, const Node &y) const { return x.w > y.w; } },然后 priority_queue<Node, vector<Node>, Cmp> q; 不能直接传函数名,除非用 decltype 取它的类型。 |
Compare 的语义 | Cmp(a, b) == true 表示 a 的优先级更低、排在 b 后面。所以想要小根堆,就要让“大的”优先级低 —— 传 greater,或写 x.w > y.w。 |
// 写法一:小根堆
priority_queue<int, vector<int>, greater<int>> small;
// 写法二:pair 默认按 first 比,first 相同再比 second
priority_queue<pair<int, int>> p; // 默认大根堆
p.push({2, 5}); p.push({2, 3}); p.push({1, 9});
cout << p.top().first << ' ' << p.top().second; // 2 5(first 同,second 大的在前)
// 写法三:自定义比较器,按 w 小的优先
struct Node { int w, id; };
struct Cmp { bool operator()(const Node &x, const Node &y) const { return x.w > y.w; } };
priority_queue<Node, vector<Node>, Cmp> q;priority_queue 不能遍历、不能删除任意元素、不能修改已入堆元素的值(改了也不会重排)。要“改值”就再 push 一份新的,取出时和记录里的最新值比一下,旧的那份就跳过。
pair 的默认比较是字典序:{dist, id} 放进小根堆,距离小的先出,距离相同时 id 小的先出。如果题目对同距离的顺序敏感,就别依赖这个次序。
有 n 个函数 F_i(x) = a_i·x² + b_i·x + c_i,定义域是正整数 x = 1, 2, 3, …。把所有函数在所有正整数上取到的值合在一起,求其中最小的 m 个,按升序输出(重复的值各算一个)。
输入格式:第一行两个整数 n、m;接下来 n 行,每行三个正整数 a_i、b_i、c_i。
输出格式:m 行,每行一个整数,依次为第 1 小、第 2 小……第 m 小的函数值。
数据范围:1 ≤ n, m ≤ 10⁴,a_i 恒为正(保证每个函数在正整数上单调递增)。
第一步:每个函数自己是有序的。a_i > 0,抛物线开口向上,对称轴在 x = −b_i/(2a_i) < 0,所以整数部分 x ≥ 1 整段都落在增区间上:F_i(1) ≤ F_i(2) ≤ F_i(3) ≤ …。这一步把“无穷多个值”变成了“一条递增序列”。
第二步:问题变成 n 路归并。每个函数贡献一条递增序列,全局前 m 小就是这 n 条序列归并后的前 m 项。这和第 17 节合并果子是同一个结构:每一步在 n 个候选里取最小。
第三步:候选只需要 n 个。对第 i 个函数,只把“它当前还没输出的最小项”放进堆。堆顶就是全体候选的最小值,输出它,然后让这个函数往前走一格(x 加 1)再放回去。其他函数的候选没变,不用动。
为什么不会漏。堆里始终装着每条序列“尚未输出部分”的首元素。全局最小必然等于某条序列尚无输出的首元素,所以它一定在堆里 —— 取堆顶就是取全局最小。
为什么只推进 m 次。输出 m 个数就结束;每个函数最多被推进 m 次,堆的大小恒为 n,总复杂度 O((n + m) log n)。n = m = 10⁴ 时是 2×10⁴ 次堆操作、每次约 14 次比较,总共几十万次比较,可以忽略不计。
// 核心只有三步:取堆顶 -> 输出 -> 同一函数推进一格
Node cur = pq.top();
pq.pop();
cout << cur.val << '\n';
int nx = cur.x + 1; // 只推进这一个函数,别的候选不动
long long nv = f[cur.id].a * nx * nx + f[cur.id].b * nx + f[cur.id].c;
pq.push({nv, cur.id, nx});函数值最大能到 a_i·x² 量级,x 可以到 10⁴,所以乘积必须用 long long 存,这一点在代码里靠 long long a 让整条表达式自动提升。
#include <bits/stdc++.h>
using namespace std;
struct Func { long long a, b, c; }; // 系数只存一份,堆结点里不重复存
struct Node {
long long val; // 这个函数当前能给出的最小值
int id, x; // 来自第 id 个函数,自变量取 x
// 小根堆:val 大的"排在后面"。方向和 sort 的 cmp 相反(第 17 节)
bool operator<(const Node &o) const { return val > o.val; }
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<Func> f(n);
priority_queue<Node> pq; // 大小恒为 n:每个函数只放一个候选
for (int i = 0; i < n; ++i) {
cin >> f[i].a >> f[i].b >> f[i].c;
long long v = f[i].a + f[i].b + f[i].c; // F_i(1) = a+b+c,a 是 long long 自动提升
pq.push({v, i, 1});
}
for (int t = 0; t < m; ++t) {
Node cur = pq.top();
pq.pop(); // pop 不返回值,所以先把堆顶存下来
cout << cur.val << '\n';
// 只让同一个函数往前走一格:其他函数的候选本来就在堆里,不需要重放
const Func &g = f[cur.id];
int nx = cur.x + 1;
long long nv = g.a * nx * nx + g.b * nx + g.c;
pq.push({nv, cur.id, nx});
}
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
struct Func { long long a, b, c; }; | 系数单独存在 vector 里。堆里每个结点只带 id,重算时回表取系数,结点小、拷贝便宜。 |
bool operator<(const Node &o) const { return val > o.val; } | 整个算法的方向由这一行决定。写成 val < o.val 就是大根堆,每次弹出当前最大值,答案整片错。末尾的 const 不能漏,漏了报错指向模板深处。 |
long long v = f[i].a + f[i].b + f[i].c; | 初始候选是 x = 1 的值。因为 a 是 long long,加法的每一步都按 64 位算,不会溢出。 |
priority_queue<Node> pq; | 三个模板参数这里只写了第一个,因为另外两个用默认值(vector<Node> 和 less<Node>),而 less<Node> 会去调我们刚重载的 operator<。 |
Node cur = pq.top(); pq.pop(); | 先复制再删。pq.pop() 返回 void,直接写 cur = pq.pop() 编译不过。 |
cout << cur.val << '\n'; | 输出的是值,不是下标、也不是 x。这里最容易在改代码时手滑写成 cur.id。 |
const Func &g = f[cur.id]; | 引用而不是拷贝,避免每次迭代复制三个 long long;const 只是表明不修改。 |
pq.push({nv, cur.id, nx}); | 把同一个函数的下一个值放回去。写成别的 id,那条序列就会断层,答案偏大。 |
pq 的大小恒为 n | 初始 n 个候选,每轮一出一进,所以堆永远是 n 个元素。O(m log n) 的前提就靠这一点。 |
问题:n 个数里找出最大的 k 个(k 远小于 n),n 可以到 10⁶。排序是 O(n log n),而只留 k 个候选可以做到 O(n log k)。
做法:维护一个大小为 k 的小根堆。堆顶是“当前第 k 大”,也就是候选里最差的那个。新来的数只有比堆顶大,才可能挤进前 k,于是弹掉堆顶、把新数放进去;否则直接扔掉。
为什么这样对:扫描到任意位置时,堆里恰好是“已扫描部分最大的 min(k, 已扫描个数) 个”。归纳一步:新数 x 若 ≤ 堆顶,则堆里已有 k 个数 ≥ 堆顶 ≥ x,x 挤不进前 k(最多与第 k 名并列);若 x > 堆顶,堆顶是 k 个里最差的,用 x 换掉它,剩下的仍然是最大的 k 个。
// n 个数里最大的 k 个,按降序输出
priority_queue<int, vector<int>, greater<int>> q; // 小根堆,堆顶 = 第 k 大
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
if ((int)q.size() < k) q.push(x); // 前 k 个直接进
else if (x > q.top()) { // 只有更大才值得换
q.pop();
q.push(x);
}
}
vector<int> ans;
while (!q.empty()) { ans.push_back(q.top()); q.pop(); } // 弹出是升序
reverse(ans.begin(), ans.end()); // 翻成降序两个必须记住的细节:①堆的大小没到 k 时不能读 q.top()(空堆 RE),所以先判 size;②求“最小的 k 个”时方向完全反过来 —— 用大根堆,堆顶是第 k 小,条件是 x < 堆顶。堆顶永远是“当前候选里最差的那个”,被替换的永远是它。
代价对比:n = 10⁶、k = 10 时,log k ≈ 3.3,总比较约 3.3×10⁶ 次;排序要 2×10⁷ 次,差一个量级。k 接近 n 时两者差不多,直接 sort 更省事。
模型:n × m 的网格,每格有权 0 或 1。从左上角走到右下角,每步上下左右,进入一格的代价等于该格的权值,求最小总代价。
普通 BFS 不行。BFS 的正确性依赖“先发现的更近”,而这里边权有 0:同一个点可能被一条长路先发现(层数多),却被一条短路后到达(总代价小)。层数和距离脱钩了。
Dijkstra 行,但贵。nm 个点各有一个 dist,每次取最小要堆,复杂度 O(nm log(nm))。可是这里边权只有 0 和 1,dist 的最小值只有两种可能:当前值 d,或 d + 1。两种值只需要两个“桶”,而 deque 恰好能当两个桶用 —— 这就是 0-1 BFS。
规矩只有两条:从队头取点 u 扩展;若走到 v 的边权是 0,把 v push_front,若是 1,把 v push_back。
为什么队头一定是最小的。设当前 dq 里的距离序列非递减,且只含 d 与 d + 1 两种值。从队头取出 dist = d 的点 u:边权 0 的新点距离也是 d,插到队头不破坏非递减;边权 1 的新点距离是 d + 1,插到队尾也不破坏非递减。两条规矩都是为了保住这个不变式。于是每次取出的都是当前最小,和 Dijkstra 一样,只是“堆”退化成了两个桶。
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
const int INF = 0x3f3f3f3f;
int n, m;
int w[N][N], d[N][N];
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j) {
cin >> w[i][j];
d[i][j] = INF;
}
deque<int> dq; // 存编号 x*m+y,省一个 pair 的拷贝
d[0][0] = 0;
dq.push_front(0); // 源点距离最小,从队头进
while (!dq.empty()) {
int id = dq.front();
dq.pop_front();
int x = id / m, y = id % m; // 注意除的是列数 m,不是行数 n
for (int k = 0; k < 4; ++k) {
int nx = x + dx[k], ny = y + dy[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
int nd = d[x][y] + w[nx][ny];
if (nd >= d[nx][ny]) continue; // 不比已知更优,就不必再进队
d[nx][ny] = nd;
if (w[nx][ny] == 0) dq.push_front(nx * m + ny); // 0 边:插队头
else dq.push_back(nx * m + ny); // 1 边:排队尾
}
}
cout << d[n - 1][m - 1] << '\n';
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
deque<int> dq; dq.push_front(0); | 源点必须从队头进:它的距离 0 是全图最小的。用 push_back 也碰巧对,但会破坏“队头最小”的阅读直觉。 |
int x = id / m, y = id % m; | 编号是按行铺开的,所以模和除都用列数 m。用成 n 时只有方阵能过,非方阵立刻 WA —— 这类错极难自查。 |
if (nd >= d[nx][ny]) continue; | 这里没有 visited,而且不能有:同一个点可能入队两次 —— 先被一条 1 边松弛到 d + 1,再被一条 0 边改小到 d(新的一份插到队头,会先出队)。所以判断条件必须是 nd < d[nx][ny]。这一行也是复杂度的保证:每个点的 dist 最多被改小一次,即最多入队两次,总代价 O(V + E)。 |
w[nx][ny] == 0 ? push_front : push_back | 整个算法只有这一行在区分两种边。判断的对象是进入 v 的这条边的权值,不是 u 的权值,也不是 dist 的奇偶。写错就退化成 SPFA。 |
cout << d[n-1][m-1] | 终点代价含终点自己的权值(因为代价计在“进入”上)。若题面把代价定义在出发格,就要改成 d[n-1][m-1] + w[0][0] —— 读题时确认一次。 |
空 deque 上调用 front() / back() / pop_front():未定义行为,几乎必然 RE。0-1 BFS 里忘记 push 源点,或者把 while 写成 do-while,第一轮就访问空容器。
0-1 BFS 把 0 边的 push_front 写成 push_back:距离序列不再非递减,同一个点被反复松弛,答案仍然正确但会超时(TLE)。这种错在随机小数据上完全看不出来,必须用长链数据压。
0-1 BFS 用 int x = id / n:只有 n == m 时才对,矩形网格一测就 WA。牢记“行优先编号,除和模都用列数”。
priority_queue 自定义比较器方向写反(想要小根堆却写 return val < o.val;):只有一个函数时看不出问题,n 个函数立刻 WA。方向口诀:cmp 写“谁在前”,operator< 写“谁在后”。
Top-K 用默认大根堆还写 if (x > q.top()):堆顶是候选里最大的,新数几乎永远比不过它,结果输出的是前 k 个输入数 → WA(数据 5 1 9 3 7 2、k = 3 时会输出 1 5 9,正确答案 9 7 5)。
用 int 存函数值或前缀乘积:P2085 里 a·x² 可到 10¹¹,溢出成负数后堆顶跑到最小端,输出一整片负数 → WA。凡是“平方”“乘积”“累加”,先估一遍量级。
想修改已经 push 进堆的元素(比如把某个点的 dist 改小后直接改结构体字段):堆内部顺序不会重排,之后 top() 拿到的是错的值 → WA。正确做法是 push 一份新值,取出时判旧值并跳过。
关掉页面,把 P2085 的代码从零敲一遍并提交。
自测数据:2 4 / 1 1 1 / 1 1 2,两个函数的值是 3,7,13,21 和 4,8,14,22,答案应为 3 4 7 8。手算一遍再对拍。
改一改①:把 operator< 里的 val > o.val 改成 val < o.val(变成大根堆),同一组数据,输出从 3 4 7 8 变成 4 8 14 22 —— 四个数全取自第二个函数,第一个函数一次都没出队。只改一个字符,却是本节最高频的错。
改一改②:Top-K 代码把 greater<int> 删掉变成大根堆,跑 n = 6、k = 3、数据 5 1 9 3 7 2。正确答案是 9 7 5,改完得到 1 5 9 —— 亲手确认一次“堆顶是谁决定了一切”。
堆是一棵完全二叉树:n 个结点时高度 h = ⌊log₂ n⌋ + 1。一次 push 或 pop 沿着一条从根到叶的路径上下移动,最多比较 h 次,所以是 O(log n)。堆的所有复杂度都来自这一条路径。
| n | ⌊log₂ n⌋ | 堆高 h | n·log₂n 次比较 | n·log₂k(k = 10)次比较 |
|---|---|---|---|---|
| 10³ | 9 | 10 | 10⁴ | 3.3×10³ |
| 10⁵ | 16 | 17 | 1.7×10⁶ | 3.3×10⁵ |
| 10⁶ | 19 | 20 | 2.0×10⁷ | 3.3×10⁶ |
| 10⁸ | 26 | 27 | 2.7×10⁹ | 3.3×10⁸ |
速记:2¹⁰ = 1024 ≈ 10³,所以每 10 次二分大约跨越 3 个十进制位。log₂(10⁶) = 19.9,log₂(10⁹) = 29.9 —— 十亿也就 30 层。竞赛里 n ≤ 10⁶ 时,log n 的常数按 20 估,堆操作 10⁶ 次约 2×10⁷ 次比较,在 1 秒内很宽裕。
Top-K 的收益就来自把 log n 换成 log k:n = 10⁶、k = 10 时是 3.3×10⁶ 对 2.0×10⁷,差 6 倍;k = 10³ 时是 10⁷ 对 2×10⁷,只差 2 倍 —— k 越大越不值得用堆,这是选择排序还是堆的量化依据。
0-1 BFS 省掉的则是整个 log。Dijkstra 每次要取 dist 最小的点,所以需要堆;而边权只有 0 和 1 时,队列中的 dist 只可能是 d 与 d + 1 两个值,“取最小”退化成“看队头属于哪个桶”,O(log V) 变成 O(1),总复杂度从 O(E log V) 降到 O(V + E)。
这类“值域很小的最短路”有统一名字:桶排序式的最短路(Dial 算法)。边权是 0…W 时开 W + 1 个桶循环使用,复杂度 O(E + W·V);W = 1 就是本节的双端队列版本。
有两个长度都是 N 的序列 A、B。在 A 中取一个数、在 B 中取一个数相加,一共能得到 N² 个和。求这些和里最小的 N 个,按升序输出。
输入格式:第一行一个整数 N;第二行 N 个整数 A_i;第三行 N 个整数 B_i。
输出格式:一行 N 个整数,为最小的 N 个和,从小到大排列,用空格隔开。
数据范围:1 ≤ N ≤ 10⁵,1 ≤ A_i, B_i ≤ 10⁹。
N² 个和存都存不下(N = 10⁵ 时是 10¹⁰ 个),必须利用“有序”。先把 A、B 都升序排序,再观察结构。
固定 A_i,序列 A_i + B_1, A_i + B_2, …, A_i + B_N 是递增的(因为 B 已排序)。于是 N² 个和 = N 个递增序列的并,问题变成多路归并。
要求全局第 1 小,只需在这 N 个序列的首元素里取最小 —— 用一个堆维护这 N 个候选,取出之后只把被取走的那条序列往前推一格。
排序。把 A、B 都升序排。设排好后 A_1 ≤ … ≤ A_N,B_1 ≤ … ≤ B_N。
转化。对每个 i,S_i = (A_i + B_1, A_i + B_2, …, A_i + B_N) 是一条非递减序列。N² 个和正是这 N 条序列的全部元素。求最小的 N 个 = N 路归并取前 N 项。
候选。每个序列只把首元素 A_i + B_1 放进堆:堆结点存 (和, i, j),i 是行号,j 是 B 的下标。堆里初始有 N 个元素。
循环 N 次。取堆顶输出;若 j < N,把同一行的下一个 (A_i + B_{j+1}, i, j+1) 放回堆。一出一进,堆大小恒为 N。
正确性。堆里始终是每条序列“尚未输出部分”的首元素,而序列内部非递减,所以全局最小必定是某个序列的首元素 —— 它一定在堆里。取出后只推进该序列,其余序列的首元素不变,不变式保持。
为什么只推进一格不会漏。S_i 的第 j+1 项只有在第 j 项被取走之后,才可能成为全局最小(它 ≥ 第 j 项,而第 j 项当时就在堆里且更小)。
复杂度。排序 O(N log N),归并 N 次、每次 O(log N),总共 O(N log N),N = 10⁵ 轻松过。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n;
long long a[N], b[N];
struct Node {
long long val; // 和:A_i + B_j
int i, j; // 来自 A 的第 i 个、B 的第 j 个(1-indexed)
// 小根堆:val 大的排在后面(与 sort 的 cmp 反着写)
bool operator<(const Node &o) const { return val > o.val; }
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) cin >> b[i];
sort(a + 1, a + n + 1);
sort(b + 1, b + n + 1); // 两个都要排,只排一个序列就不单调了
priority_queue<Node> pq;
for (int i = 1; i <= n; ++i)
pq.push({a[i] + b[1], i, 1}); // 每条序列只放首元素,共 N 个候选
for (int t = 1; t <= n; ++t) {
Node cur = pq.top();
pq.pop(); // pop 不返回值,先存堆顶
cout << cur.val << (t == n ? '\n' : ' ');
if (cur.j < n) { // 该行还有下一个,放回去
int nj = cur.j + 1;
pq.push({a[cur.i] + b[nj], cur.i, nj});
}
}
return 0;
}| 片段 | 作用 |
|---|---|
sort(a + 1, a + n + 1); sort(b + 1, b + n + 1); | 两个序列都必须升序。只排 A 的话,固定 A_i 得到的 A_i + B_j 就不再单调,“堆里放首元素”的前提直接崩了。 |
pq.push({a[i] + b[1], i, 1}); | 初始候选是每条序列的最小项,共 N 个。不要把所有 N² 个和都塞进去 —— 内存和时间都不允许。 |
bool operator<(const Node &o) const { return val > o.val; } | 取最小 → 小根堆 → operator< 里写 val > o.val。方向反了就是每次输出当前最大,答案整片错。 |
cout << cur.val << (t == n ? '\n' : ' '); | 行末不能多空格,用一个条件表达式控制分隔符。写成先输出再判空也等价,但别两边都加空格。 |
if (cur.j < n) | 边界:j = N 时该行已经取完,不能再 push。不加这个判断会访问 b[N+1],读到 0 或脏数据 → WA。 |
pq.push({a[cur.i] + b[nj], cur.i, nj}); | 只推进 cur 所在的这一行,i 必须沿用 cur.i。写成别的行号会让某条序列断层、另一条重复,答案错。 |
long long val | A_i、B_i 到 10⁹,两个相加到 2×10⁹,已经贴着 int 上限;必须 long long。 |