题单 · 习题解析

洛谷 P1853 [NWERC 2004] 投资的最大效益

★★★ 题面那句「a 是 1000 的倍数」是一张许可证:金额除以 1000,规模缩 1000 倍;而不除的那版答案分毫不差 —— 它先 MLE(172 MB)再 TLE,样例和对拍都抓不到

原题:洛谷 P1853出自 第 24 章 完全背包与多重背包 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

约翰先生获得了一大笔遗产,他暂时还用不上这一笔钱,他决定进行投资以获得更大的效益。 银行工作人员向他提供了多种债券,每一种债券都能在固定的投资后,提供稳定的年利息。 当然,每一种债券的投资额是不同的,一般来说,投资越大,收益也越大, 而且,每一年还可以根据资金总额的增加,更换收益更大的债券

题目描述

例如:有如下两种不同的债券:

  1. 投资额 4000,年利息 400;
  2. 投资额 3000,年利息 250。

初始时,有 10000 的总资产,可以投资两份债券 1,一年获得 800 的利息; 而投资一份债券 1 和两份债券 2,一年可获得 900 的利息,两年后,可获得 1800 的利息; 而所有的资产达到 11800,然后将卖掉一份债券 2,换购债券 1,年利息可达到 1050; 第三年后,总资产达到 12850,可以购买三份债券 1,年利息可达到 1200,第四年后,总资产可达到 14050。

现给定若干种债券、最初的总资产,帮助约翰先生计算,经过 n 年的投资,总资产的最大值。

输入格式

第一行为三个正整数 s, n, d,分别表示最初的总资产、年数和债券的种类。

接下来 d 行,每行表示一种债券,两个正整数 a, b 分别表示债券的投资额和年利息。

输出格式

仅一个整数,表示 n 年后的最大总资产。

说明/提示

对于 100% 的数据,1 ≤ s ≤ 10⁶2 ≤ n ≤ 401 ≤ d ≤ 101 ≤ a ≤ 10⁴a 是 1000 的倍数b 不超过 a 的 10%

输入输出样例

输入

10000 4 2
4000 400
3000 250

输出

14050

就是题面里那个手算的例子(10000 → 11800 → 12850 → 14050)。

★ 这一组样例两个错法都挡住了(01 背包 12600、容量不涨 13600)—— ⚠ 而它对本页的主角(「不除以 1000」)完全无能为力:那一版的答案分毫不差。

1★ 关键的一步(一):每一年就是一次完全背包

题面那句「每一年还可以根据资金总额的增加,更换收益更大的债券」是整道题的枢纽: 每年可以推倒重来,所以年与年之间只有一根线 —— 当年的资产。

    每一年:容量 = 当年资产,价值 = 年利息,每种债券可买任意多份   ← 完全背包
    年之间:资产 += 这一年的最大利息                              ← 一个 for 循环

⇒ 这就是题单说的「DP 套在循环里」,也是这道题在第 24 章题单里的位置。

