树链剖分(入门学习)
学习博客:https://www.cnblogs.com/ivanovcraft/p/9019090.html
先来回顾两个问题:
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最大)的结点;
轻儿子:父亲节点中除了重儿子以外的儿子;
重边:父亲结点和重儿子连成的边;
轻边:父亲节点和轻儿子连成的边;
重链:由多条重边连接而成的路径;
轻链:由多条轻边连接而成的路径;
比如上面这幅图中,用黑线连接的结点都是重结点,其余均是轻结点,
2-11就是重链,2-5就是轻链,用红点标记的就是该结点所在重链的起点,也就是下文提到的top结点,
还有每条边的值其实是进行dfs时的执行序号。
变量声明:
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数组),
解释:比如说点1,它有三个儿子2,3,4
2所在子树的大小是5
3所在子树的大小是2
4所在子树的大小是6
那么1的重儿子是4
ps:如果一个点的多个儿子所在子树大小相等且最大
那随便找一个当做它的重儿子就好了
叶节点没有重儿子,非叶节点有且只有一个重儿子
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=1for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;dfs1(v,u,depth+1); //层次深度+1size[u]+=size[v]; //子节点的size已被处理,用它来更新父节点的sizeif(size[v]>size[son[u]])son[u]=v; //选取size最大的作为重儿子} } //进入 dfs1(root,0,1);
dfs跑完大概是这样的,大家可以手动模拟一下
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对应节点uif(!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必然是它本身} }
dfs跑完大概是这样的,大家可以手动模拟一下
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);elseans+=query(id[y],id[x],rt);return ans; }
大家如果明白了树链剖分,也应该有举一反三的能力(反正我没有),修改和LCA就留给大家自己完成了
5,树链剖分的时间复杂度
树链剖分的两个性质:
1,如果(u, v)是一条轻边,那么size(v) < size(u)/2;
2,从根结点到任意结点的路所经过的轻重链的个数必定都小于logn;
可以证明,树链剖分的时间复杂度为O(nlog^2n)
下面看一道板子题:题目链接:https://www.luogu.org/problemnew/show/P3384
代码:
#include<iostream> #include<cstdio> using namespace std; const int maxn=1e5+50; int N,M,R,mod; int cnt=0; int v[maxn]; int Size[maxn],deep[maxn],fa[maxn],son[maxn],id[maxn],top[maxn],head[maxn],rk[maxn];// struct edge {int to,next;//e[i].to代表第i条边的终点 e[i].next代表与第i条边同起点的下一条边的终点 }e[maxn<<1]; struct node {int l,r,ls,rs,sum,lazy; }a[maxn<<1]; void add(int x,int y)//链式前向星存储 {e[++cnt].to=y;e[cnt].next=head[x];head[x]=cnt; } int len(int rt) {return a[rt].r-a[rt].l+1; }void push_up(int rt) {a[rt].sum=(a[a[rt].ls].sum+a[a[rt].rs].sum)%mod; } void Push_down(int rt) {a[a[rt].ls].lazy=(a[a[rt].ls].lazy+a[rt].lazy)%mod;a[a[rt].rs].lazy=(a[a[rt].rs].lazy+a[rt].lazy)%mod;a[a[rt].ls].sum=(a[a[rt].ls].sum+len(a[rt].ls)*a[rt].lazy)%mod;a[a[rt].rs].sum=(a[a[rt].rs].sum+len(a[rt].rs)*a[rt].lazy)%mod;a[rt].lazy=0; } void dfs1(int rt) {Size[rt]=1,deep[rt]=deep[fa[rt]]+1;for(int i=head[rt];i;i=e[i].next){int v=e[i].to;if(v!=fa[rt]){fa[v]=rt;dfs1(v);Size[rt]+=Size[v];if(Size[v]>Size[son[rt]]) son[rt]=v;}} } void dfs2(int R,int rt) {id[R]=++cnt;rk[cnt]=R;top[R]=rt;if(son[R])dfs2(son[R],rt);//优先走重儿子for(int i=head[R];i;i=e[i].next){ // cout<<"***"<<endl;int v=e[i].to;if((v!=son[R])&&(v!=fa[R])) dfs2(v,v);//轻儿子的重儿子就是本身 } } void Build(int l,int r,int rt) {if(l==r){a[rt].l=l;a[rt].r=r;a[rt].sum=v[rk[l]];return ;}int mid=(l+r)>>1;a[rt].ls=++cnt;a[rt].rs=++cnt;a[rt].l=l,a[rt].r=r; // cout<<a[rt].ls<<" "<<a[rt].rs<<endl; // cout<<a[rt].l<<" "<<a[rt].r<<endl; Build(l,mid,a[rt].ls);Build(mid+1,r,a[rt].rs);push_up(rt); }void update(int x,int y,int c,int rt) { // cout<<x<<" "<<y<<" "<<rt<<endl; // cout<<"*"<<endl;if(x<=a[rt].l&&a[rt].r<=y){a[rt].lazy=(a[rt].lazy+c)%mod;a[rt].sum=(a[rt].sum+len(rt)*c)%mod;return ;}Push_down(rt);int mid=(a[rt].l+a[rt].r)>>1;if(x<=mid) update(x,y,c,a[rt].ls);if(y>mid) update(x,y,c,a[rt].rs);push_up(rt); } void updates(int x,int y,int c) {while(top[x]!=top[y]){if(deep[top[x]]<deep[top[y]]) swap(x,y);//统一跳xupdate(id[top[x]],id[x],c,0);//线段树区间更新x=fa[top[x]];}//在同一条重链上if(id[x]>id[y]) swap(x,y);update(id[x],id[y],c,0); } int query(int x,int y,int rt) {int tot=0;if(x<=a[rt].l&&a[rt].r<=y){return a[rt].sum;}Push_down(rt);int mid=(a[rt].l+a[rt].r)>>1;if(x<=mid) tot+=query(x,y,a[rt].ls);if(y>mid) tot+=query(x,y,a[rt].rs);tot%=mod; // push_up(rt);return tot; } int sum(int x,int y) {int ret=0;while(top[x]!=top[y])//不在同一条重链上 {if(deep[top[x]]<deep[top[y]]) swap(x,y);ret+=query(id[top[x]],id[x],0);ret%=mod;x=fa[top[x]];}if(id[x]>id[y]) swap(x,y);ret+=query(id[x],id[y],0);ret%=mod;return ret; } int main() {cnt=0;scanf("%d%d%d%d",&N,&M,&R,&mod);for(int i=1;i<=N;i++) scanf("%d",&v[i]);for(int i=1;i<N;i++){int x,y;scanf("%d%d",&x,&y);add(x,y);//双向边 add(y,x);}/**dfs1求出原树中每个结点为根的树大小 原树中每个结点的深度 每个结点的父亲 重儿子*/cnt=0;dfs1(R);//从根节点开始 // cout<<"**"<<endl;cnt=0;/**dfs2求出新树中每个结点的编号 每个编号对应原树中的值 所在重链的根节点*/dfs2(R,R);cnt=0; // cout<<"*"<<endl;Build(1,N,0);//以得到的新编号来建树for(int i=1;i<=M;i++){int op,x,y,z;scanf("%d",&op);if(op==1){scanf("%d%d%d",&x,&y,&z);updates(x,y,z);}else if(op==2){scanf("%d%d",&x,&y);printf("%d\n",sum(x,y));}else if(op==3){scanf("%d%d",&x,&z);update(id[x],id[x]+Size[x]-1,z,0);}else{scanf("%d",&x);printf("%d\n",query(id[x],id[x]+Size[x]-1,0));}}return 0; }
转载于:https://www.cnblogs.com/caijiaming/p/10821835.html
树链剖分(入门学习)相关推荐
- 树链剖分入门+HYSBZ - 1036树的统计Count
今天学习了树链剖分,记录一下. [题目背景] HYSBZ - 1036树的统计Count [题目分析] 题目要求求任意结点之间路径的和以及路径上最大的结点,还有可能修改.如果正常做可能会很复杂(我也不 ...
- 树链剖分入门——[kuangbin]树链剖分
树链剖分的本质就是将一棵树拆分成一段一段连续的区间,然后放在一起就可以用一棵单独的线段树处理区间问题,只需要将树上节点和线段树节点的对应关系求好就可以很方便的互相转换,而树上两点之间路径的相关问题就可 ...
- SPOJ 375. Query on a tree (树链剖分)
题目链接: http://www.spoj.com/problems/QTREE/ 375. Query on a tree Problem code: QTREE You are given a t ...
- 【ZJOI2008】树的统计(树链剖分)
传送门 Solution: 就是树链剖分入门题啦~ // luogu-judger-enable-o2 #include<bits/stdc++.h> #define N 30005 #d ...
- SPOJ 375 query on a tree 树链剖分
题意: 给一棵树型数据结构 ①支持修改边的权值 ②支持成段边权最值查询 树链剖分入门题. 树链剖分+线段树 用的notonlysuccess的线段树--不开结构体事先预处理的那种 我以前写的 ...
- SPOJ - QTREE3Query on a tree again!——树链剖分
[题目描述] SPOJ - QTREE3Query on a tree again! [题目分析] 题目要求是输出从111到xxx的路径上遇到的第一个黑色的点.我们可以用树链剖分(不了解的同学请出门左 ...
- SPOJ 375 树链剖分学习
学习树链剖分的第一题,第二个dfs忘记递归了(太蠢),re了两发,改过来以后就1A了. 学习树链剖分可以参考这篇博客:http://blog.sina.com.cn/s/blog_7a17468201 ...
- POJ 3237 树链剖分学习(树链剖分小结)
一个地方wa了3发,找了一组数组发现的错误,太蠢,就是两个人数作交换写成了 a=b ,b=a.最近智商真是感人.这道题是由spoj375这题改编的,多了一个取反的操作,这个操作只需要多维护一个最小值就 ...
- 树链剖分(轻重链)入门
写在前面 仅想学树剖LCA的同学其实不必要了解线段树 前置知识:树形结构,链式前向星(熟练),线段树(熟练),DFS序(熟练),LCA(了解定义) 树链剖分(树剖):将树分解为一条条不相交的,从祖先到 ...
最新文章
- LeetCode - Add Binary
- java 连接redis失败_java 连接Redis问题及demo
- 如何让你的 Linux 系统干净整洁
- AjaxFileUpload.js
- python数据科学手册_数据科学的Python
- VMware Harbor现已加入Rancher社区Catalog
- linux下使用httping测试web响应时间
- mysql 8.0 (ga)_MySQL 8.0.20 GA 发布
- linux免费商用字体,免费可商用字体~文泉驿正黑体
- JVM监控及诊断工具之JConsole
- DCDC电源纹波测试
- 抽象工厂和工厂方法模式
- 苹果iOS系统下的推送机制及实现
- Q-learning学习的一个小例子
- 小猫爪:PMSM之FOC控制11-基于SMO实现FOC无传感器双闭环
- GeneXus项目启动
- 用火狐浏览器快速扒网页图片素材
- PHP的bz2压缩扩展工具
- 计算机中存储单位的编号称号是什么,KB、MB、GB的中文单位名称是什么?
- php手机短信接口发送验证码
热门文章
- Gradle_04_解决多项目同级依赖时找不到符号的异常
- python web开发中跨域问题的解决思路
- vue v-html 动态内容样式无效解决方法
- 【干货】私域电商崛起:2021见实私域流量白皮书高清6月版.pdf(附下载链接)...
- 【报告分享】2019-2020广告主KOL营销市场盘点及趋势预测.pdf(附下载链接)
- 区块链的一些名词解释
- sklearn中的Linear_model的score函数讲解
- 推荐系统多兴趣召回最新进展
- mysql 二进制 nodejs_nodejs怎么存取2进制数据到数据库?
- java实现c语言的函数_C语言tolower函数介绍、示例和实现