题单 · 习题解析

洛谷 P2085 最小函数值

★★ 关键的一步是**先把题面读成「n 个已经排好序的序列」**(A ≥ 1、B ≥ 0、x ≥ 1 ⇒ 每个函数递增),然后只留一句话:**只有每个序列的头部有资格当下一个最小值** ⇒ 堆里永远只放 n 个候选;⚠ 暴力先撞的是**内存**:n × m = 10⁸ 个值 = **763 MB**(题面 125 MB);★★ 而**「逐个函数归并」那一版就已经能 AC**(顶格 0.14 秒 / 时限 1 秒),堆版 **867 微秒**快 161 倍 ⇒ 选堆的理由是它把结构说清楚了;★★★ 这一页最值钱的是**命门要称到点子上**:题面写 `1 ≤ Aᵢ`,可把 A 放宽到**可以是 0**,「F 单调不降」**照样成立、0/300 轮出问题** ⇒ 那句话本身是噪声,算法依赖的是它**推出来的性质**;A 取负才塌(229 触发 / 110 被抓);⚠⚠ 而档 3 里**堆版和归并版逐字节相同(300/300)—— 两版共享同一个前提,所以一起错**,只有暴力那条路验得出来 ⇒ **参照物和解法共享假设,这个对拍验的是零**;⚠ `x ∈ ℕ*` 那个星号不含 0(样例第一个数就变);★ int 余量 2.15 倍

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

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

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

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

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

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

题目描述

n 个函数,分别为 F₁, F₂, …, F_n。定义 Fᵢ(x) = Aᵢx² + Bᵢx + Cᵢx ∈ ℕ*)。 给定这些 AᵢBᵢCᵢ,请求出所有函数的所有函数值中最小的 m 个(如有重复的要输出多个)。

输入格式

第一行输入两个正整数 nm

以下 n 行每行三个正整数,其中第 i 行的三个数分别为 AᵢBᵢCᵢ

输出格式

输出将这 n 个函数所有可以生成的函数值排序后的前 m 个元素。 这 m 个数应该输出到一行,用空格隔开。

数据规模与约定

对于全部的测试点,保证 1 ≤ n, m ≤ 100001 ≤ Aᵢ ≤ 100 ≤ Bᵢ ≤ 1000 ≤ 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 个函数值。 ⚠ x1 开始(ℕ* 不含 0)—— 第 ④ 步会说这个星号值多少钱: x = 0 时三个函数分别是 3 / 5 / 1,第一个数就变了。

1第一反应:把所有函数值算出来,排个序 —— 而它先撞的是内存

p2085Brute.cpp⚠ 答案永远对 —— 顶格要 763 MB,而题面只给 125 MB
// 第一反应:把所有函数值都算出来,排个序,取前 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 「所有函数值」到底有多少个 —— 一句乘法

每个函数只有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

p2085Merge.cpp★ 逐个函数归并 —— 顶格 0.14 秒 / 时限 1 秒,这一版就能过
// 顺着上一版往下改:**不用把它们全存下来** —— 手上只留「到目前为止最小的 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

手上只留「到目前为止最小的 m 个」,每读进一个函数就和它的前 m 个值归并一遍。 空间从 763 MB 掉到 O(m),时间是 O(nm) = 10⁸ 次简单操作。

★★ 顶格实测 0.14 秒 —— 它就已经能 AC 了。 这本书反复说的那件事:不写到最优也能过,先把能过的那一版写出来。

3★ 再快一步:堆 —— 每个序列只有「头部」有资格

p2085.cpp★ 多路归并(顶格 867 微秒,比上一版快 161 倍)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 关键一步:先把题面读成「n 个排好序的序列」

题面保证 Aᵢ ≥ 1Bᵢ ≥ 0x ≥ 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 000int 上限 2 147 483 647 ⇒ 余量 2.15 倍,int 够用。 (对照隔壁 P3378 的余量 0P18017.4%。)

4⚠ 两条和算法无关的:那个星号,和「输出到一行」

p2085Zero.cpp✗ x 从 0 开始 —— ℕ* 不含 0(样例第一个数就变了:1 vs 9)
// ✗ 错法一: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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p2085Line.cpp✗ 一行一个数地输出 —— 题面写的是「输出到一行」

两个都是四个档 300 / 300 / 300 / 300,一整类输入全错 ⇒ 样例一测就死。 ⚠ 而后一个提醒一件事:对拍比对不要 strip —— 一 strip,这个 bug 就被洗掉了。

5★★★ 「命门」要称到点子上 —— 题面写的那句话,不是算法真正依赖的那句

★★★ 把 A ≥ 1 放宽两次,看它什么时候塌

第 12 章那个判据造一档违反它的数据,看有没有任何一版的行为变了。 这里放宽两次:

300 轮 某个函数不再单调不降 堆版 ≡ 归并版 ⇒ 它们和暴力不同
档 2:A 放宽到可以是 0(题面写 1 ≤ Aᵢ 0 / 300 300 / 300 0 / 300
档 3:A 放宽到可以是负数 229 / 300 300 / 300 110 / 300

★★★ 第一行说明「Aᵢ ≥ 1」这句话本身是噪声 —— A = 0F 退化成一次函数 (甚至常数),仍然单调不降,算法一点事都没有。 ⇒ 算法真正依赖的不是题面那句话,是它推出来的那个性质Fx ≥ 1 上不降。

★★ 称约束的重量时,要称那个性质,不是那句话。 题面的约束和算法的前提之间常常隔着一步推导 —— 「A ≥ 1」是充分条件,而且是松的A ≥ 0 就够)。

⚠⚠ 而第三列那个「300 / 300」值得单独说:档 3 里堆版和逐个归并版逐字节相同 —— 它们错得一模一样,因为两版共享同一个前提(每个函数递增)。 ⇒ 只有暴力那条路不依赖它 ⇒ 这正是 「验算要走一条和算法完全无关的路」的价值: 参照物和解法共享一个假设,那这个对拍验的是零。 ★ 而 229 触发 / 110 被抓(2.1 倍),又一次写不成「≡」

6度量程序和生成器

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

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%)