超市
题目
超市里有 N 件商品,每件商品都有利润 pi 和过期时间 di,每天只能卖一件商品,过期商品不能再卖。
求合理安排每天卖的商品的情况下,可以得到的最大收益是多少。
输入格式
输入包含多组测试用例。
每组测试用例,以输入整数 N 开始,接下来输入 N 对 pi 和 di,分别代表第 i 件商品的利润和过期时间。
在输入中,数据之间可以自由穿插任意个空格或空行,输入至文件结尾时终止输入,保证数据正确。
输出格式
对于每组产品,输出一个该组的最大收益值。
每个结果占一行。
数据范围
0≤N≤10000,
1≤pi,di≤10000
最多有 14 组测试样例输入样例:
4 50 2 10 1 20 2 30 1 7 20 1 2 1 10 3 100 2 8 2 5 20 50 10
输出样例:
80 185
-
算法标签
- 堆
- 并查集
- 贪心
失败的贪心尝试
-
一个非常朴素的想法对日期有限排序,即过期时间早的先卖出,然后让天数自加.对于过期时间相同的商品优先卖利润高的
-
一组不能通过的数据
10 1 20 2 15 2 5 3
- 即优先卖掉的可能是价值低的,全局不一定最优
#include<bits/stdc++.h>
using namespace std;
const int N=10005;
int a[N],b[N];
int main(){
int n;
while(cin>>n){
for(int i=0;i<n;i++)cin>>a[i]>>b[i];
vector<int> idx(n);
iota(idx.begin(),idx.end(),0);
sort(idx.begin(),idx.end(),[&](int i,int j){
if(a[i]!=a[j])return a[i]>a[j];
return b[i]<b[j];
});
int cur=1,ans=0;
for(auto i:idx){
// cout<<'@'<<i<<endl;
if(b[i]>=cur){ans+=a[i];
cur++;
}
}
cout<<ans<<endl;
}
return 0;
}
解法1. 堆+贪心
- 建立一个小顶堆,堆中的元素表示在贪心决策的过程中前 t t t( t t t为堆的大小)天拟售出的最大利润商品集合
- 按商品过期时间进行从小到大排序,每次将商品的过期时间 d d d与堆的大小 t t t进行比较
- 如果 d > t d>t d>t则说明没有过期,加入堆中
- 如果 d = = t d==t d==t则说明前 t t t天已经每天安排了商品,需要将当前元素的价值与堆顶元素(价值最低的元素)进行比较,如果价值比堆顶元素大,将其替换掉堆顶元素能产生更大价值.否则则不操作维持现状
- 如果 d < t d<t d<t则说明前 t t t天已经安排了销售商品,不能在其过期前售出
#include <bits/stdc++.h>
// #define debug
using namespace std;
const int N = 10005;
int a[N], b[N];
int main() {
#ifdef debug
freopen("in.txt", "r", stdin);
#endif
ios::sync_with_stdio(0);
int n;
while (cin >> n) {
priority_queue<int, vector<int>, greater<int>> pq;//优先队列
for (int i = 0; i < n; i++)cin >> a[i] >> b[i];
vector<int> idx(n);
iota(idx.begin(), idx.end(), 0); //生成0~n-1的下标,并对下标进行排序
sort(idx.begin(), idx.end(), [&](int i, int j) {//c++ 匿名函数
return b[i] < b[j];
});
for (auto i: idx) {
if (b[i] > pq.size())pq.push(a[i]);//过期时间还没到
else if (b[i] == pq.size() && a[i] > pq.top()) {//刚好到过期时间并且比候选商品价值最小的要大
pq.pop();
pq.push(a[i]);
}
}
int sum = 0;
while (!pq.empty()) {
// cout << pq.top() << endl;
sum += pq.top();
pq.pop();
}
cout << sum << endl;
}
return 0;
}
解法2. 并查集+贪心
- 要想理解并查集的做法要先看未优化的 O ( n 2 ) O(n^2) O(n2)版本
- 未优化的 O ( n 2 ) O(n^2) O(n2)版本
- 首先对商品利润进行从大到小排序,然后将利润大的商品在其过期前尽可能晚一点的时刻售出
- 简单的证明(交换论证法)
- 假设不将利润最大的商品 A 1 A_1 A1放在其过期时刻之前的最晚时刻销售,该方案能产生一个最优解 O O O
- 那么将其后面安排的销售商品提前,与 A 1 A_1 A1进行交换(因为这些商品在 O O O的安排情况是不过期的,提前销售必然也是不过期的),可以产生一个等价的最优解 O ′ O' O′
- 同理将利润次大的商品依次进行轮换,能产生一个最优解,满足在商品过期前尽可能晚一点的时刻售出的贪心策略
//时间复杂度O(n^2),会超时
#include <bits/stdc++.h>
#define debug
using namespace std;
const int N = 10005;
int a[N], b[N];
bool schedule[N];
int main() {
#ifdef debug
freopen("in.txt", "r", stdin);
#endif
ios::sync_with_stdio(0);
int n;
while (cin >> n) {
memset(schedule, 0, sizeof schedule);
for (int i = 0; i < n; i++)cin >> a[i] >> b[i];
vector<int> idx(n);
iota(idx.begin(), idx.end(), 0); //生成0~n-1的下标,并对下标进行排序
sort(idx.begin(), idx.end(), [&](int i, int j) {//c++ 匿名函数
return a[i] > a[j]; //按商品利润从大到小排序
});
int sum = 0;
for (auto i: idx) {
int k = b[i];//过期日期
while (k >= 0 && schedule[k])k--;//在商品过期前安排一个日期销售
if (k >= 1) {
schedule[k] = true;//安排在第k天销售
sum += a[i];
// cout << '@' << a[i] << endl;
}
}
cout << sum << endl;
}
return 0;
}
- 很不幸,未优化的算法时间复杂度是 O ( n 2 ) O(n^2) O(n2)的,会超时
- 使用路径压缩的并查集进行优化
- 并查集做法
- 并查集的 p [ x ] p[x] p[x]根表示从 x x x天开始往前的最近的一个空闲位置,初始化时 p [ x ] = x p[x]=x p[x]=x,即第 x x x天过期的商品将在 x x x天售出
- 排序依然是根据商品的价值从大到小排序
- 当前一个决策的商品决定了在第 r r r天销售,那么会将其根 p [ r ] p[r] p[r]设置为 r − 1 r-1 r−1,意味着当下一件决策商品也打算占用 r r r位置时,由于被鸠占鹊巢,将其放在前一个位置 r − 1 r-1 r−1,如果依旧被占用直至决策位置到了0则说明这件商品不能被售出
- 时间复杂度
- 排序 O ( n log n ) O(n\log n) O(nlogn),贪心决策 O ( n ) O(n) O(n),中的时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)