购票(NOI2014)
购票(NOI2014)
题目描述
给出 \(n\) 个点带边权,根节点为 \(1\) 的树,从一个点 \(x\) 跳到它的一个祖先 \(f\) 所需要花的时间为 \(p[x]*dis(x,f)+q[x]\), \(dis(x,f)\) 表示 \(x\) 到祖先 \(f\) 的距离。同时,一个点跳一次不能跳到距离它超过 \(l[x]\) 的祖先。要求求出所有非根节点跳到根节点 \(1\) 的最短时间。
思路
这题的 \(DP\) 转移应该是最好想到的:
\(dp[x]=min(dp[f]+q[x]+(dis[x]-dis[f])*p[x])\) 其中 \(f\) 为 \(x\) 的祖先。
知道这个后,就可以设计一个 \(O(n^2)\) 的算法:\(dfs\) 一遍,每次枚举它的所有祖先,求出最小值即可。不多说
然后让我们再看看这个式子,可以这样写:
\((-dis[f]*p[x]+dp[f])+q[x]+dis[x]*p[x]\)
可见只有括号里的这个值是因祖先的不同而改变,再仔细一看这个括号中的式子有没有像什么东西?\(-x*k+y\) ,没错接下来就是斜率优化了!
但和一般的斜率优化不同的就是,我们这里的 \(k\) 并非单调,而且随着树的回溯,我们的祖先序列还会发生改变,更别说更别说这里还有距离限制呢!看来还是比较复杂的。
要不我们先不考虑没有距离限制的情况,此时我们需要做的就是维护一个单调栈,栈中的点是当前点的祖先,同时我们还不能像一般的斜率优化一样线性操作,因为栈中的点还需要在回溯时要恢复回原来的样子,这样就不好操作了。
那么既然线性复杂度不行,那么就 \(log\) 把,每次用二分找出每次要放入一个点时要放在的位置以及每次答案要从哪个祖先中转移。这些都还是比较好实现的:
int bs_ans(int k){//查询答案要从哪个祖先转移
int l=0,r=stk0-1,ans=stk0;
while(l<=r){
int mid=(l+r)>>1;
if(1.0*(stk[mid].y-stk[mid+1].y)/(stk[mid].x-stk[mid+1].x)>=1.0*k)ans=mid,r=mid-1;
else l=mid+1;
}
return ans;
}
int bs_pos(pll A){
int l=1,r=stk0,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(1.0*(stk[mid].y-stk[mid-1].y)/(stk[mid].x-stk[mid-1].x)<=1.0*(A.y-stk[mid].y)/(A.x-stk[mid].x))
ans=mid,l=mid+1;
else r=mid-1;
}
return ans+1;//注意,由于这是要插入的位置的后一个,所以插入位置为ans+1
}
既然知道如何查询和插入点后,接下来就是如何维护这个 \(stk[]\) 栈数组了,每次插入一个点时,它的 \(top\) 就变成了该点的位置,该位置原本的值 \(val\) 也会改变。所以回溯前,我们要把原来的 \(top\) 以及 \(val\) 记下来,回溯时就把它对应地赋值回去,因为递归结束时栈已经回溯到递归前的状态了,所以直接赋值回去即可,那么最终代码也就有了:
struct Pt_2{
pll stk[N];int stk0;
int bs_ans(int k){
int l=0,r=stk0-1,ans=stk0;
while(l<=r){
int mid=(l+r)>>1;
if(1.0*(stk[mid].y-stk[mid+1].y)/(stk[mid].x-stk[mid+1].x)>=1.0*k)ans=mid,r=mid-1;
else l=mid+1;
}
return ans;
}
int bs_pos(pll A){
int l=1,r=stk0,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(1.0*(stk[mid].y-stk[mid-1].y)/(stk[mid].x-stk[mid-1].x)<=1.0*(A.y-stk[mid].y)/(A.x-stk[mid].x))
ans=mid,l=mid+1;
else r=mid-1;
}
return ans;
}
void dfs(int x,int f){
pll lst;int r;
if(x!=1){
int p=bs_ans(A[x]);
dp[x]=stk[p].y+B[x]+(dis[x]-stk[p].x)*A[x];
pll P=(pll){dis[x],dp[x]};
p=bs_pos(P);r=stk0,stk0=p;//r是记录原本的top
lst=stk[++stk0],stk[stk0]=P;//注意是插在p的后一位,我二分的返回值并未+1
}
EOR(G,i,x){
int v=G.to[i];
if(v==f)continue;
dis[v]=dis[x]+G.len[i];
dfs(v,x);
}
if(x!=1){//回溯操作
stk[stk0]=lst;
stk0=r;
}
}
void solve(){
dfs(1,0);
FOR(i,2,n)pf("%lld\n",dp[i]);
}
}Pt_2;
最后就是考虑距离了。。。。
这个思维难度有点大,就直接点出了!数据结构维护区间,比如线段树。
我们在每个区间 \([l,r]\) 上都建立一个栈,可以单独开(如果内存控制得住的话),或者像我一样,开一个大数组,把区间逐个分配出去,然后记录每个栈的左右指针即可,这样总共是 \(nlogn\) 个 \(int\) 的复杂度,这样,每次将一个点作为祖先加入时,平均更新到 \(nlogn\) 个区间,然后对于每个要更新区间,我们都像上面的操作一样:二分找到该点因被放在的位置,同时回溯记录(记录方法很多,个人是根据位置+深度开数组储存的,详细见代码)。
然后递归时,在维护一个所有祖先的普通栈,由于它们的 \(dis\) 是单调递增的,所以每次都可以 \(logn\) 求出每次满足距离限制条件的区间 \([pos,top]\) ,然后便可进行线段树区间查询答案,查询过程中没找到对应区间时,就用同样的答案二分找到最优点更新到答案中。
最后就是回溯,我们在前面已经把所有的原值都记录下来了,那么只要在在线段树上递归到该点,把值恢复回去即可。
程序最终复杂度为 \(O(n(logn)^2)\)
虽然我描述的比较短(懒得打了。。),但其实细节还是很多的,详细内容可以见代码
代码
#include<bits/stdc++.h>
#define FOR(i,l,r) for(int i=(l),i##END=(r);i<=i##END;i++)
#define DOR(i,r,l) for(int i=(r),i##END=(l);i>=i##END;i--)
#define loop(i,n) for(int i=0,i##END=(n);i<i##END;i++)
#define sf scanf
#define pf printf
#define pb push_back
#define mms(a,x) memset(a,x,sizeof a)
#define pll pair<ll,ll>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef long double db;
template<typename A,typename B>inline void chkmax(A &x,const B y){if(x<y)x=y;}
template<typename A,typename B>inline void chkmin(A &x,const B y){if(x>y)x=y;}
const int N=2e5+5;
int n,T;
struct Graph{
int tot,to[N<<1],nxt[N<<1],len[N<<1],head[N];
void add(int x,int y,int z){tot++;to[tot]=y;len[tot]=z;nxt[tot]=head[x];head[x]=tot;}
void clear(){mms(head,-1);tot=0;}
#define EOR(G,i,x) for(int i=G.head[x];i!=-1;i=G.nxt[i])
}G;
ll maxl[N],A[N],B[N],dp[N],dis[N];
pll val[N*20];//所有区间的栈
int lst_pos[N][20];//记录原本位置
pll lst_val[N][20];//记录原本的点
int tot=1;
int stk[N];
struct YD_Tree{
#define ls (p<<1)
#define rs (p<<1|1)
static const int M=(N<<2);
int li[M],ri[M];
int bs_ans(int k,int p){//查询答案从哪个祖先转移
int l=li[p],r=ri[p]-2,ans=ri[p]-1;
while(l<=r){
int mid=(l+r)>>1;
if(1.0*(val[mid+1].y-val[mid].y)>=1.0*k*(val[mid+1].x-val[mid].x))
ans=mid,r=mid-1;
else l=mid+1;
}
return ans;
}
int bs_pos(pll P,int p){//查询插入位置
if(li[p]==ri[p])return li[p];
int l=li[p]+1,r=ri[p]-1,ans=li[p];
while(l<=r){
int mid=(l+r)>>1;
if(1.0*(val[mid].y-val[mid-1].y)*(P.x-val[mid].x)<=1.0*(P.y-val[mid].y)*(val[mid].x-val[mid-1].x))
ans=mid,l=mid+1;
else r=mid-1;
}
return ans+1;
}
void build(int p,int l,int r){//预处理线段树,以及分配区间
li[p]=ri[p]=tot;
tot+=r-l+1;
if(l==r)return;
int mid=(l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
}
void insert(int p,int l,int r,int x,int dep,pll P){//插入一个点
int pos=bs_pos(P,p);
lst_pos[x][dep]=ri[p];
lst_val[x][dep]=val[pos];
val[pos]=P;
ri[p]=pos+1;//我的栈是左闭右开的
if(l==r)return;
int mid=(l+r)>>1;
if(mid<x)insert(rs,mid+1,r,x,dep+1,P);
else insert(ls,l,mid,x,dep+1,P);
}
void get_ans(int p,int l,int r,int L,int R,int x){
if(L<=l&&r<=R){
if(li[p]==ri[p])return;//没有点无法转移,返回
int pos=bs_ans(A[x],p);
chkmin(dp[x],val[pos].y+B[x]+(dis[x]-val[pos].x)*A[x]);//更新答案
return;
}
int mid=(l+r)>>1;
if(mid<R)get_ans(rs,mid+1,r,L,R,x);
if(mid>=L)get_ans(ls,l,mid,L,R,x);
}
void backtrace(int p,int l,int r,int x,int dep){//回溯操作
val[ri[p]-1]=lst_val[x][dep];
ri[p]=lst_pos[x][dep];
if(l==r)return;
int mid=(l+r)>>1;
if(mid<x)backtrace(rs,mid+1,r,x,dep+1);
else backtrace(ls,l,mid,x,dep+1);
}
}Tr;
void dfs(int x){
if(x!=1){
int l=1,r=stk[0],pos=stk[0];
while(l<=r){//查询符合距离限制的祖先区间
int mid=(l+r)>>1;
if(dis[x]-dis[stk[mid]]<=maxl[x])pos=mid,r=mid-1;
else l=mid+1;
}
Tr.get_ans(1,1,n,pos,stk[0],x);
}
//插入
stk[++stk[0]]=x;
Tr.insert(1,1,n,stk[0],0,(pll){dis[x],dp[x]});
EOR(G,i,x){
int v=G.to[i];
dis[v]=dis[x]+G.len[i];
dfs(v);
}
//更新
Tr.backtrace(1,1,n,stk[0],0);
stk[stk[0]--]=0;
}
int main(){
freopen("ticket.in","r",stdin);
freopen("ticket.out","w",stdout);
G.clear();
sf("%d%d",&n,&T);
FOR(i,2,n){
int f,s;
sf("%d%d%lld%lld%lld",&f,&s,&A[i],&B[i],&maxl[i]);
G.add(f,i,s);
}
FOR(i,2,n)dp[i]=2e18;
Tr.build(1,1,n);
dfs(1);
FOR(i,2,n)pf("%lld\n",dp[i]);
return 0;
}