不會單調佇列,又看到古神拿李超樹切斜率最佳化特別順手,遂來學了一下,確實挺好用。
李超樹
一種用來快速維護有關線段的資訊的資料結構,一般是 \(O(n\mathtt{log}^{\mathbf{2}}n)\) 的,在斜率最佳化題中用到的操作是 \(O(n\mathtt{log}n)\) 的(沒有斜率最佳化題會卡 \(O(n\mathtt{log}n)\) 吧)。
原理是遞迴來判斷線段在一段區間內的優劣,這個在做題時不用理會。只需要記住板子,然後把斜截式方程代到裡面,按具體題目微調一下,然後就可以了。OIer只需要求出 dp 方程,轉化成斜截式,代到李超樹裡就行了,可是李超樹要考慮的事情就比較多了。
拿一道P5785 任務安排的程式碼當例子,按這個模板寫就行。
點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ls t[now].l
#define rs t[now].r
const ll N=3*114514,M=1919810,inf=1145141919810;
ll n,s,ti[N],f[N],dp[N],id[N];
ll rt,cnt;
struct xx{
ll val,id;
}a[N];
bool cmp(xx x,xx y){
return x.val<y.val;
}
struct tree{
ll k,b;
ll l,r;
}t[4*N];
ll get(ll id,ll x){
return t[id].k*x+t[id].b;
}
void pushdown(ll &now,ll l,ll r,ll k,ll b){
if(!now) now=++cnt; //這裡不一定
ll mid=(l+r)>>1;
ll la=get(now,a[l].val),ra=get(now,a[r].val);
ll lx=k*a[l].val+b,rx=k*a[r].val+b;
ll mid1=get(now,a[mid].val),mid2=k*a[mid].val+b;
if(la<=lx&&ra<=rx) return;
if(la>lx&&ra>rx){
t[now].k=k,t[now].b=b;
return;
}
if(mid1>mid2){
swap(la,lx),swap(ra,rx);
swap(t[now].k,k),swap(t[now].b,b);
}
if(la>lx) pushdown(ls,l,mid,k,b);
if(ra>rx) pushdown(rs,mid+1,r,k,b);
}
ll query(ll now,ll l,ll r,ll tar){
if(!now) return inf;
ll mid=(l+r)>>1,ans=get(now,a[tar].val);
if(tar<=mid) ans=min(ans,query(ls,l,mid,tar));
else ans=min(ans,query(rs,mid+1,r,tar));
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>s;
for(int i=1;i<=n;++i){
cin>>ti[i]>>f[i];
ti[i]+=ti[i-1],f[i]+=f[i-1];
a[i].val=ti[i]+s,a[i].id=i;
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;++i) id[a[i].id]=i;
for(int i=1;i<=n;++i){
dp[i]=query(rt,1,n,id[i])+f[n]*s+ti[i]*f[i];
pushdown(rt,1,n,-f[i],dp[i]);
}
cout<<dp[n];
return 0;
}
斜率最佳化
題一般都是能很簡單求出 \(O(n^{\mathbf{2]})\) 的轉移式,但是複雜度要求在 \(O(n\mathtt{log}n)\) 以內的,這個時候可以考慮斜率最佳化。因為使用的是李超樹,所以斜截式就要列成 \(y=kx+b\) 的形式。一般來說,只與 \(i\) 相關的項放到 \(y\),至於 \(j\) 相關的項和常數項放在 \(b\),同時和 \(i,j\) 相關的放在 \(kx\)。然後有些題會有些特殊操作,需要微調。
鴿