p1853.cpp★ 这一版就能 AC
// P1853 [NWERC 2004] 投资的最大效益 —— ★ 这一版就能 AC
//
// 题意:初始资产 s,投资 n 年,d 种债券;第 i 种投资额 a[i]、年息 b[i],
// **每种可以买任意多份**(钱够就行)。每年可以重新配置一次。求 n 年后的最大总资产。
//
// ★ 关键的一步(一):**每一年就是一次完全背包** —— 容量是「当年的资产」,
// 价值是「年息」。而年与年之间只有一根线:`资产 += 这一年的最大利息`。
// ⇒ 这就是题单说的「**DP 套在循环里**」。
//
// ★ 关键的一步(二):**每年独立地把利息做到最大,就是全局最优。**
// 凭什么?「资产更多 ⇒ 今年能拿到的利息不会更少」(容量单调)——
// 所以今年多赚一分,明年的可行集只会更大。⇒ 不必在年之间做 DP。
// (解析页第 ② 步用一个**不假设这件事**的全局暴力把它验了一遍。)
//
// ★★★ 关键的一步(三):题面那句「**a 是 1000 的倍数**」是一张许可证 ——
// 把所有金额**除以 1000**,背包容量从 4.5 × 10^7 变成 4.5 × 10^4,规模缩 1000 倍。
// 不除的话是 1.8 × 10^10 次,必挂(解析页第 ③ 步量了)。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long s;
int n, d;
if (!(cin >> s >> n >> d)) return 0;
vector<int> a(d), b(d);
for (int i = 0; i < d; i++) { cin >> a[i] >> b[i]; a[i] /= 1000; } // ★ 除以 1000
// 资产上界:每年最多涨 10%(利息 ≤ 投资额的 10% ≤ 资产的 10%)
// ⇒ s ≤ 10^6 × 1.1^40 ≈ 4.53 × 10^7 ⇒ 除以 1000 之后 45260 格,开 50001 富余
const int MAXC = 50001;
vector<int> f(MAXC, 0);
for (int year = 0; year < n; year++) {
int cap = (int)(s / 1000); // 能动用的「千元」数
if (cap >= MAXC) cap = MAXC - 1;
fill(f.begin(), f.begin() + cap + 1, 0);
for (int i = 0; i < d; i++)
for (int j = a[i]; j <= cap; j++) // ★ 正序 —— 每种债券可以买任意多份
f[j] = max(f[j], f[j - a[i]] + b[i]);
s += f[cap]; // 本金留着,利息滚进来
}
cout << s << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★★ 关键的一步(二):那一步「每年取最大」是推理,不是 DP —— 所以要验

上面那份代码里藏着一个没有被 DP 覆盖的判断:

每年独立地把利息做到最大,就是全局最优。

凭什么?因为「资产更多 ⇒ 今年能拿到的利息不会更少」(容量单调)—— 所以今年多赚一分,明年的可行集只会更大,不可能出现「今年少赚点、明年赚更多」。

★★ 而「听起来对」不算数 —— 造一个不做这个假设的暴力去撞它

这一页的参照物故意不假设那件事:它每年枚举所有可行的债券组合(不只是利息最大那种), 然后对每一种可能的利息各往下走一年。

随机 300 组:正解 vs 不做该假设的全局暴力 不一致 0 组

⇒ 和第 14 章 P1332第 11 章 P1966 是同一个动作: 正解里那些「题面没证明、自己觉得显然」的跳跃,要用一条和它无关的路验一遍。

p1853Brute.cpp参照物:不假设「每年取最大」

3★★★ 关键的一步(三):「a 是 1000 的倍数」是一张许可证

数据范围最后那半句话很容易当成背景:a 是 1000 的倍数

它的意思是:所有金额都可以除以 1000。而背包的复杂度是 O(容量) —— 容量缩 1000 倍,整道题就缩 1000 倍。

★★★ 量一遍:顶格(s = 10⁶,n = 40,d = 10,利息拉满到 10%)
除以 1000(正解) 不除
DP 内层执行次数 4 422 660 4 422 660 000
倍数 1000 倍,一分不差
f 数组要多大 195 KB 172 MB
内存限制 128 MB 超了

它先撞的其实是内存那堵墙,然后才是时间。f 要开到「最终资产」那么多格,而最终资产是 4524 万。)

p1853Raw.cpp第二版:不除以 1000(答案对,跑不完)
★★★ 而这一版是「答案对但跑不完」—— 对拍一万轮也发现不了

它和正解逐组相等(小数据上一次不差),错的只有时间和空间。

第 20 章 P5019 那条结论的又一次现场: 官方样例和对拍筛的都是「答案错」,对「答案对但跑不完」完全无能为力。 这一次连样例都放过了它(样例的资产才 14050,除不除都是一眨眼)。

⇒ 判它死刑只能靠数次数 + 算内存 —— 而这两笔都是三十秒的算术。

4★★ 数组开多大:这也是算出来的,而且那个界很紧

f 要开到「n 年后的最大资产」那么多格。而资产每年最多涨多少?

    利息 ≤ 投资额的 10%,而投资额 ≤ 当年资产
    ⇒ 资产每年最多 ×1.1
    ⇒ 40 年后 ≤ 10⁶ × 1.1⁴⁰ ≈ 4.526 × 10⁷      (除以 1000 之后:45 260 格)
