0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1853,日期见页头。两边不一致时信原站。
题目背景
约翰先生获得了一大笔遗产,他暂时还用不上这一笔钱,他决定进行投资以获得更大的效益。 银行工作人员向他提供了多种债券,每一种债券都能在固定的投资后,提供稳定的年利息。 当然,每一种债券的投资额是不同的,一般来说,投资越大,收益也越大, 而且,每一年还可以根据资金总额的增加,更换收益更大的债券。
题目描述
例如:有如下两种不同的债券:
- 投资额 4000,年利息 400;
- 投资额 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 ≤ 40,1 ≤ d ≤ 10,1 ≤ 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 [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;}点「运行 ▶」看结果
2★★ 关键的一步(二):那一步「每年取最大」是推理,不是 DP —— 所以要验
上面那份代码里藏着一个没有被 DP 覆盖的判断:
每年独立地把利息做到最大,就是全局最优。
凭什么?因为「资产更多 ⇒ 今年能拿到的利息不会更少」(容量单调)—— 所以今年多赚一分,明年的可行集只会更大,不可能出现「今年少赚点、明年赚更多」。
这一页的参照物故意不假设那件事:它每年枚举所有可行的债券组合(不只是利息最大那种), 然后对每一种可能的利息各往下走一年。
| 随机 300 组:正解 vs 不做该假设的全局暴力 | ★ 不一致 0 组 |
⇒ 和第 14 章 P1332、第 11 章 P1966 是同一个动作: 正解里那些「题面没证明、自己觉得显然」的跳跃,要用一条和它无关的路验一遍。
3★★★ 关键的一步(三):「a 是 1000 的倍数」是一张许可证
数据范围最后那半句话很容易当成背景:a 是 1000 的倍数。
它的意思是:所有金额都可以除以 1000。而背包的复杂度是 O(容量) ——
容量缩 1000 倍,整道题就缩 1000 倍。
| 除以 1000(正解) | 不除 | |
|---|---|---|
| DP 内层执行次数 | 4 422 660 | ★ 4 422 660 000 |
| 倍数 | ★ 1000 倍,一分不差 | |
f 数组要多大 |
195 KB | ★ 172 MB |
| 内存限制 | 128 MB | ★ 超了 |
⇒ 它先撞的其实是内存那堵墙,然后才是时间。
(f 要开到「最终资产」那么多格,而最终资产是 4524 万。)
它和正解逐组相等(小数据上一次不差),错的只有时间和空间。
⇒ 第 20 章 P5019 那条结论的又一次现场: 官方样例和对拍筛的都是「答案错」,对「答案对但跑不完」完全无能为力。 这一次连样例都放过了它(样例的资产才 14050,除不除都是一眨眼)。
⇒ 判它死刑只能靠数次数 + 算内存 —— 而这两笔都是三十秒的算术。
4★★ 数组开多大:这也是算出来的,而且那个界很紧
f 要开到「n 年后的最大资产」那么多格。而资产每年最多涨多少?
利息 ≤ 投资额的 10%,而投资额 ≤ 当年资产
⇒ 资产每年最多 ×1.1
⇒ 40 年后 ≤ 10⁶ × 1.1⁴⁰ ≈ 4.526 × 10⁷ (除以 1000 之后:45 260 格)
造一组「利息率正好 10%、钱能全投出去」的数据(a = 1000、b = 100)跑满 40 年:
算出来的上界 10⁶ × 1.1⁴⁰ |
45 259 255 |
| 实测最终资产 | ★ 45 244 600 |
| 差 | 0.03%(整数除法的取整损失) |
| 除以 1000 之后要多少格 | 45 244 ⇒ 正解开 50001,富余 |
⇒ 这个界是紧的(不像 P1060 那句 < 10⁸ 松了 666 倍)——
⇒ 「数组开多大」这个问题,题面已经把答案写在数据范围里了,只是要自己乘一遍。
5★★ 两个错法(官方样例都挡住了)
// 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;}点「运行 ▶」看结果
先把第二个「算了什么」说清楚:
随机 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度量程序和生成器
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 |