0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1631,日期见页头。两边不一致时信原站。
题目描述
有两个长度为 N 的单调不降序列 A, B,在 A, B 中各取一个数相加可以得到 N² 个和,
求这 N² 个和中最小的 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)。
⚠ 注意 6 和 7 各只出现一次 —— 而重复的和是要输出多次的(第 ④ 步)。
1第一反应:把 N² 个和全造出来排序 —— 而题面给它留了 50 分
// 第一反应:把 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;}点「运行 ▶」看结果
「对于 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 序列合并 —— ★ 这一版就能 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;}点「运行 ▶」看结果
| P2085 最小函数值 | ★ 这道题 | |
|---|---|---|
n 个有序序列是谁 |
第 i 个函数在 x = 1,2,3,… 上的取值 |
★ 表里的第 i 行(A[i] + B[1..N]) |
| 它凭什么有序 | A ≥ 1、B ≥ 0 ⇒ F 递增 |
★ B 单调不降(题面保证) |
| 堆里放什么 | n 个候选,各带「来自哪个函数、下一个 x」 |
N 个候选,各带「哪一行、下一列」 |
⇒ 「多路归并」不是一个模板,是一个问法: 这道题里那 N 个「已经有序的东西」是谁? 答出来,剩下的都一样。
★ 顺手把 int 那笔账乘一遍:Aᵢ + Bⱼ ≤ 2 × 10⁹,int 上限 2 147 483 647
⇒ 够用,余量 7.4% —— 和 P1801 一模一样的余量
(对照 P3378 的 0 和 P2085 的 2.15 倍)。
3⚠ 另一种写法:从左上角往右下角爬 —— 而它自带一个坑
| 300 轮 | ★ 前提:同一个格子被两条路推进过 | ✗ 忘了去重 | ✗ 忘了往右挪 |
|---|---|---|---|
档 0:顺手(N 4 |
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 个数完全相同)
⇒ 多爬的那些格子,爬到的还是同一个值。
⇒ 正解那种写法(按行推进,一行只往右挪一格)从结构上就没有这个坑 —— 「关键的一步不一定是算法,也可以是换一种写法让一整类坑消失」的又一次。
4⚠ 两条题面上的细节
① A、B 单调不降 —— 造一档违反它(两条序列都不排序):
300 / 300 轮真的不单调,而暴力和正解不一致 288 / 300。
⇒ 一旦无序,「每行的最小值是第一个」就不成立,整个多路归并塌掉。这是命门。
② 重复的和要输出多次 —— 题面没有一个字说「去重」,
而档 2(B 全相等)的正确答案就是同一个数重复 N 次。
⚠ 顺手拿 set 去重的话,这一档一交就死。
(P3378 上同一个坑的另一个版本:那道题是题面明写「有多个最小只删 1 个」。)
5度量程序和生成器
6一页纸
| ★ 暴力值多少分 | 题面 50% 档 N ≤ 10³ ⇒ 0.04 秒,稳拿 50 分;顶格要 74.5 GB |
| ★★ 关键一步 | 把 N² 个和摆成表:B 单调不降 ⇒ 每一行本来就有序 ⇒ 多路归并 |
| ★ 和上一题的关系 | P2085 的那 n 个有序序列是函数,这里是行 —— 同一个问法 |
| ⚠ 另一种写法的坑 | 「从左上角往右下爬」会把同一个格子推进两次 ⇒ 必须去重;正解那种写法没有这个坑 |
| ⚠⚠ 反直觉之一 | 值域压小反而更难抓(160 → 54)—— 重复的和越多,多爬一格越看不出来 |
| ★ 反直觉之二 | B 全相等那一档是能证的 0:答案本身就是同一个数重复 N 次 |
| ⚠ 命门 | 「单调不降」违反之后暴力和正解差 288 / 300 |
| ★ int 的账 | A + B ≤ 2 × 10⁹ ⇒ 余量 7.4%(和 P1801 一模一样) |