因為是“最小化最大值”,容易想到二分答案。設二分值為\(\text{mid}\),我們要判斷是否能使最終所有竹子的高度都\(\leq\text{mid}\)。如果從前往后安排每一天,會發現很難找到一種固定的貪心策略,來確定當天砍哪些竹子。
換個角度。考慮最后一天,所有竹子會長高\(a_i\)米。那么在最后一天開始生長之前,第\(i\)個竹子不能高於\(\text{mid}-a_i\)米。
- 如果我們在最后一天不砍第\(i\)個竹子,那么在倒數第二天開始之前,第\(i\)個竹子不能高於\(\text{mid}-2a_i\)米。
- 如果在最后一天砍了第\(i\)個竹子,設砍了\(x\)次,那么在倒數第二天開始之前,第\(i\)個竹子不能高於\(\text{mid}+p\cdot x-2a_i\)米。
以此類推。按照這種“時光倒流”的想法:每個竹子初始高度為\(\text{mid}\),每天高度會減少\(a_i\),我們每用一次砍伐機會可以使其高度增加\(p\)。這里的“高度”,代表的實際含義是:如果之后按照我們確定的這種方式砍伐(因為我們在時光倒流,所以之后的砍伐方式都已經確定了),那么(在正向時間中)當前時刻這根竹子的高度應不高於多少,才能使得最終高度不超過\(\text{mid}\)。根據這個含義,我們要做的就是保證在整個時光倒流的過程中,每根竹子的“高度”始終不能為負。因為一但“高度”為負,意味着要求(正向時間中)高度不得高於一個負數,這顯然是不可能的。
關於“時光倒流”,可以結合下面這張圖理解。其中藍色是正向的時間,也就是實際上的高度。綠色是我們時光倒流,確定出的“高度”:即,每個時刻,實際高度不得高於多少。

於是,通過時光倒流,問題轉化為:每個竹子初始高度為\(\text{mid}\),每天高度會減少\(a_i\),我們每用一次砍伐機會可以使其高度增加\(p\)。在\(m\)天中要保證所有竹子高度始終非負,且\(m\)天結束后每個竹子高度要\(\geq h_i\)。
這樣轉化的好處是,我們避免了在正向時間中,一次砍伐減少的高度不足\(p\)的問題;轉化后,每次砍伐操作一定能使當前竹子高度增加\(p\),與初始高度無關。
轉化后,這是經典的貪心問題(CF1132D Stressful Training)。我們計算出每個竹子,在不被砍伐的情況下,每天減少\(a_i\)米,最多能堅持幾天。以這個天數作為關鍵字,把所有堅持不足\(m\)天的竹子扔進一個小根堆里。依次完成\(k\cdot m\)次砍伐,每次取出堆頂。如果當前砍伐所在的天數已經大於堆頂能堅持的天數,則直接\(\texttt{return false}\),把\(\text{mid}\)調大。否則,對堆頂砍一刀(使其高度增加\(p\))。如果高度增加后,它能堅持到\(m\)天之后,就不用再放進堆里了;否則更新它能堅持到的天數,然后放回堆中。
最后,用剩余的砍伐次數,嘗試把所有竹子補到\(h_i\)即可。
時間復雜度\(O((n+k\cdot m)\cdot \log n\cdot \log\inf)\)。其中\(\log n\)來自堆,\(\log\inf\)來自二分答案,這里取\(\inf=\max(h_i+m\cdot a_i)\leq5001\cdot 10^9\)。
參考代碼:
//problem:CF506C
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int MAXN=1e5,MAXM=5000,MAXA=1e9;
int n,m,K,P,h[MAXN+5],a[MAXN+5];
bool check(ll mid){
priority_queue<pll,vector<pll>,greater<pll> >q;//greater -> 小的在前
static ll c[MAXN+5];
for(int i=1;i<=n;++i){
c[i]=mid;
if(mid-(ll)m*a[i]<0){
q.push(mk(mid/a[i],i));//(最多能維持多少天, 編號)
}
}
int cnt=0;
while(cnt<=K*m){
if(q.empty())break;
pll t=q.top();q.pop();
int d=cnt/K+1;
if(t.fi<d)return false;
c[t.se]+=P;
++cnt;
if(c[t.se]-(ll)m*a[t.se]<0){
q.push(mk(c[t.se]/a[t.se],t.se));
}
}
if(cnt>K*m)return false;
for(int i=1;i<=n;++i){
if(c[i]-(ll)m*a[i]>=h[i])continue;
ll gap=h[i]-(c[i]-(ll)m*a[i]);
ll need=gap/P+(gap%P!=0);
if(cnt+need>K*m)return false;
cnt+=need;
}
return true;
}
int main() {
cin>>n>>m>>K>>P;
ll l=0,r=(ll)(MAXM+1)*MAXA;
for(int i=1;i<=n;++i){
cin>>h[i]>>a[i];
l=max(l,(ll)a[i]);
}
while(l<r){
ll mid=(l+r)>>1;
if(check(mid))r=mid;
else l=mid+1;
}
cout<<l<<endl;
return 0;
}
