你有 n 个任务。第 i 个任务有一个数值 ci 和难度 pi。你的初始耐力值为 1,记为 S。你需要按任务 1 到任务 n 的顺序处理这些任务。对于每个任务,你有两种选择:
- 放弃该任务:不会发生任何变化。
- 完成该任务 :你将获得 S⋅ci 的分数。但完成后,耐力值 S 会变为 S⋅(1−100pi)。
你需要最大化处理完所有任务后的总分数。
输入格式
输入包含多组测试数据。第一行一个整数 t(1≤t≤103),表示测试用例数量。
每组测试用例:
- 第一行一个整数 n(1≤n≤105),表示任务数量。
- 接下来 n 行,每行两个整数 ci(1≤ci≤100)和 pi(0≤pi≤100)。
保证所有测试用例的 n 之和不超过 105。
输出格式
对于每组测试用例,输出一个实数,表示能获得的最大分数。当你的答案与标准答案的绝对误差或相对误差不超过 10−6 时,视为正确。
形式化地说,设你的答案为 a,标准答案为 b,满足 max(1,∣b∣)∣a−b∣≤10e−6 即可。
样例输入
2
2
10 0
20 5
3
10 5
10 80
20 5
样例输出
30.0000000000
29.0000000000
解题思路
这个问题的核心是贪心策略,关键在于找到 "完成任务" 的决策标准 ------ 判断完成当前任务是否能让总分数增加。
1. 核心观察:
对于每个任务 i,我们需要判断:完成它是否比放弃它更划算 。假设当前耐力为 S,完成任务 i 的收益是 S⋅ci,但代价是后续所有任务的收益都会乘以系数 ki=(1−100pi)(因为耐力会永久降低)。
因此,完成任务 i 的净收益需要满足:S⋅ci>S⋅ki⋅(后续所有最优收益)−S⋅(后续所有最优收益)简化后(两边除以 S):ci>(后续所有最优收益)⋅(ki−1)即:ci>(后续所有最优收益)⋅(−100pi)进一步变形为:完成任务⟺ci>(后续最优总收益)⋅100pi
2. 逆向推导(关键技巧)
直接正向计算会因为 "后续收益未知" 无法决策,因此我们从后往前遍历任务:
- 定义
dp[i]表示 "处理第 i 到第 n 个任务能获得的最大总分数"。 - 对于最后一个任务 n:我们肯定会去完成,因为没有后续影响了。
- 对于第 i 个任务(i<n):
- 设衰减系数 k=1−100pi。
- 完成任务 i 的总收益:ci+k⋅dp[i+1](当前收益 ci + 后续收益(因耐力衰减需乘以 k))。
- 放弃任务 i 的总收益:dp[i+1](后续收益不变)。
- 因此 dp[i]=max(dp[i+1],ci+k⋅dp[i+1])。
完整代码实现
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 设置输出格式:固定小数形式,保留10位小数(满足题目1e-6的精度要求)
cout << fixed << setprecision(10);
int t; // 测试用例数量
cin >> t; // 读取测试用例数
while (t--) { // 处理每组测试用例
// 定义变量:n为任务数,c数组存每个任务的分值,p数组存每个任务的耐力衰减百分比
// 数组大小设为100001是为了适配n≤1e5的题目限制
int n,c[100001],p[100001];
cin >> n; // 读取当前测试用例的任务数
double dp = 0.0; // dp表示「从当前位置往后能获得的最大收益」(逆向贪心的核心变量)
// 读取所有任务的c_i(分值)和p_i(衰减百分比)
for (int i = 0; i < n; ++i){
cin >> c[i] >> p[i];
}
// 初始化:先处理最后一个任务(逆向遍历的起点)
// 初始时dp=0,完成最后一个任务的收益就是c[n-1](无后续任务,衰减不影响)
dp += 1.0 * c[n-1];
// 逆向遍历:从倒数第二个任务开始,直到第一个任务(i=0)
for (int i = n-2; i >= 0; i--) {
// 计算完成当前任务后,耐力的衰减系数:(1 - p_i/100)
double k = 1.0 - p[i]/100.0;
// 贪心决策核心:
// 选项1:放弃当前任务 → 收益保持dp不变(后续的最大收益)
// 选项2:完成当前任务 → 收益=当前任务分值 + 衰减后后续的最大收益(k*dp)
// 取两者的最大值更新dp,保证每一步都是局部最优
dp = max(dp, (double)c[i] + k * dp);
}
// 输出当前测试用例的最大收益(dp最终值即为初始耐力1时的全局最优解)
cout << dp << '\n';
}
return 0;
}
复杂度分析
- 时间复杂度:O(t⋅n),每个测试用例遍历一次任务列表,总操作数不超过 10e5,符合时间限制。
- 空间复杂度:O(n)(存储任务列表)