题解[CF932F Escape Through Leaf]
题意:给定一棵树,每个点有连个值 \(a,b\) 。
每次可以从一个点 \(x\) 到其子树内一点 \(y\) 的代价为 \(a_x\times b_y\)。
求每个到其子树内任一叶子结点的最小代价。
\(n,|a_i|,|b_i|\leq 10^5\)
设 \(f_x\) 为 \(x\) 到其子树内任一叶子节点的最小代价,则有 \(f_x=\mathop{\mathrm{Min}}\limits_{y\in subtree(x)}f_y+a_xb_y\)。
这是很明显的斜率优化,每个点的信息可视为 \((b_y,f_y)\) ,维护一个下凸壳,然后用斜率为 \(-a_x\) 的直线截得的截距最小值。这可以用李超树合并或凸包启发式合并无脑实现。
但事实上分治也可以在 \(O(n\log n)\) 的时间胜任。
将子树限制转化到 \(dfs\) 序上,则 \(dfn_x+1\leq dfn_y\leq dfn_x+sz_x-1\) 内的 \((b_y,f_y)\) 能贡献给 \(x\) 所需的点。
由于区间限制,且查询信息最小值不满足可加性,很自然想到线段树分治。
以 \(dfs\) 序为下标建出线段树。将线段树上在 \([dfn_x+1,dfn_x+sz_x-1]\) 的区间内的点打上对 \(x\) 的标记,表示这部分的点可以更新 \(f_x\) 。这些标记将是 \(O(n\log n)\) 个。
由于能更新 \(f_x\) 点的 \(dfs\) 序在 \(x\) 之后,需要以 \(\text{右左中}\) 的顺序遍历线段树。
类似于 \(\text{cdq}\) 分治,处理完右边与左边后将两边 \((b_y,f_y)\) 的点集按横坐标归并排序,再维护出些点集的下凸壳,然后找出线段树上这个点能更新的点 \(x\) 的斜率,在凸壳上查询。
为了让查询的斜率递增,可以在之前先按斜率排序再在线段树上打标记。
这能保证查询到某一点 \(x\) 时 \(f_x\) 能被所有能更新它的点更新。
归并排序,维护下凸壳,查询的总时间复杂度都是 \(O(n\log n)\) 的。
这可以说是线段树分治也可以说是 \(\text{cdq}\) 分治,二者并没有严格的界限。
本题卡精度,最好把 \(\text{long long}\) 换成 \(\text{double}\)。
代码:
#include<bits/stdc++.h>
#define ll double
using namespace std;
const int N=2e5+10,M=5e6+10;
const ll inf=1e18;char ch;
int n,m,x,y,l_,r_,tt;bool rf;
inline void read(int &x){
x=0;ch=getchar();while(ch<47)ch=getchar();
while(ch>47)x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
}
inline void read_(int &x){
rf=x=0;ch=getchar();while((ch<47)&&(ch^'-'))ch=getchar();
if(ch=='-')rf=1,ch=getchar();
while(ch>47)x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
if(rf)x=-x;
}
int nextn[N],to[N],h[N],edg;
inline void add(int x,int y){to[++edg]=y,nextn[edg]=h[x],h[x]=edg;}
int nextnq[M],toq[M],hq[N<<2],edgq;
inline void addq(int x,int y){toq[++edgq]=y,nextnq[edgq]=hq[x],hq[x]=edgq;}
int dfn[N],sz[N],rev[N],kk[N];ll f[N];
struct node{int i,a,b;bool operator <(const node &x)const{return a<x.a;}}s[N];
struct point{
ll x,y;point()=default;point(ll _x,ll _y):x(_x),y(_y){}
bool operator <=(const point &a)const{return y*a.x<=x*a.y;}
point operator -(const point &a)const{return point(x-a.x,y-a.y);}
}p[N],tmp;
struct stp{//segmenttree_partition
ll x,y;stp()=default;stp(ll _x,ll _y):x(_x),y(_y){}
bool operator <(const stp &a)const{return x!=a.x?x<a.x:y>a.y;}
}q[N],q0[N];
void init(int x,int anc){
int i,y;sz[x]=1;rev[dfn[x]=++tt]=x;
for(i=h[x];y=to[i];i=nextn[i])if(y^anc)init(y,x),sz[x]+=sz[y];
}
#define ls k<<1
#define rs k<<1|1
void add(int k,int l,int r,int x,int y,int i){
if(x<=l&&r<=y)addq(k,i);
else {
int mid=(l+r)>>1;
if(x<=mid)add(ls,l,mid,x,y,i);
if(mid<y)add(rs,mid+1,r,x,y,i);
}
}//打标记,这里用加边实现
void solve(int k,int l,int r){
int i;
if(l^r){
int mid=(l+r)>>1;
solve(rs,mid+1,r);
solve(ls,l,mid);
merge(q+l,q+mid+1,q+mid+1,q+r+1,q0+l);
for(i=l;i<=r;++i)q[i]=q0[i];
}
else q[l].y=f[rev[l]];//区间为dfs序,对应树上点rev[l]
r_=0;
for(i=l;i<=r;++i){
tmp=point(q[i].x,q[i].y);
while(r_>1&&(tmp-p[r_])<=(p[r_]-p[r_-1]))--r_;
p[++r_]=tmp;
}//维护区间点集的下凸壳
l_=1;
for(i=hq[k];y=toq[i];i=nextnq[i]){
tmp=point(1,kk[y]);
while(l_<r_&&(p[l_+1]-p[l_])<=tmp)++l_;
f[y]=min(f[y],p[l_].y-p[l_].x*tmp.y);
}//更新f
}
main(){
read(n);register int i;
for(i=1;i<=n;++i)read_(s[i].a),kk[i]=-s[i].a;
for(i=1;i<=n;++i)read_(s[i].b),s[i].i=i,f[i]=inf;
for(i=1;i^n;++i)read(x),read(y),add(x,y),add(y,x);
init(1,0);
for(i=1;x=rev[i],i<=n;++i)q[i]=stp(s[x].b,0);
sort(s+1,s+n+1);//排序保证线段树上查询时斜率递增
for(i=1;x=s[i].i,i<=n;++i){
if(sz[x]^1)add(1,1,n,dfn[x]+1,dfn[x]+sz[x]-1,x);
else f[x]=0;
}
solve(1,1,n);
for(i=1;i<=n;++i)printf("%.0lf ",f[i]);
}