★ 算出来的界 vs 实测的顶格:差 0.03%

造一组「利息率正好 10%、钱能全投出去」的数据(a = 1000b = 100)跑满 40 年:

算出来的上界 10⁶ × 1.1⁴⁰ 45 259 255
实测最终资产 45 244 600
0.03%(整数除法的取整损失)
除以 1000 之后要多少格 45 244 ⇒ 正解开 50001,富余

⇒ 这个界是的(不像 P1060 那句 < 10⁸ 松了 666 倍)—— ⇒ 「数组开多大」这个问题,题面已经把答案写在数据范围里了,只是要自己乘一遍。

5★★ 两个错法(官方样例都挡住了)

p1853ZeroOne.cpp错法一:内层倒序(每种债券只买一份)
p1853Fixed.cpp错法二:容量一直用初始资产
// P1853 错法二:容量一直用**初始资产**,不跟着涨
//
// 「每年跑一次完全背包」这句话记住了,可容量忘了更新 ——
// 于是第 2 年之后赚到的钱躺在账上,谁也没去投。
//
// ★ 说清楚它算了什么:它的答案**恒等于「n 年每年都赚初始资产能赚的那份利息」**,
// 也就是 `s + n × f(s)`。解析页第 ④ 步把这句话量成了 300 组逐组相等。
// ⇒ 它是**线性增长**,而正解是**指数增长** —— 年数越多差得越离谱。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long s;
int n, d;
if (!(cin >> s >> n >> d)) return 0;
vector<int> a(d), b(d);
for (int i = 0; i < d; i++) { cin >> a[i] >> b[i]; a[i] /= 1000; }
const int MAXC = 50001;
int cap = (int)(s / 1000); // ⚠ 只算了一次,之后再没更新
if (cap >= MAXC) cap = MAXC - 1;
vector<int> f(cap + 1, 0);
for (int i = 0; i < d; i++)
for (int j = a[i]; j <= cap; j++)
f[j] = max(f[j], f[j - a[i]] + b[i]);
for (int year = 0; year < n; year++) s += f[cap];
cout << s << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

先把第二个「算了什么」说清楚:

随机 300 组:它的答案 vs s + n × f(s) 300 / 300 逐组相等

⇒ 它算的是「每年都只赚初始资产能赚的那份利息」——线性增长,而正解是指数增长

★★ 说清楚之后,它的抓获率就是白送的推论:年数越多,越藏不住

只拧年数上限(各 300 轮):

n 的上限 3 10 40
「容量不涨」被抓 28 111 202
「01 背包」被抓 183 194 233

★ 第一行从 28 爬到 202 —— 因为线性和指数的差距要靠年数拉开。 ⚠ 而顺手写的生成器最容易停在 n 很小那一档(那里只抓 28 次)。 ⇒ 又一次「生成器的规模旋钮两头都要拧」。

6度量程序和生成器

p1853Count.cpp度量程序(本页所有数字都出自它)
p1853Gen.cpp数据生成器

7一页纸

关键的一步(一) 每一年就是一次完全背包,年之间只传一个数(当年资产)—— DP 套在循环里
关键的一步(二) 「每年独立取最大利息 = 全局最优」是推理(容量单调)⇒ ★ 用不做该假设的暴力验过,300 组 0 不一致
★★★ 关键的一步(三) 题面「a 是 1000 的倍数」⇒ 金额除以 1000,规模缩 1000 倍
不除的代价 内层 4 422 660 → 4 422 660 000 次;f 从 195 KB → 172 MB(限制 128)⇒ 先 MLE 再 TLE
⚠ 而它答案是对的 「答案对但跑不完」—— 样例和对拍都发现不了,只能数次数 + 算内存
数组开多大 每年最多 ×1.1 ⇒ 10⁶ × 1.1⁴⁰ ≈ 4.53 × 10⁷;实测顶格 45 244 600(差 0.03%,界很紧
错法一 内层倒序(每种债券只买一份)—— 样例挡住,300 轮被抓 183~233
错法二 容量不跟着资产涨 ⇒ 恒等于 s + n × f(s)(300/300);★ 抓获率随年数从 28 爬到 202