题解:CF1132D Stressful Training
upd 2024.11.8 手欠撤销了题解,所以更改了一些地方,主要关于无解判断和部分卡常
省流:一个写了双
双 \log 解法
题目传送门
容易发现,答案具有单调性,考虑二分答案。
这样,问题变为已知充电量,求出是否存在方案,使得前
但是如果维护每分钟的剩余电量,每分钟都更改,由于每台电脑消耗的速度不相同,每分钟为
考虑不维护每分钟电量,只维护从开始时可使用时间(包括所有充电后的可使用时间)。
如果我们先给剩余时间少(很快就会没电)的人充电,与先给剩余时间多(晚些时候会没电)的人充电:
-
如果我们给剩余时间多的人充电,导致剩余时间少的人没电了,那么显然得不偿失。
-
否则,因为充电量一直不变,所以先充电,后充电对答案没有影响,即先给剩余时间少的人充电没有负面影响,结果不变。
从以上两点看,我们选择剩余时间更短的人充电一定更优。
如何选择剩余时间最短的人?
暴力求这个点是
这样,对于每分钟,用堆维护所有人没电的时间,选择时间最短的,将其充电后重新放回堆里,时间复杂度为
总时间复杂度为
对于充电头来说,当然是功率越大越强,但是到达对于每个人都是只需一次充电的程度之后(即充一次就可以用到比赛结束),没有区别,所以答案上界(有解时)为
堆采用 STL 实现。
一些小优化:(单个加速 0.1 秒左右)
- 不需要再充电的就不用进堆了,反正再次充电也没用。
- 有了上一点优化后,当堆为空时,直接返回
true就好了。 - 如果所有人的电池容量减去消耗速度都不够
k 分钟花的,直接返回false。 - 每次
check都新开一个堆,而不将原来的堆清空后使用。 - 开 C++20 O2。
- 快读。
- 由于此做法的空间复杂度小,可以把快读的
fread一次读入的字符数量增加。
一些较大的优化
- 可以把初始堆记录下来,这样每次
check时,建立 STL 堆的复杂度就从O(n\log{n}) 变为O(n) ,总复杂度变为O(k\log{n}\log{val}) 。 - 不用开
long long的用int就行了。 - 将剩余的时间存起来,而不是单独计算,剩余时间用
int而不是double存。 - 事实上,无解的情况可以
O(n+k) 判断,见下方代码。
代码:
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
constexpr int maxn=2e5+100;
#define ll long long
ll a[maxn],b[maxn];
ll n,m;
ll totb;
ll tote;
char buf[50000001];
inline char nc()
{
static char *p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,30000000,stdin),p1==p2)?EOF:*p1++;
}
ll read()
{
static char a=nc();
while(!isdigit(a)){a=nc();}
ll n=0;
while(isdigit(a))
{
n=(n<<1)+(n<<3)+(a^48);
a=nc();
}
return n;
}
//以上是快读
struct man
{
ll a1,b1;int last;// a1 表示初始(包括充电后的)电量 ,b1 表示每分钟消耗电量
//last表示从0时刻起可用时间
//按整分钟算,14.01 分钟和 14.99 分钟一样,都是第 15 分钟没电(即可以忽略小数部分)
bool operator <(const man tmp)const
{
return last>tmp.last;//注意 STL 是大根堆
}
};
priority_queue<man> q;
bool check(ll x)
{
if((totb-x)>tote/(double)(m-1))return false;//优化 3
priority_queue<man> q2=q;
//大优化 1
for(int i=0;i<m;i++)//第 i 到 i+1 分钟
{
if(q2.empty())return true;//优化 2
man tmp=q2.top();
//cout<<tmp.a1<<' '<<i<<'\n';
q2.pop();
if(tmp.last<i)return false;//已经有人电量为负了
else if((tmp.a1+x)<tmp.b1*m)q2.push({tmp.a1+x,tmp.b1,(tmp.a1+x)/tmp.b1});
//充电后重新入队,优化 1
}
return true;// k 分钟后无事发生
}
int tmp_bucket[200001];
bool can_doit()
{
for(int i=1;i<=n;i++)
{
if(a[i]<b[i]*m)tmp_bucket[a[i]/b[i]]++;
//如果超过了 m 分钟,直接无视
}
for(int i=0;i<m;i++)
{
if(i)tmp_bucket[i]+=tmp_bucket[i-1];
//防止越界
if(tmp_bucket[i]>i+1)//这段时间内可以充电 i+1 次,如果有超过 i+1 个人要充电
return false;//无解
}
return true;
}
signed main()
{
n=read(),m=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
tote+=a[i];
}
for(int i=1;i<=n;i++)
{
b[i]=read();
totb+=b[i];
}
for(int i=1;i<=n;i++)if(a[i]<b[i]*m)q.push({a[i],b[i],a[i]/b[i]});
if(!can_doit())//无解
{
cout<<-1;
return 0;
}
//预处理 check 时的初始堆
ll l=-1,r=2e12,mid;
//二分区间要开大,左区间需要为 -1
while(l<r-1)
{
mid=(l+r)>>1;
if(check(mid)){r=mid;}
else l=mid;
}
cout<<r;
return 0;
}
走向单 \log
我们发现,有了上述优化后,在堆中的人一定是可使用时长小于
我们用一个一维数组套 vector,用第一维来表示可使用时间(从开始时计算,下同),用第二维来表示所有的,这个时间没电的人。
每次从上一次的最小值开始,往后遍历 vector,找到最近一个有元素的 vector 并取出其的一个元素(实际使用时从后往前取以规避线性删除),判断合法性并将其充电后放到最新的位置,如果发现这次充电后不用再充电,则不做操作。
时间复杂度为
如果跟双
代码:
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
using namespace std;
constexpr int maxn=2e5+100;
#define ll long long
ll a[maxn],b[maxn];
ll n,m;
ll totb;
ll tote;
inline char nc()
{
static char buf[30000000],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,30000000,stdin),p1==p2)?EOF:*p1++;
}
ll read()
{
static char a=nc();
while(!isdigit(a)){a=nc();}
static ll n;n=0;
while(isdigit(a))
{
n=(n<<1)+(n<<3);
n+=(int)(a^48);
a=nc();
}
return n;
}
//以上是快读
struct man
{
ll a1,b1;int last;// a1 表示初始(包括充电后的)电量 ,b1 表示每分钟消耗电量
//last表示从0时刻起可用时间
//按整分钟算,14.01 分钟和 14.99 分钟一样,都是第 15 分钟没电(即可以忽略小数部分)
};
struct fm//find_max 的缩写
{
vector<man> bucket[maxn];//桶
int now;//使用这个变量作为指针
fm(){now=0;}
void operator =(const fm tmp)
{
for(int i=tmp.now;i<=m;i++)
{
bucket[i]=tmp.bucket[i];
}
now=tmp.now;
}
inline bool empty()
{
while(!bucket[now].size()&&now<=m)now++;//跳过空桶
return now==m+1;
}
inline man top()
{
//由于每次调用前都会检查是否为空,所以不必再次清空
return bucket[now].back();
}
inline void push(man tmp)
{
bucket[tmp.last].emplace_back(tmp);
}
inline void pop()
{
bucket[now].pop_back();
}
}q,q2;
bool check(ll x)
{
if((totb-x)>tote/(double)(m-1))return false;//优化 3
q2=q;
for(int i=1;i<=m;i++)
{
if(q2.empty())return true;//优化 2
man tmp=q2.top();
//cout<<tmp.a1<<' '<<i<<'\n';
q2.pop();
if(tmp.last<(i-1))return false;
//已经有人电量为负了,注意这里是 i-1 ,是因为计算前 i-1 分钟而不是前 i 分钟
else if((tmp.a1+x)<tmp.b1*m)q2.push({tmp.a1+x,tmp.b1,(tmp.a1+x)/tmp.b1});
}
//充电后重新入队,优化 1
return true;// k 分钟后无事发生
}
signed main()
{
n=read(),m=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
tote+=a[i];
}
for(int i=1;i<=n;i++)
{
b[i]=read();
totb+=b[i];
}
for(int i=1;i<=n;i++)if(a[i]<b[i]*m)q.push({a[i],b[i],a[i]/b[i]});
//预处理 check 时的初始桶
if(q.empty()){cout<<0;return 0;}//其实不仅处理了不用充电,而且重置了指针
ll l=-1,r=2e12,mid;
//二分区间要开大,左区间需要为 -1
while(l<r-1)
{
mid=(l+r)>>1;
if(check(mid)){r=mid;}
else l=mid;
//cout<<"Success\n";
}
if(r==2e12)cout<<-1;//无解
else cout<<r;
return 0;
}
不知道是常数哪里过大了,时间反而是原来双
附上错误指南:WA,TLE集