题单 · 习题解析

洛谷 P1631 序列合并

★★ [P2085](/sol/p2085/) 的双序列版:把 N² 个和摆成一张表,**B 单调不降 ⇒ 每一行本来就有序** ⇒ 还是那 N 个「头部」;⇒ **「多路归并」不是模板,是一个问法:这道题里那 N 个「已经有序的东西」是谁**;★ 题面「50% 的数据 N ≤ 10³」就是出题人递过来的暴力档(0.04 秒稳拿 50 分;顶格 10¹⁰ 个和 = **74.5 GB**);★★★ 而另一种写法(从左上角往右下爬)自带一个坑:**同一个格子会被两条路推进堆**,必须去重 —— 而它的抓获率有两处反直觉:⚠ **值域压小反而更难抓**(1~20 抓 160、1~3 只抓 54,因为重复的和越多、多爬一格越看不出来)⇒ **「小数据更容易抓 bug」也有主语**;★ 「B 全相等」那一档是**能证的精确的 0**(答案本身就是同一个数重复 N 次,实测 300/300);⇒ 正解那种「按行推进」的写法**从结构上就没有这个坑**;⚠ 命门是「单调不降」(违反后暴力和正解差 288/300);★ int 余量 **7.4%**,和 [P1801](/sol/p1801/) 一模一样

原题:洛谷 P1631出自 第 37 章 堆与 priority_queue 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1631,日期见页头。两边不一致时信原站。

题目描述

有两个长度为 N单调不降序列 A, B,在 A, B 中各取一个数相加可以得到 个和, 求这 个和中最小的 N 个。

输入格式

第一行一个正整数 N

第二行 N 个整数 A₁…A_N

第三行 N 个整数 B₁…B_N

输出格式

一行 N 个整数,从小到大表示这 N 个最小的和。

数据规模与约定

对于 50% 的数据,N ≤ 10³

对于 100% 的数据,1 ≤ N ≤ 10⁵1 ≤ Aᵢ, Bᵢ ≤ 10⁹

时限 1 秒,内存 128 MB。

输入输出样例

输入

3
2 6 6
1 4 8

输出

3 6 7

A = [2,6,6]B = [1,4,8] ⇒ 九个和里最小的三个是 3(2+1)、6(2+4)、7(6+1)。 ⚠ 注意 67 各只出现一次 —— 而重复的和是要输出多次的(第 ④ 步)。

1第一反应:把 N² 个和全造出来排序 —— 而题面给它留了 50 分

p1631Brute.cpp★ N ≤ 10³ 那一档 0.04 秒,稳拿 50 分;顶格要 74.5 GB
// 第一反应:把 N² 个和全造出来,排序,取前 N 个
//
// ★ **它不是零分**:题面写着「对于 50% 的数据,N ≤ 10³」——
// 那一档只有 10⁶ 个和,本机 0.07 秒,**稳拿 50 分**。
// ⇒ 题面那一行分档,就是出题人递过来的暴力档([第 24 章那条](/sol/p1776/))。
//
// ⚠ 顶格 N = 10⁵ 就完全不行了:10¹⁰ 个和,光存下来就要 **80 GB**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<long long> a(n), b(n);
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) cin >> b[i];
vector<long long> all;
all.reserve((size_t)n * n); // ⚠ 顶格是 10^10 个
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) all.push_back(a[i] + b[j]);
sort(all.begin(), all.end());
string out;
for (int k = 0; k < n; k++) { if (k) out += ' '; out += to_string(all[k]); }
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 题面那一行分档,就是出题人递过来的暴力档
「对于 50% 的数据,N ≤ 10³ ⇒ 只有 10⁶ 个和,本机 0.04 秒 / 11.7 MB ⇒ ★ 稳拿 50 分
顶格 N = 10⁵ 10¹⁰ 个和,光存下来 74.5 GB(题面给 128 MB)

读数据范围的时候顺手把它乘出来第 24 章那条)—— 这道题的暴力档写得明明白白,先把 50 分拿到手,再想剩下那一半。

2★ 关键一步:把 N² 个和摆成一张表,你会发现每一行本来就是有序的

       B₁   B₂   B₃  ...
   A₁   ·    ·    ·          第 i 行是 A[i] + B[1..N]
   A₂   ·    ·    ·          ★ B 单调不降 ⇒ 每一行本身就是一个有序序列
   A₃   ·    ·    ·          ⇒ 只有每行「当前的头部」有资格当下一个最小值
