这道题是蓝桥上的一道题目(Lanqiao OJ 3875)
问题描述
小蓝作为班级里的体育健将,他被安排在校运会时参加 𝑛 个体育项目,第 𝑖 个体育项目参赛需要耗时 𝑎𝑖 分钟,赛后休息需要 𝑏𝑖 分钟(这意味着当他参加完这场比赛后需要经过 𝑏𝑖 分钟才能再次参加其他项目)。
由于小蓝实在太强了,无论参加任何比赛他都可以拿到金牌,但他能够参加校运会的时间有限,最多只能参加 𝑘 分钟,请问他最多能拿多少块金牌?
输入格式
第一行包含两个整数 𝑛 和 𝑘,表示体育项目数量和小蓝的可参加校运会时间。
第二行包含 𝑛 个整数 a1,a2,⋯,an,表示每个项目需要的参赛时间。
第三行包含 𝑛 个整数 b1,b2,⋯,bn,表示每个项目后面需要的休息时间。
数据范围保证:1≤𝑛≤2e5,1≤𝑎𝑖,𝑏𝑖,𝑘≤1e8。
输出格式
输出一个整数,表示在 𝑘k 分钟内小蓝最多可以拿到几个金牌。
输入样例
3 10
3 4 5
2 4 2
输出样例
2
说明
样例中小蓝可以先参加比赛 1,然后再参加比赛 3。虽然参加比赛 3 后还需要休息 2 分钟,但这不影响他在 10 分钟内参加了两个项目拿下两块金牌。
解题思路:
一. 暴力贪心
开始时采用了暴力贪心策略,但是有数据过不了,姑且先介绍一下暴力贪心:
首先对所有项目的总时间计算并进行从小到大的排序,再从总时间最小的开始遍历所有项目。如果该项目的比赛时间小于等于目前所剩的时间,那么就让答案+1,再从目前所剩时间里减去该项目的比赛时间和休息时间的总和,随后进行下一次循环。如果当前所剩时间<=0,那么就终止循环并输出答案。
以下是贪心的代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 1;
int a[N], b[N], c[N];
int main()
{
int n, k;cin >> n >> k;
int num = 0;
vector<pair<int,int>> vec;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++)
{
cin >> b[i];
vec.push_back(make_pair(a[i]+b[i] ,i));
}
sort(vec.begin(),vec.end());
for(int i = 0; i < n && k > 0; i++)
{
int idx = vec[i].second;
if(k-a[idx]>=0)
{
k = k - vec[i].first;
num ++;
}
}
cout << num << '\n';
return 0;
}
然而,暴力贪心存在一个问题,就是它是由局部最优解推得总体最优解的,对于暴力贪心这种贪心策略,某些情况下的局部最优解并不能得到总体最优解,以下数据就是一个例子:
2 10
2 5
5 3
如果用暴力贪心,得到的答案是1,但是正确答案显而易见是2,因而我们需要换一种思路,或者进行贪心优化
二. 前缀和、二分答案
本方法是评论区里一位大佬zccccc提供的思路,本文借鉴一下。此方法的本质还是枚举,同时采取了各种优化:二分法——优化枚举,前缀和——优化求区间和
由于我们在计算总时间时,唯一不需要计算休息时间的就是最后一个项目,它也是最特殊的,所以我们把它单拎出来进行枚举。
总体思路:
(注:参赛时间et,休息时间br)
1.枚举参加的最后一个项目item[i],算出除去该项目后的剩余时间temp - item[i].et
2.按总时间(参赛+休息)排序,并求出其前缀和数组s,s[i]的含义是参加前i件项目的总时间和
3.二分答案进行枚举,在确定了最后一个项目的前提下,看前面最多能参加多少项目,使得刚好不超过总时间,参加i个项目的总时间就是s[i](这是因为前面已经进行了排序、前缀和)
需要注意的是,如果前x件项目中包含枚举的i,我们需要将其删去并将s[x]改为s[x+1]。
总结:这种方法相当于改变了贪心策略,把原先暴力贪心的策略:将最后一个项目(不计算休息时间)和前面所有项目(计算休息时间)放在一起贪心,改变成了以下策略:先枚举最后一个项目,再对前面所有项目进行贪心,这时采用暴力贪心即可,因为前面所有项目都必须计算休息时间,所以直接把两个时间加起来排序就行,这已经是最优的策略了,局部最优解一定能得到整体最优解。
以下是完整代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5;
int n,k,ans;
long long s[N];
struct node {
int et,br;
}item[N];
//按总时间从小到大排序
bool cmp(const node &a, const node &b) {
return a.et + a.br <= b.et + b.br;
}
//如果前x件项目中包含枚举的y,我们需要将其删去并将s[x]改为s[x+1]。
bool check(int x, int y,int time) {
//前x件项目中不包含枚举的y
if(x < y) {
if(s[x] <= time) return true;//参与前x项,可以在剩余时间内完成
else return false; //参与前x项,不能在剩余时间内完成
}
//前x件项目中包含枚举的y
else {
if(x + 1 > n) return false;
if(s[x + 1] - item[y].et - item[y].br <= time) return true;
else return false;
}
}
int main() {
//输入
ios::sync_with_stdio(false),cin.tie(0);
cin>>n>>k;
for(int i = 1; i <= n; i ++) cin>>item[i].et;
for(int i = 1; i <= n; i ++) cin>>item[i].br;
//排序
sort(item+1,item+1+n,cmp);
//前缀和:前i件项目的时间和
for(int i = 1; i <= n; i ++) s[i] = s[i-1] + item[i].et + item[i].br;
//枚举每一个项目,作为最后一个项目
for(int i = 1; i <= n; i ++) {
int temp = k;
if(temp < item[i].et) continue;//如果这个项目的比赛时间直接大于总时间了就直接排除
temp -= item[i].et;//算出剩余时间
//二分答案进行枚举
int l = 0, r = n;//从1~n个项目
while(l + 1 != r) {
int mid = (l + r) >> 1;
if(check(mid,i,temp)) l = mid;//参与前mid项,可以在剩余时间内完成
else r = mid; //参与前mid项,不能在剩余时间内完成
}
ans = max(l+1,ans); //l项加上之前枚举的最后一项,共l+1项
}
cout<<ans;
return 0;
}