题目描述
小奇要开采一些矿物,它驾驶着一台带有钻头(初始能力值 (w))的飞船,按既定路线依次飞过喵星系的 (n) 个星球。
星球分为 (2) 类:资源型和维修型。
- 资源型:含矿物质量 (a_i),若选择开采,则得到 (a_i imes p) 的金钱,之后钻头损耗 (k\%),即 (pleftarrow p imes (1-k\%))
- 维修型:维护费用 (b_i),若选择维修,则支付 (b_i imes p) 的金钱,之后钻头修复 (c\%),即 (pleftarrow p imes (1+c\%))
((p) 为钻头当前能力值)
注:维修后钻头的能力值可以超过初始值。
请你帮它决策最大化这个收入。
输入格式
第一行 (4) 个整数 (n),(k),(c),(w)。
以下 (n) 行,每行 (2) 个整数 (type),(x):
- (type) 为 (1) 则代表其为资源型星球,(x) 为其矿物质含量 (a_i);
- (type) 为 (2) 则代表其为维修型星球,(x) 为其维护费用 (b_i)。
输出格式
输出一行一个实数 (保留两位小数),表示要求的结果。
数据范围
测试时间限制 (1000 extrm{ms}),空间限制 (256 extrm{MiB})。
- 对于 (30\%) 的数据 (nle 100)
- 对于 (50\%) 的数据 (nle 1000),(k=100)
- 对于 (100\%) 的数据 (nle 10^5),(0le k,c,w,a_i,b_ile 100)
保证答案不超过 (10^9)。
分析
这道题的部分分设计不当,我们来考虑一下 Author 为什么这么设部分分。
(30; mathtt{pts})
注意到这题的钻头和金钱是两个变量,我们一定要控制一个。但是都是实数不好表示。怎么办?
再次注意到这题的钻头耐久机制是乘法。由于乘法满足交换律和结合律,所以我们只要记录下开了几次矿,修了几次就行了。
定义 (f_{i,j,k}) 为修理 (i) 次,开矿 (j) 次,目前已经到达第 (k) 个星球的最大金钱数。
递推也好得到:(f_{i,j,k}=egin{cases}max{f_{i,j,k-1},f_{i,j-1,k-1}+a_i imes calc(i,j-1)}&k; ext{号星球是资源型}\max{f_{i,j,k-1},f_{i-1,j,k-1}-b_i imes calc(i-1,j)}&k; ext{号星球是修理型}end{cases})。
边界值:(f_{0,0,0}=0),其余赋值 (-infty)。最终求 (max{f_{i,j,n}})。
复杂度 (Theta(n^3))。
是不是已经有点不明所以,不知道如何优化了?接下来就会更加令人疑惑了。
(50;mathtt{pts})
这是福利的部分分,却是 AC 的坑。
我们来看一下这 (50\%) 的数据。最引人注目的就是那个闪闪发光的——
好了这波福利部分分应该怎么拿呢?
考虑到这是用乘法进行计算,并且只要挖一次,钻头就会彻底报废。
那么我们只要枚举修理的次数就可以了。复杂度可以降到 (Theta(n^2))。
(100;mathtt{pts})
完了,现在没有福利保障了,(n) 的范围也大增,怎么搞呢?
如果你有被部分分骗了的感觉,那你就能感受到此题 Author 的丑恶。
我们回过头去想最开始的那个 DP。
记得不?那个 (Theta(n^3)) 的 DP。
这个复杂度为什么这么高?因为有钻头的变量在影响着我们。
那么,我们要做一个大胆的操作,那就是把钻头的影响消除。
这个怎么搞呢?我们来想一想:
下面为了方便考虑,我们定义数列 ({c_i}=egin{cases}a_i&i; ext{号星球是资源型}\-b_i&i; ext{号星球是维修型}end{cases});
(u_i=egin{cases}1-k\%&i; ext{号星球是资源型}\1+c\%&i; ext{号星球是维修型}end{cases});
(r_i= ext{最优解是否选择第 }i ext{ 个。选了就是 }1 ext{,否则就是 }dfrac{1}{u_i})。
假设在最优解的情况下,经过第 (i) 个星球后,钻头的耐久成为了 (w_i)。
那么总共的钱数 (M=sum^n_{i=1}(w_i imes c_i))
而对于 (w_i=w imes prod^i_{j=1}(u_j imes r_j))
注意到只有 (r_i) 是不确定的。而不确定的 (r_i) 只会影响 (w_k(1le kle i))。
也就是说,任意一个星球的决策不会影响后面所有的决策。
本着 DP 没有后效性的原则,我们自然而然想出了一个操作,那就是——
好了,现在正解已经呼之欲出了。
定义 (f_i) 为 后 (i) 个星球的金钱最大值。
我们只需要假定当前钻头耐久为 (1),完成这一轮决策后再把这一轮的钻头初始耐久设为 (1)。
如此这般,最后再乘上 (w),就是答案。
当然,我们可以把 (f_i) 浓缩成一个数,节省空间。
Code
日常放代码。虽然这一次的题目已经没有什么代码难度了。
#include <cstdio>
#include <cctype>
using namespace std;
constexpr int max_n = 100000;
double val[max_n];
bool is_fix[max_n];
inline int read()
{
int ch = getchar(), n = 0, t = 1;
while (isspace(ch)) { ch = getchar(); }
if (ch == '-') { t = -1, ch = getchar(); }
while (isdigit(ch)) { n = n * 10 + ch - '0', ch = getchar(); }
return n * t;
}
inline double my_max(double x, double y) { return (x > y)? x:y; }
int main()
{
int n = read(), cost = read(), exp = read(), wei = read(), type, tmp;
double ans = 0;
for (int i = 0; i < n; i++)
{
scanf("%d%d", &type, &tmp);
val[i] = tmp;
is_fix[i] = (type == 2);
}
for (int i = n - 1; i >= 0; i--)
{
if (is_fix[i])
ans = my_max(ans, ans * (1 + exp / 100.0) - val[i]);
else
ans = my_max(ans, ans * (1 - cost / 100.0) + val[i]);
}
printf("%.2lf
", ans * wei);
return 0;
}
后记
我考场上时也没有想到正解。虽然朦朦胧胧知道要利用乘法这个性质。
后来看到 solution 时真是自愧不如。
又过了一阵子,我好不容易脑补出来这么一个想法的过程。自我感觉讲得很清楚。
如果这篇题解对你有帮助,欢迎点赞、留言,给 5ab 提出意见。感谢大家的资瓷哦!