[NOI2014] 购票
程序员文章站
2022-09-28 07:58:42
设f\[x\]为从x跑到1的最小花费,基本转移如下 $$ \left\{ \begin{aligned} f[1]&=0\\ f "x]&=\min_{dep[x] dep[y]\le l[x]} f[y]+p[x" +q[x]\\ &=p[x]dep[x]+q[x]+\min_{dep[x] de ......
设f[x]为从x跑到1的最小花费,基本转移如下
\[
\left\{
\begin{aligned}
f[1]&=0\\
f[x]&=\min_{dep[x]-dep[y]\le l[x]} f[y]+p[x](dep[x]-dep[y])+q[x]\\
&=p[x]dep[x]+q[x]+\min_{dep[x]-dep[y]\le l[x]} f[y]-p[x]dep[y]
\end{aligned}
\right.
\]
有斜率优化的影子
\[
f[x]=p[x]dep[x]+q[x]+(f[y]-p[x]dep[y])\\
f[y]=\underline{p[x]}dep[y]+\underline{f[x]-(p[x]dep[x]+q[x])}
\]
于是在dp的过程中需要维护根到x父亲的一条链,二分求出被x接受的链的后缀,需要得到后缀上点(dep,f)的凸包,然后二分斜率求答案 。压入点、弹出点、计算后缀的凸包……显然这时间爆炸。
注意到这个后缀是可以分成若干段分别计算的,考虑对栈优雅地分块?时间复杂度\(o(n\sqrt n\log n)\)不太友好。
可以使用带撤销的二进制分组来肝,即用线段树来做一些完整区间(为方便处理,提前把树处理成满二叉树)的凸包,除了每层的最后一的满区间。
(其实这根树链剖分的做法已经很相似了啊)
#include <bits/stdc++.h> #define ll long long using namespace std; const int n=2e5+10; const int m=n*8; int n; int head[n],to[n],lst[n]; int fa[n][20],dep[n],mxd; ll wit[n],dis[n],p[n],q[n],l[n],f[n]; int len=1,id[n*2]; namespace sgt { int bel[m],num[m],siz[m],cnt[25],tmp[n]; vector<int> tre[m]; bool hav[m]; #define ls (x<<1) #define rs (x<<1|1) void build(int x,int l,int r) { num[x]=++tmp[bel[x]=id[r-l+1]]; if(l==r) {tre[x].resize(1); return;} int mid=(l+r)>>1; build(ls,l,mid); build(rs,mid+1,r); } void upd(int x,int l,int r,int p,int k) { if(++siz[x]==r-l+1) ++cnt[bel[x]]; if(l==r) { tre[x][0]=k; hav[x]=true; return; } int mid=(l+r)>>1; if(p<=mid) upd(ls,l,mid,p,k); else upd(rs,mid+1,r,p,k); } void del(int x,int l,int r,int p) { hav[x]=false; if(--siz[x]==r-l) --cnt[bel[x]]; if(l==r) return; int mid=(l+r)>>1; if(p<=mid) del(ls,l,mid,p); else del(rs,mid+1,r,p); } #define x(i) dis[i] #define y(i) f[i] inline ll calc(int y,int x) {return f[y]+p[x]*(dis[x]-dis[y])+q[x];} inline bool cross(ll x1,ll y1,ll x2,ll y2) {return 1.0*x1*y2>=1.0*x2*y1;} inline void merge(vector<int> &a,vector<int> &l,vector<int> &r) { int top=0; for(auto i:l) tmp[++top]=i; for(auto i:r) { while(top>1&&cross(x(i)-x(tmp[top-1]),y(i)-y(tmp[top-1]), // 凸壳部分 x(tmp[top])-x(tmp[top-1]),y(tmp[top])-y(tmp[top-1]))) top--; tmp[++top]=i; } a.resize(top); for(int i=1; i<=top; ++i) a[i-1]=tmp[i]; } void work(int x,int l,int r) { if(hav[x]) return; hav[x]=true; int mid=(l+r)>>1; work(ls,l,mid); work(rs,mid+1,r); merge(tre[x],tre[ls],tre[rs]); } inline ll calc(vector<int>& a,int x) { int l=0,r=a.size()-2,mid; ll ans=calc(a.back(),x),k=p[x]; while(l<=r) { mid=(l+r)>>1; ans=min(ans,calc(a[mid],x)); if(cross(1,k,x(a[mid+1])-x(a[mid]),y(a[mid+1])-y(a[mid]))) r=mid-1; else l=mid+1; } return ans; } ll query(int x,int l,int r,int l,int r,int x) { if(l<=l&&r<=r) { if(!hav[x]&&num[x]<cnt[bel[x]]) work(x,l,r); if(hav[x]) return calc(tre[x],x); } int mid=(l+r)>>1; ll ans=1e18; if(l<=mid) ans=query(ls,l,mid,l,r,x); if(mid<r) ans=min(ans,query(rs,mid+1,r,l,r,x)); return ans; } } inline void add_edge(int x,int y,int w) { static int cnt=0; to[++cnt]=y; wit[cnt]=w; lst[cnt]=head[x]; head[x]=cnt; } inline ll calc(int x) { int z=fa[x][0],y=x; for(int i=19; ~i; --i) if(fa[y][i]&&dis[x]-dis[fa[y][i]]<=l[x]) y=fa[y][i]; return sgt::query(1,1,len,dep[y],dep[z],x); } void pre(int x) { mxd=max(mxd,dep[x]=dep[fa[x][0]]+1); for(int i=1; (1<<i)<=dep[x]; ++i) fa[x][i]=fa[fa[x][i-1]][i-1]; for(int i=head[x]; i; i=lst[i]) { dis[to[i]]=dis[x]+wit[i]; pre(to[i]); } } void dfs(int x) { if(x!=1) f[x]=calc(x); sgt::upd(1,1,len,dep[x],x); for(int i=head[x]; i; i=lst[i]) dfs(to[i]); sgt::del(1,1,len,dep[x]); } int main() { ll val; scanf("%d%lld",&n,&val); for(int i=2; i<=n; ++i) { scanf("%d%lld%lld%lld%lld",&fa[i][0],&val,p+i,q+i,l+i); add_edge(fa[i][0],i,val); } pre(1); for(int i=1; len<mxd; len<<=1,++i) id[len]=i; sgt::build(1,1,len); dfs(1); for(int i=2; i<=n; ++i) printf("%lld\n",f[i]); return 0; }
代码大规模借鉴@i207m。
上一篇: 自动格式化磁盘并挂载