0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2085,日期见页头。两边不一致时信原站。
题目描述
有 n 个函数,分别为 F₁, F₂, …, F_n。定义 Fᵢ(x) = Aᵢx² + Bᵢx + Cᵢ(x ∈ ℕ*)。
给定这些 Aᵢ、Bᵢ 和 Cᵢ,请求出所有函数的所有函数值中最小的 m 个(如有重复的要输出多个)。
输入格式
第一行输入两个正整数 n 和 m。
以下 n 行每行三个正整数,其中第 i 行的三个数分别为 Aᵢ、Bᵢ 和 Cᵢ。
输出格式
输出将这 n 个函数所有可以生成的函数值排序后的前 m 个元素。
这 m 个数应该输出到一行,用空格隔开。
数据规模与约定
对于全部的测试点,保证 1 ≤ n, m ≤ 10000,1 ≤ Aᵢ ≤ 10,0 ≤ Bᵢ ≤ 100,0 ≤ Cᵢ ≤ 10⁴。
时限 1 秒,内存 125 MB。
输入输出样例
输入
3 10 4 5 3 3 4 5 1 7 1
输出
9 12 12 19 25 29 31 44 45 54
★ 三个函数 4x²+5x+3 / 3x²+4x+5 / x²+7x+1,要最小的 10 个函数值。
⚠ x 从 1 开始(ℕ* 不含 0)—— 第 ④ 步会说这个星号值多少钱:
x = 0 时三个函数分别是 3 / 5 / 1,第一个数就变了。
1第一反应:把所有函数值算出来,排个序 —— 而它先撞的是内存
// 第一反应:把所有函数值都算出来,排个序,取前 m 个//// 「所有」有多少个?—— 每个函数只有前 m 个值有可能进前 m 名(第 m+1 个已经比 m 个数都大了),// 所以是 **n × m = 10⁴ × 10⁴ = 10⁸ 个值**。//// ⚠⚠ 这一版**先撞的是内存,不是时间**:// 10⁸ 个 int = **400 MB**,而题面只给 **128 MB** ⇒ MLE。// (就算内存管够,排 10⁸ 个数也要十几秒。)// ⇒ 「答案对但跑不完」的又一次,而这次的墙是内存// ([第 24 章 P1853](/sol/p1853/)、[第 28 章 P2704](/sol/p2704/) 都撞过同一堵墙)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<long long> all; all.reserve((size_t)n * m); // ⚠ 顶格就是 10^8 个 for (int i = 0; i < n; i++) { long long a, b, c; cin >> a >> b >> c; for (int x = 1; x <= m; x++) all.push_back(a * x * x + b * x + c); } sort(all.begin(), all.end());
string out; for (int k = 0; k < m; k++) { if (k) out += ' '; out += to_string(all[k]); } out += '\n'; cout << out; return 0;}点「运行 ▶」看结果
每个函数只有前 m 个值有可能进前 m 名(第 m+1 个已经比 m 个数都大了)
⇒ 一共 n × m = 10⁴ × 10⁴ = 10⁸ 个值。
| 存下来要 | 10⁸ × 8 字节 = 800 000 000 字节 ≈ 763 MB |
| 题面给的内存 | 125 MB ⇒ ★ 差 6.1 倍,先 MLE |
| 就算内存管够,排 10⁸ 个数 | 本机 n = m = 3000 那一档就要 0.46 秒,外推到顶格约 5 秒 |
⇒ 「答案对但跑不完」的又一次,而这次挡在前面的是内存 (第 24 章 P1853 先 MLE 再 TLE、第 28 章 P2704 400 MB,同一堵墙)。
2★ 顺着往下改一步:不用把它们全存下来 —— 而这一版就已经能 AC
// 顺着上一版往下改:**不用把它们全存下来** —— 手上只留「到目前为止最小的 m 个」//// 每读进一个函数,就算出它的前 m 个值(本来就是递增的),// 和手上那 m 个**归并**一遍,只保留前 m 个。// ⇒ 空间从 400 MB 掉到 O(m),时间是 O(nm) = 10⁸ 次简单操作。//// ★★ **而这一版就已经能 AC 了**(顶格本机 0.28 秒 / 时限 1 秒,第 ③ 步那张表)——// 这本书反复强调的那件事:**不写到最优也能过,先把能过的那一版写出来。**// ⚠ 它和堆那版差 6.6 倍,而两版都在时限里 ⇒ 选堆的理由不是「非它不可」,是它**说得清**。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0;
vector<long long> best, cur, tmp; best.reserve(m); cur.reserve(m); tmp.reserve(m); for (int i = 0; i < n; i++) { long long a, b, c; cin >> a >> b >> c; cur.clear(); for (int x = 1; x <= m; x++) cur.push_back(a * x * x + b * x + c); tmp.clear(); size_t p = 0, q = 0; while (tmp.size() < (size_t)m && (p < best.size() || q < cur.size())) { if (q >= cur.size() || (p < best.size() && best[p] <= cur[q])) tmp.push_back(best[p++]); else tmp.push_back(cur[q++]); } best.swap(tmp); }
string out; for (int k = 0; k < m; k++) { if (k) out += ' '; out += to_string(best[k]); } out += '\n'; cout << out; return 0;}点「运行 ▶」看结果
手上只留「到目前为止最小的 m 个」,每读进一个函数就和它的前 m 个值归并一遍。
空间从 763 MB 掉到 O(m),时间是 O(nm) = 10⁸ 次简单操作。
★★ 顶格实测 0.14 秒 —— 它就已经能 AC 了。 这本书反复说的那件事:不写到最优也能过,先把能过的那一版写出来。
3★ 再快一步:堆 —— 每个序列只有「头部」有资格
// P2085 最小函数值 —— ★ 这一版就能 AC:**多路归并**(堆里永远只放 n 个候选)//// ============ 先把题面读成一句话 ============// F(x) = A x² + B x + C,而题面保证 **A ≥ 1、B ≥ 0、x ≥ 1**// ⇒ 每个函数在 x = 1, 2, 3, … 上的取值是**严格递增**的。// ⇒ 于是这道题变成:**n 个已经排好序的序列,求它们并起来的前 m 小**。// (⚠ 那句 A ≥ 1 是**命门**,不是背景 —— 第 ⑤ 步会把它称一称。)//// ============ 关键一步:不用把 n × m 个值造出来 ============// 每个序列**只有它的当前头部**有资格当下一个最小值(后面的都比它大)。// ⇒ 堆里永远只放 **n 个候选**(每个函数一个),取一次就把那个函数往后推一格。// ⇒ 时间 O((n + m) log n),空间 O(n) —— 而暴力是 n × m = 10⁸ 个值、400 MB。//// ★ 这就是[本章第 12 步](/ch/37-heap/)那句话的又一个现场:// **堆的本事是「只看该看的那几个」** —— 它需要的条件最少,来一个看一个。
#include <bits/stdc++.h>using namespace std;
struct Node { long long v; // 当前值 F_i(x) int i, x; // 来自第 i 个函数,当前的 x bool operator>(const Node& o) const { return v > o.v; }};
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<long long> A(n), B(n), C(n); priority_queue<Node, vector<Node>, greater<Node>> q; // ★ 小根堆 for (int i = 0; i < n; i++) { cin >> A[i] >> B[i] >> C[i]; long long v = A[i] * 1 * 1 + B[i] * 1 + C[i]; // x = 1,题面 x ∈ N* q.push({v, i, 1}); }
string out; for (int k = 0; k < m; k++) { Node t = q.top(); q.pop(); if (k) out += ' '; out += to_string(t.v); int i = t.i, x = t.x + 1; // 那个函数往后推一格 long long v = A[i] * x * x + B[i] * x + C[i]; q.push({v, i, x}); } out += '\n'; cout << out; return 0;}点「运行 ▶」看结果
题面保证 Aᵢ ≥ 1、Bᵢ ≥ 0、x ≥ 1 ⇒ 每个函数在 x = 1, 2, 3, … 上严格递增。
⇒ 这道题其实是:n 个已经有序的序列,求它们并起来的前 m 小。
而有序序列有一个性质:只有当前的头部才有资格当下一个最小值(后面的都比它大)。
⇒ 堆里永远只放 n 个候选,取一次就把那个函数往后推一格。
顶格 n = m = 10⁴(A 机 · 2026-08-31 · 独占) |
时间 | 空间 |
|---|---|---|
| 全部算出来排序 | 约 5 秒 | ★ 763 MB ⇒ MLE |
| 逐个函数归并 | 139 812 微秒(0.14 秒) | O(m) |
| ★ 堆 | ★ 867 微秒 | O(n) |
⇒ 堆比归并快 161 倍,而两版都在时限里 —— ★ 所以选堆的理由不是「非它不可」,是它把这道题的结构说清楚了: 「谁更快」和「它需要什么条件」是两个问题。
★ 顺手把 int 那笔账乘一遍:顶格函数值上界 10·m² + 100·m + 10⁴ = 1 001 010 000,
int 上限 2 147 483 647 ⇒ 余量 2.15 倍,int 够用。
(对照隔壁 P3378 的余量 0、P1801 的 7.4%。)
4⚠ 两条和算法无关的:那个星号,和「输出到一行」
// ✗ 错法一:x 从 0 开始//// 题面写的是 `F_i(x) = A_i x² + B_i x + C_i (x ∈ N*)` ——// **`N*` 是正整数集,不含 0**(`N` 才含)。这一个星号最容易被略过。//// ⚠ 而它的后果非常大:x = 0 时 F(0) = C,而题面允许 `C ≥ 0`// ⇒ 凭空多出 n 个很小的值,前 m 名整个被它们挤掉。// ★ 官方样例第一个数就变了(正解 9,它打 1),一测就死。
#include <bits/stdc++.h>using namespace std;
struct Node { long long v; int i, x; bool operator>(const Node& o) const { return v > o.v; }};
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<long long> A(n), B(n), C(n); priority_queue<Node, vector<Node>, greater<Node>> q; for (int i = 0; i < n; i++) { cin >> A[i] >> B[i] >> C[i]; q.push({C[i], i, 0}); // ⚠ x 从 0 起 } string out; for (int k = 0; k < m; k++) { Node t = q.top(); q.pop(); if (k) out += ' '; out += to_string(t.v); int i = t.i, x = t.x + 1; q.push({A[i] * x * x + B[i] * x + C[i], i, x}); } out += '\n'; cout << out; return 0;}点「运行 ▶」看结果
两个都是四个档 300 / 300 / 300 / 300,一整类输入全错 ⇒ 样例一测就死。
⚠ 而后一个提醒一件事:对拍比对不要 strip —— 一 strip,这个 bug 就被洗掉了。
5★★★ 「命门」要称到点子上 —— 题面写的那句话,不是算法真正依赖的那句
用第 12 章那个判据:造一档违反它的数据,看有没有任何一版的行为变了。 这里放宽两次:
| 300 轮 | 某个函数不再单调不降 | 堆版 ≡ 归并版 | ⇒ 它们和暴力不同 |
|---|---|---|---|
档 2:A 放宽到可以是 0(题面写 1 ≤ Aᵢ) |
★ 0 / 300 | 300 / 300 | ★ 0 / 300 |
档 3:A 放宽到可以是负数 |
229 / 300 | 300 / 300 | ★ 110 / 300 |
★★★ 第一行说明「Aᵢ ≥ 1」这句话本身是噪声 —— A = 0 时 F 退化成一次函数
(甚至常数),仍然单调不降,算法一点事都没有。
⇒ 算法真正依赖的不是题面那句话,是它推出来的那个性质:F 在 x ≥ 1 上不降。
★★ 称约束的重量时,要称那个性质,不是那句话。 题面的约束和算法的前提之间常常隔着一步推导 —— 「
A ≥ 1」是充分条件,而且是松的(A ≥ 0就够)。
⚠⚠ 而第三列那个「300 / 300」值得单独说:档 3 里堆版和逐个归并版逐字节相同 —— 它们错得一模一样,因为两版共享同一个前提(每个函数递增)。 ⇒ 只有暴力那条路不依赖它 ⇒ 这正是 「验算要走一条和算法完全无关的路」的价值: 参照物和解法共享一个假设,那这个对拍验的是零。 ★ 而 229 触发 / 110 被抓(2.1 倍),又一次写不成「≡」。
6度量程序和生成器
7一页纸
| ★ 先把题面读成一句话 | A ≥ 1、B ≥ 0、x ≥ 1 ⇒ 每个函数递增 ⇒ n 个有序序列求前 m 小 |
| ★ 关键一步 | 只有每个序列的头部有资格 ⇒ 堆里永远只放 n 个候选 |
| ⚠ 暴力先撞哪堵墙 | n × m = 10⁸ 个值 = 763 MB(题面 125 MB)⇒ 先 MLE 再 TLE |
| ★★ 哪一版就能过 | 逐个归并 0.14 秒就能 AC;堆 867 微秒,快 161 倍 ⇒ 选它是为了说得清 |
| ⚠ 那个星号 | x ∈ ℕ* 不含 0(样例第一个数就变);输出一行,空格隔开 |
| ★★★ 命门要称到点子上 | A ≥ 1 是噪声(放宽到 0 一点事没有),真正的前提是「F 单调不降」 |
| ⚠⚠ 参照物的陷阱 | 堆版和归并版共享同一个前提 ⇒ 一起错 300/300,只有暴力那条路验得出来 |
| ★ int 的账 | 顶格函数值 1 001 010 000 ⇒ 余量 2.15 倍(P3378 是 0、P1801 是 7.4%) |