首先答案期望最大值最小化,考虑二分答案,二分答案:最高的竹子的高度是X,这道题的重点在于:怎么判断最高的竹子高度为X能不能达到?
不好处理的点在于,如果当前某个竹子的高度h_cur小于了p,敲击带来的贡献就不再是p了,而是h_cur,我们用倒着考虑这个问题的方法可以规避到这一点,具体地:
首先明确,每天如果分为早上和晚上,那么敲击动作是在早上,生长动作是在晚上。倒过来,时间倒流,那么过程变为:晚上降低高度a_i,早上如果选择,那么可以提高高度p,如果一直不提高某个竹子,过几天,竹子的高度可能变为负数,如果出现了这种情况就不行了,因为倒着模拟,竹子的高度其实还是和正着的正常情况一样的,而竹子的高度不可能是负数。
正向(正常情况),竹子应该是:(降)-升-(降)-升-... ...
逆向:降-(升)-降-(升)-... ...
逆向过程竹子的最终高度大于等于a_i,则用和逆向过程一样的策略,在正向的常规时间中击打竹子,可以使得竹子的最终高度小于等于X。
初始时,给每个竹子计算竹子高度小于0的日期ddl,将ddl加入优先队列,倒着模拟每一天,每天优先队列中ddl小的先敲打,敲打之后,计算什么时候需要下一次敲打来防止其高度小于0,同样将这个新的ddl'加入优先队列。最后如果竹子高度不足a_i,给其补敲,注意最终的敲击次数要小于等于m*k。这部分的实现请看代码。
正向击打的时候击打效果可能小于p(此时竹子的高度小于p),那逆向模拟为什么每次都能按照高度p提升呢?分情况来看,每天上午的竹子高度只有可能大于0或者等于0,如果当天(上午,提升之前)的竹子高度大于0,则一定可以提升高度p,因为正向来看,如果打击之后竹子高度为正,则一定击打了高度p,如果当天的竹子高度等于0,此时确实不一定击打高度p了,但是我们按照高度p来拔高,用同样的拔高次数时,竹子的高度更高。
如下图,拔高一次时,红色线比黑色高,之后黑色拔高了第二次后超过红色线,但红色也拔高第二次后,就超过了黑色线。
这样,下次拔高的时间更晚(高度降到0的时间更长),这使得调度条件更宽松,并且,最终需要补充的拔高次数更少(最终高度更容易大于等于a_i)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long ;
const ll maxn=1e5+5;
ll a[maxn],h[maxn],h1[maxn];
ll n,k,p,m;
ll check(ll x){ //x:竹子的最终高度
priority_queue<pair<ll,ll>,vector<pair<ll,ll>>,greater<pair<ll,ll>>> q;
//计算初始时,每个竹子的最晚击打时间ddl
for(ll i=1;i<=n;i++){
h1[i]=x;
}
for(ll i=1;i<=n;i++){
ll ddl=h1[i]/a[i];
if(ddl>=m) continue;
q.push({ddl,i});
}
ll cnt=0; //击打次数
while(cnt<m*k){
if(q.empty()) break;
//计算当前天数day
ll day=cnt/k+1;
ll ddl=q.top().first,t=q.top().second;
q.pop();
if(ddl<day) return 0;
//进行击打
h1[t]+=p;
ll new_ddl=h1[t]/a[t];
if(new_ddl<m){
q.push({new_ddl,t});
}
cnt++;
}
//printf("敲击了%lld次\n",cnt);
if(!q.empty()) return 0;
//检查所有竹子的高度是否大于等于初始高度,否则需要提升
for(ll i=1;i<=n;i++){
h1[i]-=m*a[i];
if(h1[i]<h[i]){
ll dh=h[i]-h1[i];
ll need=(dh+p-1)/p;
cnt+=need;
if(cnt>m*k) return 0;
}
}
return 1;
}
int main()
{
ios::sync_with_stdio(0);cin.tie(0);
cin>>n>>m>>k>>p;
for(ll i=1;i<=n;i++) cin>>h[i]>>a[i];
//cout<<check(14)<<"\n";
ll l=0,r=1e13,ans=-1;
while(l<r){
ll mid=(l+r)>>1;
if(check(mid)){
ans=mid;
r=mid;
}
else l=mid+1;
}
cout<<ans<<"\n";
return 0;
}