贪心【逆向dp】

你有 n 个任务。第 i 个任务有一个数值 ci​ 和难度 pi​。你的初始耐力值为 1,记为 S。你需要按任务 1 到任务 n 的顺序处理这些任务。对于每个任务,你有两种选择:

  1. 放弃该任务:不会发生任何变化。
  2. 完成该任务 :你将获得 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)(存储任务列表)
相关推荐
努力学算法的蒟蒻2 小时前
day114(3.16)——leetcode面试经典150
算法·leetcode·职场和发展
重庆兔巴哥2 小时前
如何在Dev-C++中使用MinGW-w64编译器?
linux·开发语言·c++
夜月yeyue2 小时前
Linux 邻接(Neighbor)子系统架构与 NUD 状态机
linux·运维·服务器·嵌入式硬件·算法·系统架构
叶子野格2 小时前
《C语言学习:Visual Studio使用》2
c++·学习·visual studio
【数据删除】3482 小时前
计算机复试学习笔记 Day43
笔记·学习
深瞳智检2 小时前
lesson-01 NLP 概述学习笔记 & 学习心得
人工智能·笔记·学习·自然语言处理·llm·大语言模型
Qt程序员2 小时前
深入理解:GDB调试器的工作原理
linux·c++·gdb·调试器
ArturiaZ2 小时前
【day55】
数据结构·c++·算法
仰泳的熊猫2 小时前
题目2279:蓝桥杯2018年第九届真题-日志统计
数据结构·c++·算法·蓝桥杯