李超线段树优化动态规划
3.李超线段树优化动态规划
3.1 李超线段树优化动态规划的基本方法
数据结构中先搞 李超线段树 的做法,因为代码短常数小。或者可以上 OI-WIKI 学习,它那个应该讲得比我好,而且还有图。
李超线段树适用于优化一些 \(1D / 1D\) 型的动态规划,而且它们的转移方程形似 \(f_i=p(\max or \min\{r(j)\times q(i)+s(j)\})\),其中 \(p(x)\) 是关于 \(x\) 的一次函数,\(q(i)\) 和 \(r(j)、s(j)\) 分别是关于 \(i\),\(j\) 的函数。
普遍做法是对于每个 \(i\) ,查询当前 \(x=q(i)\) 的纵坐标的最大值或最小值,然后将括号内的东西视为直线 \(y=r(j)\times x+s(j)\) 扔进李超线段树。
时间复杂度 \(O(n\log n)\)。
3.2 例题
例题3-1:[CEOI2017] Building Bridges
这一题可以设计状态:\(f_i=\min\limits_{0\le j<i}\Big\{f_j+(h_i-h_i)^2+h_j^2+s_{i-1}-s_j\Big\}\)。
拆开得到 \(f_j+h_j^2-s_j=2h_i\times h_j+f_i-h_i^2-s_{i-1}\)。
发现 \(f_j\) 不单调,无法直接维护凸包。
一个直观的想法是用平衡树维护,查询时在平衡树上二分。但是我不想写平衡树。
给出一个码量和时间复杂度都很小的李超线段树做法。
将式子变成 \(f_i=h_i^2+s_{i-1}+\min\limits_{0\le j<i}\Big\{-2h_j\times h_i+f_j+h_j^2-s_j\Big\}\)。
那么 \(\min\) 里的部分就可以看成对于每个 \(j\) 加入一条 \(
k=-2h_j\),\(b=f_j+h_j^2-s_j\) 的直线,并查询在 \(h_i\) 处最小的值。
这就是李超线段树的模板了,由于不用确定大区间,所以时间复杂度 \(O(n\log n)\)。
注意这里是求最小值,所以要将编号为 \(0\) 的直线的截距 \(b\) 初始化为无穷大。
点击查看代码
struct _{ll k,b;}p[100010];
ll s[4000040],h[100010],w[100010],f[100010],n,c=0;
inline ll cal(ll x,ll v){return p[x].k*v+p[x].b;}
void upd(ll x,ll l,ll r,ll u){
ll &v=s[x],mid=(l+r)>>1;
if(cal(u,mid)<cal(v,mid))swap(u,v);
if(cal(u,l)<cal(v,l))upd(x<<1,l,mid,u);
if(cal(u,r)<cal(v,r))upd(x<<1|1,mid+1,r,u);
}
ll query(ll x,ll l,ll r,ll v){
if(l==r)return cal(s[x],v);
ll mid=(l+r)>>1;
if(v<=mid)return min(cal(s[x],v),query(x<<1,l,mid,v));
else return min(cal(s[x],v),query(x<<1|1,mid+1,r,v));
}
int main(){
n=read();p[0].b=1e18;
for(ll i=1;i<=n;i++)h[i]=read();
for(ll i=1;i<=n;i++)w[i]=w[i-1]+read();
p[1]=_{-2*h[1],f[1]+h[1]*h[1]-w[1]};upd(1,0,1000001,1);
for(ll i=2;i<=n;i++){
f[i]=h[i]*h[i]+w[i-1]+query(1,0,1000001,h[i]);
p[i]=_{-2*h[i],f[i]+h[i]*h[i]-w[i]};upd(1,0,1000001,i);
}
cout<<f[n]<<'\n';
return 0;
}
例题3-3:[NOI2007] 货币兑换
这一题也可以用李超线段树做。首先我们可以发现每次操作要将金券卖完或是将钱花完买金券,而且每天可以操作无数次,那我们就可以把每一天结束的最大钱数记录下来,然后再记录它全部拿来买金券可以各卖多少个,然后转移就行了。
具体转移:每一天你可以不操作,所以 \(f_i=\max(f_i,f_{i-1})\)。或者是你可以再第 \(j\) 天将所有钱换成金券,然后一直等到第 \(i\) 天才操作,将所有金券换成人民币,所以 \(f_i=\max\limits_{1\le j<i}\{x_j\times a_i+y_j\times b_i\}\)。然后发现跟 \(i\) 和 \(j\) 都有关的项有两个怎么办?可以将它改成 \(f_i=b_i\times\max\limits_{1\le j<i}\{x_j\times c_i+y_j\}\),其中 \(c_i=\frac{a_i}{b_i}\)。
然后问题就变成了加入直线 \(k=x_j\),\(b=y_j\),然后查询每个 \(c_i\) 处的最大值。但是我们发现 \(c_i\) 不是整数怎么办?我们发现原本的李超线段树要求整点查询只是为了方便分区间,具体的值反而没有那么重要。所以我们可以将所有 \(c_i\) 离散化一下,这样不会影响 \(c_i\) 之间的大小关系,也不会影响答案,计算时用真实的 \(c_i\) 就好了,可以用李超线段树维护。时间复杂度 \(O(n\log n)\) 。
点击查看代码
struct _{double k,b;}p[100010];
int n,s[400010];
double f[100010],a[100010],b[100010],c[100010],d[100010],r[100010];
inline int cmp(double x,double y){
if(x-y>eps)return 1;
if(y-x>eps)return -1;
return 0;
}
inline double cal(int x,int v){return c[v]*p[x].k+p[x].b;}
void upd(int x,int l,int r,int u){
int &v=s[x],mid=(l+r)>>1;
if(cal(u,mid)>cal(v,mid))swap(u,v);
if(cmp(cal(u,l),cal(v,l))==1)upd(x<<1,l,mid,u);
if(cmp(cal(u,r),cal(v,r))==1)upd(x<<1|1,mid+1,r,u);
}
double query(int x,int l,int r,int v){
if(l==r)return cal(s[x],v);
int mid=(l+r)>>1;
if(v<=mid)return max(cal(s[x],v),query(x<<1,l,mid,v));
else return max(cal(s[x],v),query(x<<1|1,mid+1,r,v));
}
int main(){
n=read();scanf("%lf",&f[0]);
for(int i=1;i<=n;i++){scanf("%lf%lf%lf",&a[i],&b[i],&r[i]);d[i]=c[i]=a[i]/b[i];}
sort(c+1,c+n+1);
for(int i=1;i<=n;i++){
f[i]=max(f[i-1],b[i]*query(1,1,n,lower_bound(c+1,c+n+1,d[i])-c));
double g=a[i]*r[i]+b[i];p[i]=_{f[i]*r[i]/g,f[i]/g};upd(1,1,n,i);
}
printf("%.3f\n",f[n]);
return 0;
}
3.2习题
习题3-1:[SDOI2012]基站建设
点击查看代码
struct _{double k,b;}p[500010];
ll n,m,s[2000010],a[500010],v[500010],r[500010];
double f[500010],ans=1e18;
inline ll cmp(double x,double y){
if(x-y>eps)return 1;
if(y-x>eps)return -1;
return 0;
}
inline double cal(ll x,ll v){return p[x].k*a[v]+p[x].b;}
void upd(ll x,ll l,ll r,ll u){
ll &v=s[x],mid=(l+r)>>1;
if(cmp(cal(u,mid),cal(v,mid))<0)swap(u,v);
if(cmp(cal(u,l),cal(v,l))<0)upd(x<<1,l,mid,u);
if(cmp(cal(u,r),cal(v,r))<0)upd(x<<1|1,mid+1,r,u);
}
double query(ll x,ll l,ll r,ll v){
if(l==r)return cal(s[x],v);
ll mid=(l+r)>>1;
if(v<=mid)return min(cal(s[x],v),query(x<<1,l,mid,v));
else return min(cal(s[x],v),query(x<<1|1,mid+1,r,v));
}
int main(){
scanf("%lld%lld",&n,&m);p[0].b=1e18;
for(ll i=1;i<=n;i++){
scanf("%lld%lld%lld",&a[i],&r[i],&v[i]);
p[i]=_{1.0/(sqrt(r[i])*2),-a[i]/(sqrt(r[i])*2)};
}
p[1].b+=(f[1]=v[1]);upd(1,1,n,1);
for(ll i=2;i<=n;i++){p[i].b+=(f[i]=v[i]+query(1,1,n,i));upd(1,1,n,i);}
for(ll i=1;i<=n;i++)
if(a[i]+r[i]>=m)ans=min(ans,f[i]);
printf("%.3lf\n",ans);
return 0;
}