题目大意
有一棵nnn个点的树,每条边都有边权。uuu到vvv的花费为uuu到vvv的路径上的边的边权和。
有qqq次询问,每次询问给出p,q,vp,q,vp,q,v,要求在[p,q][p,q][p,q]中选择一个uuu,使得uuu到vvv的花费最小,并输出这个最小值。
保证树上任意一点到另一点的花费不超过10910^9109。
时间限制5000ms5000ms5000ms,空间限制1024MB1024MB1024MB。
1≤n,q≤1051\leq n,q\leq 10^51≤n,q≤105
题解
我们考虑用分块来做。
对于散块,枚举块上的每个点iii并O(logn)O(\log n)O(logn)求iii和vvv的lcalcalca,用toi+tov−2tolcato_i+to_v-2to_{lca}toi+tov−2tolca更新答案,其中toito_itoi表示从根节点到结点iii的花费,处理所有散块的总时间复杂度为O(qnlogn)O(q\sqrt n\log n)O(qnlogn)。
对于每个整块,我们先预处理好块内的信息,再用这个块的信息更新每个询问。
对于一个点iii,如果它是uuu和vvv的lcalcalca,则uuu到vvv的花费为tou+tov−2toito_u+to_v-2to_itou+tov−2toi。当vvv确定时,我们要求一个uuu,满足u∈[p,q]u\in[p,q]u∈[p,q]且tou−2toito_u-2to_itou−2toi最小。
那么,假设当前枚举到第kkk个块,我们对图做一次dfsdfsdfs求mnimn_imni,mnimn_imni表示点iii的子树中所有在第kkk块中的最小的touto_utou。那么,我们枚举vvv和vvv的每个祖先,假设枚举到xxx,则可以用mnx−2tox+tovmn_x-2to_x+to_vmnx−2tox+tov来更新答案。
但是,万一xxx不是uuu和vvv的lcalcalca,统计的答案中不就包含了不合法的答案了吗?
设uuu和vvv的lcalcalca为yyy,当前枚举的节点为xxx且xxx为yyy的祖先,则路径u−x−vu-x-vu−x−v的花费一定比路径u−y−vu-y-vu−y−v的花费多,而答案要求的是最小值,所以路径u−x−vu-x-vu−x−v的花费会被路径u−y−vu-y-vu−y−v的花费所覆盖。也就是说,所有不合法的方案都能被合法的方案覆盖,这也就保证了这种方法的正确性。
那么,我们要求mnx−2tox+tovmn_x-2to_x+to_vmnx−2tox+tov的最小值,也就是求从vvv到根节点上每个点xxx的mnx−2toxmn_x-2to_xmnx−2tox的最小值,这个用树链剖分即可解决。
然而,树链剖分的查询是O(log2n)O(\log^2n)O(log2n)的,而总共会查询O(qn)O(q\sqrt n)O(qn)次,那么总时间复杂度为O(qnlog2n)O(q\sqrt n\log^2n)O(qnlog2n),我们考虑优化。
我们先思考为什么每次查询是O(log2n)O(\log^2n)O(log2n),因为在查询vvv时要向上跳logn\log nlogn次,每次要在线段树上O(logn)O(\log n)O(logn)查询。那么,我们可以用一个东西存储每个点向上跳一次时在线段树上查询的结果,下一次再遇到这个点时就可以O(1)O(1)O(1)查询。因为总共有nnn个点,每个点最多会在线段树中查询一次,那么qqq次查询的时间复杂度平摊下来就是O(nlogn)O(n\log n)O(nlogn)的。于是,我们将qqq次查询的时间复杂度从O(qlog2n)O(q\log^2n)O(qlog2n)优化成O(nlogn)O(n\log n)O(nlogn)。那么,处理n\sqrt nn个块的总时间复杂度为O(nnlogn)O(n\sqrt n\log n)O(nnlogn)。
总时间复杂度为O((n+q)nlogn)O((n+q)\sqrt n\log n)O((n+q)nlogn),空间复杂度为O(nn)O(n\sqrt n)O(nn),因为时间限制和空间限制都比较大,所以是可以过的。
一些缩短运行时间的小技巧
如果你TLETLETLE了,可以采用以下方法来缩短运行时间。
- 因为在分块中最前面部分和最后面部分不管是整块还是散块,都是按散块来处理,所以处理整块的时候不需要处理第一块和最后一块
- 用vectorvectorvector来维护每个块需要更新的询问,在每个询问求整块时放进对应块的vectorvectorvector,这样对于每个块就不用再将每个询问都枚举一遍了,这也是空间复杂度达到O(nn)O(n\sqrt n)O(nn)的原因
- 因为输入量比较大,你可以使用快读
当然,如果你实现得比较好,常数比较小的话,不用这些方法也是可以过的。
code
#include<bits/stdc++.h>
#define lc k<<1
#define rc k<<1|1
#define rg register
using namespace std;
const int N=100000,B=320;
int n,q,bl,tot=0,d[2*N+5],l[2*N+5],r[2*N+5],w[2*N+5];
int dep[N+5],fa[N+5],siz[N+5],son[N+5],s[N+5],re[N+5],tp[N+5];
long long to[N+5],mn[N+5],mw[N+5],ans[N+5],tr[4*N+5];
struct node{
int v,id;
};
vector<node>hv[B+5];
int rd(){
int t=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9'){
t=t*10+ch-'0';
ch=getchar();
}
return t;
}
int pos(int i){return (i-1)/bl+1;}
void add(int xx,int yy,int zz){
l[++tot]=r[xx];d[tot]=yy;r[xx]=tot;w[tot]=zz;
}
void dfs1(int u,int f){
fa[u]=f;
dep[u]=dep[f]+1;
siz[u]=1;
for(rg int i=r[u];i;i=l[i]){
if(d[i]==f) continue;
to[d[i]]=to[u]+w[i];
dfs1(d[i],u);
siz[u]+=siz[d[i]];
if(siz[d[i]]>siz[son[u]]) son[u]=d[i];
}
}
void dfs2(int u,int v){
tp[u]=v;
s[u]=++s[0];re[s[0]]=u;
if(son[u]) dfs2(son[u],v);
for(rg int i=r[u];i;i=l[i]){
if(d[i]==fa[u]||d[i]==son[u]) continue;
dfs2(d[i],d[i]);
}
}
int lca(int x,int y){
while(tp[x]!=tp[y]){
if(dep[tp[x]]<dep[tp[y]]) swap(x,y);
x=fa[tp[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
void solve(int x,int y,int v,int id){
int vl=pos(x),vr=pos(y);
if(vl==vr){
for(rg int i=x;i<=y;i++){
ans[id]=min(ans[id],to[i]+to[v]-2*to[lca(i,v)]);
}
return;
}
for(rg int i=x;i<=vl*bl;i++){
ans[id]=min(ans[id],to[i]+to[v]-2*to[lca(i,v)]);
}
for(rg int i=vr*bl-bl+1;i<=y;i++){
ans[id]=min(ans[id],to[i]+to[v]-2*to[lca(i,v)]);
}
for(rg int i=vl+1;i<=vr-1;i++){
hv[i].push_back((node){v,id});
}
}
void dfs3(int u){
for(rg int i=r[u];i;i=l[i]){
if(d[i]==fa[u]) continue;
dfs3(d[i]);
mn[u]=min(mn[u],mn[d[i]]);
}
}
void build(int k,int l,int r){
if(l==r){
tr[k]=mn[re[l]]-2*to[re[l]];
return;
}
int mid=l+r>>1;
build(lc,l,mid);
build(rc,mid+1,r);
tr[k]=min(tr[lc],tr[rc]);
}
long long find(int k,int l,int r,int x,int y){
if(l>=x&&r<=y) return tr[k];
int mid=l+r>>1;
long long re=1e15;
if(x<=mid) re=min(re,find(lc,l,mid,x,y));
if(y>mid) re=min(re,find(rc,mid+1,r,x,y));
return re;
}
long long gt(int x){
long long re=1e15;
while(tp[x]){
if(mw[x]==1e15) mw[x]=find(1,1,n,s[tp[x]],s[x]);
re=min(re,mw[x]);
x=fa[tp[x]];
}
return re;
}
int main()
{
// freopen("game.in","r",stdin);
// freopen("game.out","w",stdout);
n=rd();
bl=sqrt(n);
for(rg int i=1,x,y,z;i<n;i++){
x=rd();y=rd();z=rd();
add(x,y,z);add(y,x,z);
}
dfs1(1,0);
dfs2(1,1);
q=rd();
for(rg int i=1,x,y,v;i<=q;i++){
x=rd();y=rd();v=rd();
ans[i]=1e15;
solve(x,y,v,i);
}
for(rg int i=2;i<pos(n);i++){
for(rg int j=1;j<=n;j++) mn[j]=mw[j]=1e15;
for(rg int j=i*bl-bl+1;j<=i*bl;j++) mn[j]=to[j];
dfs3(1);
build(1,1,n);
for(rg int j=0;j<hv[i].size();j++){
ans[hv[i][j].id]=min(ans[hv[i][j].id],gt(hv[i][j].v)+to[hv[i][j].v]);
}
}
for(rg int i=1;i<=q;i++){
printf("%lld\n",ans[i]);
}
return 0;
}

文章讲述了如何在一个有n个点和q次询问的树中,通过分块和树链剖分的方法,高效地计算出给定区间内两点之间的最小花费路径。主要涉及lca(最近公共祖先)的计算和在线段树上的优化查询。
782