p1631.cpp★ 这一版就能 AC(顶格 12 342 微秒 / 时限 100 万微秒)
// P1631 序列合并 —— ★ 这一版就能 AC:**多路归并**(和隔壁 [P2085] 是同一招)
//
// ============ 先把 N² 个和摆成一张表 ============
// B₁ B₂ B₃ ...
// A₁ · · ·
// A₂ · · · 第 i 行是 A[i] + B[1..N]
// A₃ · · · ★ 而 B 单调不降 ⇒ **每一行本身就是有序的**
//
// ⇒ 又是「N 个已经排好序的序列,求前 N 小」——
// 只有每行**当前的头部**有资格当下一个最小值。
// ⇒ 堆里永远只放 N 个候选(每行一个),取 N 次。O(N log N)。
//
// ⚠ 题面「两个长度为 N 的**单调不降**序列」是命门:
// 一旦 A 或 B 不再有序,「每行的最小值是第一个」就不成立了(第 ⑤ 步量过)。
// ⚠ 1 ≤ Aᵢ, Bᵢ ≤ 10⁹ ⇒ 和最大 2×10⁹ < 2 147 483 647 ⇒ int **够用,余量 7.4%**
// (和 [P1801](/sol/p1801/) 一模一样的余量)。
#include <bits/stdc++.h>
using namespace std;
struct Node {
long long v; // A[i] + B[j]
int i, j;
bool operator>(const Node& o) const { return v > o.v; }
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<long long> a(n), b(n);
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) cin >> b[i];
priority_queue<Node, vector<Node>, greater<Node>> q;
for (int i = 0; i < n; i++) q.push({a[i] + b[0], i, 0}); // ★ 每行的头部
string out;
for (int k = 0; k < n; k++) {
Node t = q.top();
q.pop();
if (k) out += ' ';
out += to_string(t.v);
int i = t.i, j = t.j + 1;
if (j < n) q.push({a[i] + b[j], i, j}); // 那一行往右挪一格
}
out += '\n';
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 这就是上一题的双序列版 —— 同一招,换了个壳
P2085 最小函数值 ★ 这道题
n 个有序序列是谁 i 个函数在 x = 1,2,3,… 上的取值 ★ 表里的第 i A[i] + B[1..N]
它凭什么有序 A ≥ 1、B ≥ 0F 递增 B 单调不降(题面保证)
堆里放什么 n 个候选,各带「来自哪个函数、下一个 x」 N 个候选,各带「哪一行、下一列」

「多路归并」不是一个模板,是一个问法这道题里那 N 个「已经有序的东西」是谁? 答出来,剩下的都一样。

★ 顺手把 int 那笔账乘一遍:Aᵢ + Bⱼ ≤ 2 × 10⁹int 上限 2 147 483 647 ⇒ 够用,余量 7.4% —— 和 P1801 一模一样的余量 (对照 P33780P20852.15 倍)。

3⚠ 另一种写法:从左上角往右下角爬 —— 而它自带一个坑

p1631Dup.cpp✗ 从 (1,1) 扩展 (i+1,j) 和 (i,j+1),忘了去重(官方样例放过了它)
★★★ 对拍 300 轮 —— 这个 bug 的抓获率有两处反直觉
300 轮 ★ 前提:同一个格子被两条路推进过 ✗ 忘了去重 ✗ 忘了往右挪
档 0:顺手(N 410,值域 120) 294 160 295
⚠ 档 1:值域压到 1~3 297 54 285
★ 档 2:B 全部相等 44 0 300
⚠ 档 3:故意违反题面(两条序列都不排序) 152 295 282

① 值域压小反而更难抓(160 → 54) —— 和这本书里常见的方向正好相反 (P1908 那条是「值域越小越容易现形」)。 道理也说得清:重复的和越多,「多爬一个格子」爬到的越可能还是同一个值。 ⇒ ★★ 「小数据更容易抓 bug」也有主语 —— 得看那个 bug 是被什么区分出来的。

② 档 2 那个 0 是能证的B 全等于 c ⇒ 每个和 A[i] + c 都出现 N 次 ⇒ 前 N全是 A[1] + c(实测 300 / 300 轮答案的 N 个数完全相同) ⇒ 多爬的那些格子,爬到的还是同一个值。

正解那种写法(按行推进,一行只往右挪一格)从结构上就没有这个坑 —— 「关键的一步不一定是算法,也可以是换一种写法让一整类坑消失」的又一次。

p1631Row.cpp✗ 取出之后忘了把那一行往右挪(官方样例当场死:3 7 7)

4⚠ 两条题面上的细节

★ 「单调不降」是命门;而「重复的和」是要输出多次的

AB 单调不降 —— 造一档违反它(两条序列都不排序): 300 / 300 轮真的不单调,而暴力和正解不一致 288 / 300。 ⇒ 一旦无序,「每行的最小值是第一个」就不成立,整个多路归并塌掉。这是命门。

② 重复的和要输出多次 —— 题面没有一个字说「去重」, 而档 2(B 全相等)的正确答案就是同一个数重复 N。 ⚠ 顺手拿 set 去重的话,这一档一交就死。 (P3378 上同一个坑的另一个版本:那道题是题面明写「有多个最小只删 1 个」。)

5度量程序和生成器

p1631Count.cpp度量程序(本页的数字都出自它)
p1631Gen.cpp(五个档位)数据生成器

6一页纸

★ 暴力值多少分 题面 50% 档 N ≤ 10³0.04 秒,稳拿 50 分;顶格要 74.5 GB
★★ 关键一步 个和摆成表:B 单调不降 ⇒ 每一行本来就有序 ⇒ 多路归并
★ 和上一题的关系 P2085 的那 n 个有序序列是函数,这里是 —— 同一个问法
⚠ 另一种写法的坑 「从左上角往右下爬」会把同一个格子推进两次 ⇒ 必须去重;正解那种写法没有这个坑
⚠⚠ 反直觉之一 值域压小反而更难抓(160 → 54)—— 重复的和越多,多爬一格越看不出来
★ 反直觉之二 B 全相等那一档是能证的 0:答案本身就是同一个数重复 N
⚠ 命门 「单调不降」违反之后暴力和正解差 288 / 300
★ int 的账 A + B ≤ 2 × 10⁹ ⇒ 余量 7.4%(和 P1801 一模一样)