树链剖分学习笔记
毕竟刚学到了皮毛,还不足以讲,如果想较为深入的学习的话,请观摩博客:https://www.cnblogs.com/zwfymqz/p/8094500.html
更博啦。经过这几天的树链剖分学习,感觉还不是那么难,所以就写一下笔记,供大家分享。这份博客就用洛谷的一道模板题来讲https://www.luogu.org/problemnew/show/P3384
学习之前必须要掌握一些知识,不然你看几天都不会看得懂:线段树,dfs序。
如果掌握了就来看一下这几个定义:
- 重结点:子树结点数目最多的结点;
- 轻节点:父亲节点中除了重结点以外的结点;
- 重边:父亲结点和重结点连成的边;
- 轻边:父亲节点和轻节点连成的边;
- 重链:由多条重边连接而成的路径;
- 轻链:由多条轻边连接而成的路径;
定义没有什么好说的,我们就开始正式讲树链剖分。
比如上面这幅图中,用黑线连接的结点都是重结点,其余均是轻结点,2-11、1-11就是重链,其他就是轻链,用红点标记的就是该结点所在链的起点,也就是我们👇提到的top结点,还有每条边的值其实是进行dfs时的执行序号。
然后就是实现这个算法的辅助数组
名称 | 解释 |
---|---|
size[x] | 保存以x为根的子树节点个数 |
top[x] | 保存当前节点所在链的顶端节点 |
son[x] | 保存重儿子 |
deep[x] | 保存结点x的深度值 |
fa[x] | 保存结点u的父亲节点 |
id[x] | 保存树中每个节点剖分以后的新编号(DFS的执行顺序) |
此外还有两条性质
1.如果(u,v)是轻边,那么size[v]<size[u]/2
2.从根结点到任意结点的路所经过的轻重链的个数必定都小与O(logn);
然后我们要对这棵树进行预处理
我们用到了两个dfs,dfs1是处理子树大小和父子关系还有节点深度的,dfs2是用来处理编号和重链以及中儿子的。
然后这两个dfs比较好理解,这里就只粘代码,代码里面有注释,可以感性理解一下。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
void dfs1(int x,int f) { fa[x]=f;//首先记录父亲 size[x]=1; for(int i=head[x]; i!=-1; i=e[i].next) { if(!deep[e[i].to] && e[i].to != f) { deep[e[i].to]=deep[x]+1;//深度 dfs1(e[i].to,x); size[x]+=size[e[i].to];//子树大小 if(size[e[i].to]>size[son[x]]) son[x]=e[i].to;//如果当前子树比中儿子的子树大,那么当前子树的根为重儿子 } } }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
void dfs2(int x,int topf)//topf为当前重链的顶部 { idx[x] = ++cnt;//编号 top[x] = topf; if(!son[x]) return;//小优化 dfs2(son[x],topf); for(int i=head[x]; i!=-1; i=e[i].next) if(!idx[e[i].to] && e[i].to!=fa[x]) dfs2(e[i].to,e[i].to); }
理解了两个dfs,我们就好进行了!
其余的事情就是线段树做的事了,区间加和,区间查询,pushdown操作一样。但是树链剖分还有几个其他的操作。
- 节点路径修改
- 节点路径查询和
- 子树修改
- 子树查询
- 换根
节点路径修改这个操作需要用到一个函数:树上修改(change_tree)
这个思想就是用到了倍增,这样考虑,因为两个节点分别在两条重链上,然后你不断缩短重链,期间不断进行区间修改,然后这两个节点最后一定在一个重链上,在进行最后一步区间修改,大功告成。树上查询也是这样的。然后就是这段代码。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
void add_tree(int x,int y,int val) { while(top[x]!=top[y]) { if(deep[top[x]]<deep[top[y]]) swap(x,y); add_interval(1,idx[top[x]],idx[x],val); x=fa[top[x]]; } if(deep[x]<deep[y]) swap(x,y); add_interval(1,idx[y],idx[x],val); }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int asksum_tree(int x,int y) { int ans=0; while(top[x]!=top[y]) { if(deep[top[x]]<deep[top[y]]) swap(x,y); ans=(ans+asksum_interval(1,idx[top[x]],idx[x]))%p; x=fa[top[x]]; } if(deep[x]<deep[y]) swap(x,y); return (ans+asksum_interval(1,idx[y],idx[x]))%p; }
子树修改和子树查询也非常的简单。你可以观察一下发现,因为树映射到线段树上的是一段区间,所以它的子树的区间为[id[x],id[x]+size[x]-1],所以代码如下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//子树修改 change_interval(1,id[x],id[x]+size[x]-1,val) //子树查询 ask_interval(1,id[x],id[x]+size[x]-1)
之后就是我所说的换根操作。这个操作看似很麻烦,其实不然,我们可以用分类讨论的思想:设查询点为a,当前根节点为root
- 查询点与根节点相同:直接返回根节点的值
- LCA(a,root)!=a 那么直接返回a的子树的信息
- LCA(a,root)==a ,所以a和root在一棵子树,因为这棵树对应在线段树上的区间是连续的,所以我们访问除root子树之外的所有的区间,就是查询a的子树[1,id[root]-1]&[id[root]+size[x],n]
有些题有可能你会碰上树剖求LCA,这里就奉上树剖求LCA代码,代码很简单,你一定能看懂,而且你会发现耗时惊的一批
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int LCA(int x,int y) { while(top[x]!=top[y]) { if(deep[top[x]]<deep[top[y]])swap(x,y); x=fa[top[x]]; } return deep[x]<=deep[y]? x:y; }