感觉树上问题的难度真的好大啊
LCA,倍增,生成树,树链剖分,唉好难
下面这道题写了好长时间,就是树链剖分+LCA,我写了两种方法不知道为什么有一种是RE和MLE了
真的好痛苦,调了好久的bug还没有调出来,emmmm我寻思问问大佬们了,希望斧正一下我的代码
在给出代码之前看了网课(洛谷的啦),emmm寻思总结一下喽:
先来回顾两个问题:
1,将树从x到y结点最短路径上所有节点的值都加上z
这也是个模板题了吧
我们很容易想到,树上差分可以以O(n+m)的优秀复杂度解决这个问题
2,求树从x到y结点最短路径上所有节点的值之和
lca大水题,我们又很容易地想到,dfs O(n)预处理每个节点的dis(即到根节点的最短路径长度)
然后对于每个询问,求出x,y两点的lca,利用lca的性质 distance (x,y)=dis(x)+dis(y)-2*dis(lca) 求出结果
时间复杂度O(mlogn+n)
现在来思考一个bug:
如果刚才的两个问题结合起来,成为一道题的两种操作呢?
刚才的方法显然就不够优秀了(每次询问之前要跑dfs更新dis)
树剖是通过轻重边剖分将树分割成多条链,然后利用数据结构来维护这些链(本质上是一种优化暴力)
首先明确概念:
- 重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;
- 轻儿子:父亲节点中除了重儿子以外的儿子;
- 重边:父亲结点和重儿子连成的边;
- 轻边:父亲节点和轻儿子连成的边;
- 重链:由多条重边连接而成的路径;
- 轻链:由多条轻边连接而成的路径;
变量声明
const int maxn=1e5+10; struct edge{ int next,to; }e[2*maxn]; struct Node{ int sum,lazy,l,r,ls,rs; }node[2*maxn]; int rt,n,m,r,a[maxn],cnt,head[maxn],f[maxn],d[maxn],size[maxn],son[maxn],rk[maxn],top[maxn],id[maxn];
名称解释f[u]保存结点u的父亲节点d[u]保存结点u的深度值size[u]保存以u为根的子树节点个数son[u]保存重儿子
rk[u]保存当前dfs标号在树中所对应的节点top[u]保存当前节点所在链的顶端节点id[u]保存树中每个节点剖分以后的新编号(DFS的执行顺序)
1,对于一个点我们首先求出它所在的子树大小,找到它的重儿子(即处理出size,son数组)
2,在dfs过程中顺便记录其父亲以及深度(即处理出f,d数组),操作1,2可以通过一遍dfs完成
void dfs1(int u,int fa,int depth) //当前节点、父节点、层次深度 { f[u]=fa; d[u]=depth; size[u]=1; //这个点本身size=1 for(int i=head[u];i;i=e[i].next) { int v=e[i].to; if(v==fa) continue; dfs1(v,u,depth+1); //层次深度+1 size[u]+=size[v]; //子节点的size已被处理,用它来更新父节点的size if(size[v]>size[son[u]]) son[u]=v; //选取size最大的作为重儿子 } } //进入 dfs1(root,0,1);
3,第二遍dfs,然后连接重链,同时标记每一个节点的dfs序,并且为了用数据结构来维护重链,
我们在dfs时保证一条重链上各个节点dfs序连续(即处理出数组top,id,rk)
void dfs2(int u,int t) //当前节点、重链顶端 { top[u]=t; id[u]=++cnt; //标记dfs序 rk[cnt]=u; //序号cnt对应节点u if(!son[u]) return; dfs2(son[u],t); /*我们选择优先进入重儿子来保证一条重链上各个节点dfs序连续, 一个点和它的重儿子处于同一条重链,所以重儿子所在重链的顶端还是t*/ for(int i=head[u];i;i=e[i].next) { int v=e[i].to; if(v!=son[u]&&v!=f[u]) dfs2(v,v); //一个点位于轻链底端,那么它的top必然是它本身 } }
4,两遍dfs就是树链剖分的主要处理,通过dfs我们已经保证一条重链上各个节点dfs序连续,
那么可以想到,我们可以通过数据结构(以线段树为例)来维护一条重链的信息
回顾上文的那个题目,修改和查询操作原理是类似的,以查询操作为例,其实就是个LCA,不过这里使用了top来进行加速,
因为top可以直接跳转到该重链的起始结点,轻链没有起始结点之说,他们的top就是自己。需要注意的是,每次循环只能跳
一次,并且让结点深的那个来跳到top的位置,避免两个一起跳从而插肩而过。
int sum(int x,int y) { int ans=0,fx=top[x],fy=top[y]; while(fx!=fy) //两点不在同一条重链 { if(d[fx]>=d[fy]) { ans+=query(id[fx],id[x],rt); //线段树区间求和,处理这条重链的贡献 x=f[fx],fx=top[x]; //将x设置成原链头的父亲结点,走轻边,继续循环 } else { ans+=query(id[fy],id[y],rt); y=f[fy],fy=top[y]; } } //循环结束,两点位于同一重链上,但两点不一定为同一点,所以我们还要统计这两点之间的贡献 if(id[x]<=id[y]) ans+=query(id[x],id[y],rt); else ans+=query(id[y],id[x],rt); return ans; }
题目传送门:https://www.luogu.com.cn/problem/P4281
看完上面的解释,这道题的思路就有了:
首先,如果每次询问都只有两个点,这个问题就很简单,只要是树上的路径上的点就可以,寻找树上的路径其实就是寻找LCALCA的过程。
这可以启发我们对于三个点的情况的思考。
如果这里有三个点,我们来认真的思考一下。经过上一问的启发,我们来思考一下能不能运用LCALCA来解决这道题。
我们可以发现,树上三个点的三对LCALCA一定有两个是相同的。这是一件想想的话比较显然的事情。必然能够找到某个节点,让三个点中
的两个在一侧,一个在另一侧。而这个点就是两个公共的LCALCA。思考的再深入些(并且结合瞎蒙),我们会发现这个相同的LCALCA肯
定是深度最小的一个LCALCA。
这里,我们首先可以显而易见的发现,这个点必须在三个点互相通达的路径上。
我们再思考一下LCALCA与路径和的关系。假设我们知道aa和bb的LCALCA是xx,而且xx是上述的3个LCALCA中深度最大的那个,那么可
以发现从xx到aa的距离加上从xx到bb的距离一定是最小的。根据上面的结论,我们知道aa,cc和bb,cc的LCALCA点yy一定在一个点上,
而且这个yy一定比xx深度小。
那么这个时候,我们会发现此时aa,bb,cc到xx的距离和是最小的。证明的话可以这么想:如果x'x′比xx高,那么虽然cc到xx的距离减小了
ww,但是aa,bb到x'x′的距离均增大了ww,显然距离和增大。如果x'x′比xx低,有一个节点到x'x′的距离减小了ww,剩下两个节点到x'x′的距
离均增大了ww,显然距离和也增大。
所以我们就找到了到三个点距离和最小的点:这三个点的三对LCALCA中,深度大的那两个LCA就是答案。
我们在求LCALCA之前,可以先预处理出深度depdep,那么从节点uu到vv的路径长度就是dis = dep[u] + dep[v] - 2*dep[lca(u,v)]dis=dep[u]+dep[v]−2∗dep[lca(u,v)]。
运用这个式子分别算出aa,bb,cc到a1a1,b1b1,c1c1(三个LCALCA)的距离,最后发现总的disdis居然是轮换式:
ans = dep[a]+dep[b]+dep[c]-dep[a1]-dep[b1]-dep[c1]ans=dep[a]+dep[b]+dep[c]−dep[a1]−dep[b1]−dep[c1]
AC代码:
#include<cstdio> #include<algorithm> #define N 500010 #define rg register using namespace std; int n,m,tot,last[N],dep[N],son[N],size[N],fa[N],top[N]; struct edge{ int to,pre; }e[N<<1]; inline int read(){ int k=0,f=1; char c=getchar(); while(c<'0'||c>'9')c=='-'&&(f=-1),c=getchar(); while('0'<=c&&c<='9')k=k*10+c-'0',c=getchar(); return k*f; } inline void add(int x,int y){ e[++tot]=(edge){y,last[x]}; last[x]=tot; } void dfs1(int x){ size[x]=1; dep[x]=dep[fa[x]]+1; for(rg int i=last[x],to;i;i=e[i].pre)if((to=e[i].to)!=fa[x]){ fa[to]=x; dfs1(to); size[x]+=size[to]; if(size[to]>size[son[x]]) son[x]=to; } } void dfs2(int x,int tp){ top[x]=tp; if(son[x]) dfs2(son[x],tp); for(rg int i=last[x],to;i;i=e[i].pre) if((to=e[i].to)!=fa[x]&&to!=son[x]) dfs2(to,to); } inline int lca(int x,int y){ int f1=top[x],f2=top[y]; while(f1!=f2){ if(dep[f1]<dep[f2]) swap(f1,f2),swap(x,y); x=fa[f1]; f1=top[x]; } return dep[x]<dep[y]?x:y; } int main(){ n=read(); m=read(); for(rg int i=1;i<n;i++){ int u=read(),v=read(); add(u,v); add(v,u); } dfs1(1); dfs2(1,1); for(rg int i=1;i<=m;i++){ int a=read(),b=read(),c=read(); int l1=lca(a,b),l2=lca(a,c),l3=lca(b,c); if(dep[l1]>=dep[l2]&&dep[l1]>=dep[l3]){ printf("%d ",l1); printf("%d\n",dep[a]+dep[b]+dep[c]-dep[l1]-(dep[lca(l1,c)]<<1)); continue; } if(dep[l2]>=dep[l1]&&dep[l2]>=dep[l3]){ printf("%d ",l2); printf("%d\n",dep[a]+dep[b]+dep[c]-dep[l2]-(dep[lca(l2,b)]<<1)); continue; } if(dep[l3]>=dep[l1]&&dep[l3]>=dep[l2]){ printf("%d ",l3); printf("%d\n",dep[a]+dep[b]+dep[c]-dep[l3]-(dep[lca(l3,a)]<<1)); continue; } } return 0; }