2 条题解
-
0
超市促销商品销售计划 - 贪心算法题解
问题分析
超市有
n件促销商品,每件商品有两个属性:- 利润
p_i:售出该商品可获得的利润 - 截止时间
d_i:商品必须在时间d_i之前完成销售(包含d_i)
销售约束条件:
- 每件商品的销售需要恰好 1 个单位时间
- 同一时间只能销售一件商品
- 可以选择销售哪些商品以及销售顺序
目标:制定销售计划,使总利润最大化。
算法思路
核心贪心策略
这个问题是经典的 "带截止时间的任务调度问题" 的一个变体。我们需要从
n个任务(商品)中选择一部分,安排到时间线上,使得:- 每个任务在其截止时间前完成
- 同一时间只能执行一个任务
- 最大化所选任务的总价值(利润)
关键洞察:从时间轴的末尾向前倒推安排商品销售。
算法流程
- 数据分组:将所有商品按截止时间分组
- 倒序处理:从最晚的截止时间开始,向时间 1 推进
- 选择策略:在每个时间点,选择当前可用的所有商品中利润最大的进行销售
详细算法步骤
1. 数据预处理
// profit[d] 存储所有截止时间为 d 的商品利润 vector<int> profit[N]; // N = 10010,因为 d_i ≤ 10000 // 读入数据并分组 for(int i = 0; i < n; i++){ scanf("%d%d", &p, &d); profit[d].push_back(p); max_d = max(max_d, d); // 记录最晚截止时间 }2. 倒序安排销售
priority_queue<int> pq; // 最大堆,存储当前可选的商品利润 int best = 0; // 最大总利润 // 从最晚的时间开始,倒着处理每个时间点 for(int time = max_d; time >= 1; time--){ // 将所有截止时间等于当前时间的商品加入候选集 for(int i = 0; i < profit[time].size(); i++){ pq.push(profit[time][i]); } // 如果当前时间点有候选商品,选择利润最大的销售 if(!pq.empty()){ best += pq.top(); // 选择最大利润的商品 pq.pop(); // 该商品已被安排 } }算法逻辑解释
假设当前处理的时间点是
t:- 优先队列
pq中包含所有截止时间 ≥ t 且尚未被安排的商品 - 这些商品都可以安排在时间
t或之后销售 - 选择其中利润最大的商品在时间
t销售是最优的 - 剩余商品可以安排在更早的时间(如果它们的截止时间允许)
正确性证明
引理1:倒序处理的优势
从后往前处理时间轴确保了:
- 早期时间有更多选择:当处理时间
t时,我们已经看到了所有截止时间 ≥t的商品 - 不会错过高利润商品:高利润商品即使截止时间较早,也有机会被安排
引理2:贪心选择的最优性
假设在某个时间点
t,当前可选的商品集合为C。选择C中利润最大的商品g_max安排在时间t不会导致次优解。证明(交换论证): 假设存在一个更优的安排,在时间
t安排了商品g(g ≠ g_max,且profit(g) < profit(g_max))。 我们可以交换g_max和g的安排:- 如果
g_max在原安排中在时间t' > t,交换后g_max在时间t(合法,因为d_{g_max} ≥ t) g被安排到时间t'(也合法,因为d_g ≥ t,且t' > t)
交换后总利润增加,因此原安排不是最优的。
定理:算法得到最优解
通过数学归纳法可以证明:对于每个时间点
t,算法的选择都是当前情况下的最优选择,且这些局部最优选择构成了全局最优解。时间复杂度分析
- 分组存储:O(n),n ≤ 10000
- 优先队列操作:
- 每个商品最多入队一次:O(log n)
- 每个时间点最多出队一次:O(log n)
- 总时间复杂度:O(n log n)
在题目数据范围(n ≤ 10000)内,这个复杂度完全可接受。
空间复杂度分析
- 分组数组:O(N),N = 10010
- 优先队列:最坏情况下 O(n)
- 总空间复杂度:O(n)
示例分析
示例1(题目中的例子)
输入:4 50 2 10 1 20 2 30 1 商品列表: - 商品A: 利润50, 截止时间2 - 商品B: 利润10, 截止时间1 - 商品C: 利润20, 截止时间2 - 商品D: 利润30, 截止时间1 分组: profit[1] = {10, 30} profit[2] = {50, 20} 执行过程: 时间2:候选集 {50, 20} → 选50,利润=50 时间1:候选集 {10, 30, 20} → 选30,利润=30 总利润:80示例2
输入:7 20 1 2 1 10 3 100 2 8 2 5 20 50 10 分组(简化): profit[1] = {20, 2} profit[2] = {100, 8} profit[3] = {10} profit[10] = {50} profit[20] = {5} 执行过程(关键步骤): 时间20:选5 时间19-11:无候选 时间10:选50 时间9-4:无候选 时间3:选10 时间2:选100 时间1:选20 总利润:5+50+10+100+20=185算法特点
1. 为什么使用优先队列(最大堆)?
- 需要快速找到当前候选商品中利润最大的
- 优先队列(最大堆)支持 O(1) 时间获取最大值,O(log n) 时间插入和删除
2. 为什么从后往前处理?
- 确保每个时间点都能看到所有符合条件的商品
- 避免高利润商品因为截止时间较早而被忽略
3. 算法的巧妙之处
- 不需要显式地跟踪时间占用情况
- 通过倒序处理自然保证了时间冲突的避免
- 分组存储简化了数据组织
实现细节
边界情况处理
- n = 0:直接输出 0
- 所有商品截止时间相同:算法仍然正确
- 利润相同,截止时间不同:算法会优先安排截止时间较晚的(因为先看到它们)
代码优化
原代码中的
profit[N]使用 vector 数组,可以动态调整大小,避免内存浪费。每次测试用例开始前需要清空数组。总结
本问题通过倒序贪心算法高效解决:
- 分组存储:按截止时间组织商品
- 倒序处理:从最晚时间向最早时间推进
- 贪心选择:每个时间点选择当前可用的最大利润商品
- 优先队列:高效管理候选商品
算法保证了最优解,时间复杂度 O(n log n),空间复杂度 O(n),能够高效处理最大规模的数据。这种"从后往前+最大堆"的策略是解决带截止时间调度问题的经典方法,广泛应用于任务调度、资源分配等领域。
- 利润
-
0
解题思路分析
核心思想
采用贪心算法求解。其核心思路是从最大截止时间开始,依次考虑每个截止时间的商品利润。在每个截止时间,优先选择利润最大的商品进行销售,这样能保证在满足截止时间的前提下,尽可能获取最大利润。
算法正确性证明
假设存在一种更优的销售计划,其选择的商品与当前贪心算法选择的商品不同。由于优先选择利润大的商品,对于任何截止时间
d,如果在当前贪心算法中没有选择某个利润较小的商品,而在所谓更优的计划中选择了它,那么在当前截止时间内,将这个利润较小的商品替换为贪心算法选择的利润最大的商品,只会使总利润增加或保持不变。因为截止时间的限制,在同一截止时间内,选择利润更大的商品总是更优的策略。 对于不同截止时间的商品选择,从大截止时间到小截止时间依次考虑,能确保在满足所有截止时间的情况下,最大程度地积累利润。例如,如果有两个商品A(利润pA,截止时间dA)和B(利润pB,截止时间dB,dA > dB),先考虑dA,选择A不会影响在dB时对商品的选择,且能保证先获取较大的利润。时间复杂度和空间复杂度分析
时间复杂度
输入部分:读取n个商品的数据,时间复杂度为。 计算最优利润部分:对于每个截止时间
d(最多有max_d个不同的截止时间),将其对应的利润值压入优先队列,每个截止时间内处理利润值的时间复杂度为(k为该截止时间下商品的数量),总的时间复杂度为,但由于所有商品数量为n,所以这部分时间复杂度也可近似看作。因此,整体时间复杂度为。空间复杂度
存储利润的向量数组
profit[N],最多存储n个利润值,空间复杂度为。 优先队列pq中最多存储n个元素,空间复杂度为。所以,总的空间复杂度为。总结
通过合理的数据结构和贪心算法,有效地解决了寻找最优销售计划利润的问题。其从输入数据的处理、利润计算到结果输出,逻辑清晰,时间复杂度和空间复杂度都能满足题目要求。代码如下:
#include <iostream> #include <vector> #include <stdio.h> #include <utility> #include <queue> #include <stack> #include <algorithm> #include <set> using namespace std; const int N = 10010; int n; vector<int> profit[N]; int main(){ while(scanf("%d",&n)!=EOF){ int max_d = 0; int p,d; for(int i=0;i<N;i++) profit[i].clear(); for(int i=0;i<n;i++){ scanf("%d%d",&p,&d); profit[d].push_back(p); max_d = max(max_d,d); } priority_queue<int> pq; int best = 0; for(d=max_d;d>=1;d--){ for(int i=0;i<profit[d].size();i++){ pq.push(profit[d][i]); } if(pq.empty()) continue; best += pq.top(); pq.pop(); } printf("%d\n",best); } }
- 1
信息
- ID
- 457
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 5
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者