UOJ 7 NOI2014 购票
题意:给一棵树计算一下各个点在距离限制下以一定的费用公式通过不停地到祖先最后到达一号点的最小花费。
第一种做法:线段树维护带修凸壳。显然的,这个公式计算是p*x+q 所以肯定和斜率有关系。然后这题的dp方程也是非常显然的,dp[x]=min(dp[y]+(dis[x]-dis[y])*p[x]+q[x]) ,其中y是x的祖先,并且dis[x]-dis[y]<=l[x]。然后这个式子稍微划一下就能推出单调性,以及以(dis[x],dp[x])这样子的点的形式,求最小值那么下凸壳。很自然地想到这个是个从上往下的dp,也就是x这个点到祖先的这条链会对x的答案产生影响。那么继续非常直觉的想法就是dfs这颗树每次维护这条链的凸壳。然而这个时候直接维护单调栈是有问题的,因为距离的限制,导致有些本来不会是单调栈中的点在某次询问时可能是最优的点。而其实我们每次对于一个点的答案因为距离的限制也就成为了一段区间,那么也就是要维护这条链上的区间,自然地想到线段树,对于每个线段树的节点开一个凸包,这样单次修改最多修改logn个节点,对于插入一个点要二分一下最左边满足条件的位置,然后记录一下历史版本,即之前该位置的值以及凸壳的siz。然后dfs完这个点后就可以O(1)还原了。就做到了O(logn)插入,O(1)还原。然后第一次写这种题的我发现这个写起来很奇怪,因为大多数时候二分的log是跑不满的。然后更新dp时只要框出区间,然后在线段树上查询这个区间就好了。复杂度O(n*logn*logn)。
#include<bits/stdc++.h> #define pb push_back #define mp make_pair #define fi first #define se second #define ls (x<<1) #define rs (x<<1|1) #define db double #define all(x) x.begin(),x.end() #define ll long long #define pll pair<ll,ll> #define pii pair<int,int> #define eps 1e-9 #define inf 0x3f3f3f3f using namespace std; const int maxn=2e5+100; inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,t; int d_top; ll dp[maxn],s[maxn],p[maxn],q[maxn],l[maxn],d_sta[maxn],d[maxn]; vector<int>sta[maxn*4]; vector<int>vec[maxn]; int top[maxn*4],pre[maxn][20],last[maxn][20],seg_d[maxn*4],fa[maxn],id[maxn]; void build(int x,int l,int r) { seg_d[x]=seg_d[x>>1]+1; sta[x].resize(r-l+3); if(l==r) return; int mid=(l+r)>>1; build(ls,l,mid); build(rs,mid+1,r); } bool xl(int i,int j,int k) { return 1.0*(dp[j]-dp[i])*(d[k]-d[j])>=1.0*(dp[k]-dp[j])*(d[j]-d[i]); } int find_pos(int x,int i) { if(!top[x])return 1; int l=1,r=top[x]-1,res=top[x]+1; while(l<=r) { int mid=(l+r)>>1; if(xl(sta[x][mid],sta[x][mid+1],i)){ r=mid-1;res=mid+1; } else l=mid+1; }return res; } void push(int x,int i) { int pl=find_pos(x,i); last[i][seg_d[x]]=top[x]; pre[i][seg_d[x]]=sta[x][pl]; top[x]=pl; sta[x][pl]=i; } void back(int x) { int i=sta[x][top[x]]; sta[x][top[x]]=pre[i][seg_d[x]]; top[x]=last[i][seg_d[x]]; } ll cal(int x,int y) { return dp[x]+(d[y]-d[x])*p[y]+q[y]; } bool cmp(int x,int y,ll val) { return dp[y]-dp[x]<=val*(d[y]-d[x]); } ll ask(int x,int pl) { int l=1,r=top[x]-1,res=1; while(l<=r) { int mid=(l+r)>>1; if(cmp(sta[x][mid],sta[x][mid+1],p[pl])){ l=mid+1;res=mid+1; } else{ r=mid-1; } } return cal(sta[x][res],pl); } void insert(int x,int l,int r,int pl,int o) { if(o)push(x,id[pl]); else back(x); if(l==r)return; int mid=(l+r)>>1; if(pl<=mid)insert(ls,l,mid,pl,o); else insert(rs,mid+1,r,pl,o); } ll query(int x,int l,int r,int L,int R) { if(L<=l&&r<=R)return ask(x,id[R+1]); int mid=(l+r)>>1; ll res=4e18; if(L<=mid)res=query(ls,l,mid,L,R); if(R>mid)res=min(res,query(rs,mid+1,r,L,R)); return res; } void dfs(int x) { d_sta[++d_top]=d[x]=d[fa[x]]+s[x];id[d_top]=x; if(x!=1){ int st=lower_bound(d_sta+1,d_sta+d_top+1,d[x]-l[x])-d_sta; dp[x]=query(1,1,n,st,d_top-1); } insert(1,1,n,d_top,1); for(int i=0;i<vec[x].size();i++){ dfs(vec[x][i]); } insert(1,1,n,d_top,0); d_top--; } int main() { n=read();t=read(); for(int i=2;i<=n;i++) { fa[i]=read();s[i]=read();p[i]=read();q[i]=read();l[i]=read(); vec[fa[i]].pb(i); } build(1,1,n); dfs(1); //cout<<222<<"\n"; for(int i=2;i<=n;i++) { cout<<dp[i]<<"\n"; } }
第二种做法:CDQ分治+点分治
在第一种做法中已经提到,就是算这条链对这个点的影响,而在链的时候,也就是一个区间对于这个点的影响,这种问题我们可以用CDQ分治加以解决(当然也可以平衡树...),然后应用到树上我们就用到了点分治这个东西来控制复杂度。其实我是写过点分治的题的。。就是先找下重心G,然后先递归处理掉fa[G]这颗子树的情况(因为G是真正的根)。处理完了之后就能计算这条链对于G的子树的贡献,把G的子树中满足条件的点拿出来,并按照最上方的点深度从高到低排序(倒着做),每次加入一个点维护凸壳,然后二分找到最优的位置,更新答案。注意这时要把这个点的最上方的点改成在dfs3中的新的top,因为那部分答案已经计算过了,相当于l~mid已经结束,剩下的是mid~r区间里左边的点对它的贡献(在G的子树中),否则复杂度也是不对的。点分治好像更快一些,但也是O(n*logn*logn),点分治一层log,二分一层log。
#include<bits/stdc++.h> #define pb push_back #define mp make_pair #define fi first #define se second #define ls (x<<1) #define rs (x<<1|1) #define db double #define all(x) x.begin(),x.end() #define ll long long #define ldb long double #define pll pair<ll,ll> #define pii pair<int,int> #define eps 1e-9 #define inf (((1ll<<62)-1)<<1)+1 using namespace std; const int maxn=2e5+100; inline ll read() { ll x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int top[maxn],siz[maxn],fa[maxn][20],bel[maxn],st[maxn],vis[maxn],maxx[maxn],p[maxn]; int cnt,ntop,tot; int n,T; int fir[maxn],nxt[maxn*2],to[maxn*2]; ll f[maxn],s[maxn],q[maxn],l[maxn],dep[maxn],dis[maxn],dp[maxn]; bool cmp(const int &a,const int &b) { return dep[top[a]]>dep[top[b]]; } ll Max(ll a,ll b){ return a>b?a:b; } ll Min(ll a,ll b) { return a>b?b:a; } struct node { ll x,y; }no[maxn]; void add_e(int x,int y) { ++cnt;nxt[cnt]=fir[x];to[cnt]=y;fir[x]=cnt; } void dfs(int x) { dep[x]=dep[fa[x][0]]+1; for(int i=0;i<17;i++)fa[x][i+1]=fa[fa[x][i]][i]; for(int i=fir[x];i;i=nxt[i]){ int tt=to[i];dis[tt]=dis[x]+s[tt];dfs(tt); } } int bz(int x,ll lim) { int tt=x; for(int i=17;i>=0;i--) { if(fa[x][i]&&dis[tt]-dis[fa[x][i]]<=lim) x=fa[x][i]; } return x; } void dfs1(int x) { siz[x]=1;maxx[x]=0; for(int i=fir[x];i;i=nxt[i]){ if(!vis[to[i]]) { dfs1(to[i]);siz[x]+=siz[to[i]];maxx[x]=Max(maxx[x],siz[to[i]]); } } } void dfs2(int rt,int x,int &G) { if(Max(maxx[x],siz[rt]-siz[x])<Max(maxx[G],siz[rt]-siz[G]))G=x; for(int i=fir[x];i;i=nxt[i]){if(!vis[to[i]])dfs2(rt,to[i],G);} } int calg(int x) { int G=x; dfs1(x);dfs2(x,x,G);return G; } double xl(node x,node y) { return 1.0*(y.y-x.y)/(1.0*(y.x-x.x)); } void ins(node x) { while(ntop>1&&xl(x,no[ntop])>=xl(no[ntop],no[ntop-1]))ntop--; no[++ntop]=x; } ll query(ll val) { int l=1,r=ntop-1; while(l<=r) { int mid=(l+r)>>1; if(no[mid+1].y-no[mid+1].x*val-(no[mid].y-no[mid].x*val)>=0){ r=mid-1; }else{ l=mid+1; } } return no[l].y-no[l].x*val; } void dfs3(int rt,int x,int maxd) { if(dep[top[x]]<=maxd){ st[++tot]=x;bel[tot]=rt; } for(int i=fir[x];i;i=nxt[i])if(!vis[to[i]])dfs3(rt,to[i],maxd); } void dfs4(int x) { int G=calg(x);vis[G]=1; if(x!=G) { dfs4(x); int v=fa[G][0]; while(dep[v]>=dep[x]&&dep[v]>=dep[top[G]]) { dp[G]=Min(dp[G],dp[v]+(dis[G]-dis[v])*p[G]+q[G]);v=fa[v][0]; } } tot=ntop=0; for(int i=fir[G];i;i=nxt[i]) { if(!vis[to[i]]) dfs3(to[i],to[i],dep[G]); } sort(st+1,st+1+tot,cmp); int v=G; for(int i=1;i<=tot;i++) { int tt=st[i]; while(dep[v]>=dep[top[tt]]) { ins((node){dis[v],dp[v]});v=fa[v][0]; } dp[tt]=Min(dp[tt],query(p[tt])+dis[tt]*p[tt]+q[tt]); top[tt]=bel[i]; } for(int i=fir[G];i;i=nxt[i])if(!vis[to[i]])dfs4(to[i]); } int main() { n=read();T=read(); for(int i=2;i<=n;i++) { fa[i][0]=read();s[i]=read();p[i]=read();q[i]=read();l[i]=read();dp[i]=inf; add_e(fa[i][0],i); } dfs(1); for(int i=1;i<=n;i++)top[i]=bz(i,l[i]);//,cout<<top[i]<<"\n"; //cout<<dep[fa[1][0]]<<"\n"; dfs4(1); for(int i=2;i<=n;i++){printf("%lld\n",dp[i]);} }
PS:其实这两个题要是不看别人的代码我恐怕要写到天荒地老,实在太菜了,当然比起去年在这种题面前想一下的能力都没有,现在起码那个线段树的做法还是比较显然的,点分治稍微磨炼一下估计更显然。怎么说呢,最近陆陆续续写了8、9个斜率优化的题,花了很多时间却也不能说很懂了。就CDQ分治其实最特别的代码还是她论文那个题,感觉比起什么陌上花开有意义多了。而这个题别人列举了7、8种做法,反正我也就这么稍微口糊一下,网上题解一大堆,但是我还有毅力去写这些题本身也是值得纪念的事情啊。 剩下的感触就在“有点xx”里写了。