【算法学习】【洛谷】树链剖分 & P3384 【模板】树链剖分 P2146 软件包管理器
刚学的好玩算法,AC2题,非常开心。
其实很早就有教过,以前以为很难就没有学,现在发现其实很简单也很有用。
更重要的是我很好调试,两题都是几乎一遍过的。
介绍树链剖分前,先确保已经学会以下基本技巧:
DFS序列,线段树/树状数组,LCA(最近公共祖先)
DFS序列确保你能听懂以下环节,线段树/树状数组是维护序列的有力工具,而LCA涉及树上的很多基本问题。
经常会遇到这样的题目:
对于一棵树,给x到y的路径上的点/边都做一个操作,并且查询x到y的路径上的点/边的值。
如果不是x到y的路径,而是节点x的子树,这可以方便地用DFS序列维护。
因为一棵子树在一个DFS序列中是连续的一段区间。
用类似的思想,我们尝试把树上的链也变成一段连续区间。
然而这是不可能的,因为链上是有分支的。
于是我们退而求其次,把一个链分成有限个区间,这边是有可能实现的,应运而生的算法就是树链剖分。
对于一棵树上的每个节点\(x\),我们定义\(siz(x)\)为以\(x\)为根的子树的节点个数。
对于一棵树上的每一个非叶子节点,我们找到它的儿子们中\(siz\)最大的那一个,并且标记它到这个儿子的这条边。
那么对树上所有点都进行这个操作之后,被标记的边会形成若干条链,并且覆盖每一个节点。
我们把这样的边叫做重边,其他的边叫做轻边,把连续重边形成的链叫做重链,一个点也可以组成一条长度为1的重链。
定理①:每个点属于且只属于一个重链。
定理②:从一个点到根的路径中,重链个数不超过\(O(log_2n)\)个,\(n\)表示树大小。
证明:从一个点向它的父亲走,如果这条边是轻边,那么子树的节点数至少翻倍,重链个数加一。
那么显然轻边个数不超过\(O(log_2n)\),而不是轻边的就组成重链,于是重链个数也不超过\(O(log_2n)\)个。
好了,我们知道了重链的定义,性质,如何使用它呢?
因为一个点到根不超过\(O(log_2n)\)条重链,那么任意两点间也不会超过\(O(log_2n)\)条重链。
而每条重链都是一段连续的区间,那么链上问题就变成了区间问题的并了。
这就实现了链到区间的转化。
要想使用树链剖分,需要在代码中实现它,我们需要做些什么?
先考虑用使用重链,求出任意两点之间的路径吧,或者,求出它们的LCA。
考虑初始时\(x,y\)两点,我们发现每一条重链都能直接用它的最顶端的节点表示,记作\(top\)节点。
那么先看\(x,y\)属不属于同一条重链:\(top(x)=top(y)\)?
只要不属于,一定要有一个跑出它的重链,实际上,必然是\(top(x),top(y)\)中深度更深的那个跳出其重链。
不妨设其为\(x\),那么\(x\to y\)的路径上就多了一条\(top_x\to x\)的路径,之后执行\(x=faz(top(x))\),\(faz(x)\)表示\(x\)的父亲节点。
不断重复这个操作,直到\(top(x)=top(y)\),那么这时\(x,y\)的LCA就是\(x,y\)中深度小的那个了。
这样的时间复杂度是\(O(log_2n)\)的。
在做这些操作前,我们需要先求出\(top(x)\)、\(siz(x)\)、\(dep(x)\)、\(faz(x)\)等关键数组。
如何求出?其实一次DFS就够了。
DFS(u){
dep[u] = dep[faz[u]] + 1
Heavy = 0, siz[u] = 1
for ( i = u.sons ) {
faz[i] = u
DFS(i)
siz[u] = siz[u] + siz[i]
if ( siz[Heavy] < siz[i] )
Heavy = i
}
son[u] = Heavy
for ( i = u.sons ) {
if ( i != Heavy ) {
j=i
while ( j != NULL )
top[j] = i, j = son[j]
}
}
}
这样就能处理好了,这里的\(son[x]\)表示\(x\)的重儿子。
还有一个小问题就是:我们没有把树中的每个节点映射到序列中的某个编号上。
在通常的DFS序列中这个编号就是\(dfn\)数组,在树链剖分中,我们也可以使用\(dfn\)数组,但是要满足链上的点在一个区间中。
做到这一点也不难,直接在里面一样做就好,也不用第二个DFS。
问题在于,有的题目不止要你改链上的点,还要子树修改,而一棵树的总重链数是可以到\(O(n)\)级别的,自然不能也用链做。
然而子树修改是个很简单的东西,只要满足子树内有序就好了,但是要同时满足的话……
也不是不可能,再写一个DFS就好啦。就和平常的求DFS序列的DFS一样,不过要注意先遍历到重儿子,这样可以保证重链上的点的编号连续。
总结:
树链剖分,DFS序列,本质上都是一样的,相当于一个树到数组的映射,把分支的结构变成连续的结构,并且满足一些性质。
DFS序列只要满足子树的节点是连续的即可,而树链剖分还要满足特定树链的节点连续。
这必然使树链剖分有更强的操作性,树链剖分是很重要的静态在线树上算法,通过预处理,一切操作都可以在线地完成。
接下来放两道例题:
直接的树链剖分模板,使用线段树维护区间。
// luogu-judger-enable-o2
#include<cstdio>
#define F(i,a,b) for(int i=a;i<=(b);++i)
#define F2(i,a,b) for(int i=a;i<(b);++i)
#define eF(i,u) for(int i=h[u];i;i=nxt[i])
#define swap(a,b) tmp=a, a=b, b=tmp int n,q,R,P,a[100001],b[100001],tmp;
int h[100001],to[200001],nxt[200001],tot;
inline void ins(int x,int y){nxt[++tot]=h[x];to[tot]=y;h[x]=tot;}
int top[100001],son[100001],siz[100001],dep[100001],faz[100001],dfn[100001],cnt;
int dat[262145],laz[262145]; inline void Ex(int u){
for(int i=u;i;i=son[i]) top[i]=u;
} void DFS(int u,int f){
siz[u]=1; dep[u]=dep[faz[u]]+1;
int Heavy=0;
eF(i,u) if(to[i]!=f){
DFS(to[i],faz[to[i]]=u);
siz[u]+=siz[to[i]];
if(siz[Heavy]<siz[to[i]]) Heavy=to[i];
}
son[u]=Heavy;
eF(i,u) if(to[i]!=f) if(to[i]!=Heavy) Ex(to[i]);
} void DFS2(int u,int f){
dfn[u]=++cnt; b[cnt]=a[u];
if(son[u]) DFS2(son[u],u);
eF(i,u) if(to[i]!=f) if(to[i]!=son[u]) DFS2(to[i],u);
} void build(int i,int l,int r){
if(l==r) {dat[i]=b[l]%P; return;}
int mid=l+r>>1;
build(i<<1,l,mid), build(i<<1|1,mid+1,r);
dat[i]=(dat[i<<1]+dat[i<<1|1])%P;
} inline void push_down(int i,int len){
int len2=len>>1;
dat[i<<1]=(dat[i<<1]+(len-len2)*laz[i])%P;
dat[i<<1|1]=(dat[i<<1|1]+len2*laz[i])%P;
laz[i<<1]=(laz[i<<1]+laz[i])%P;
laz[i<<1|1]=(laz[i<<1|1]+laz[i])%P;
laz[i]=0;
} void Add(int i,int l,int r,int a,int b,int x){
if(r<a||b<l) return;
if(a<=l&&r<=b) {dat[i]=(dat[i]+x*(r-l+1))%P; laz[i]=(laz[i]+x)%P; return;}
push_down(i,r-l+1);
int mid=l+r>>1;
Add(i<<1,l,mid,a,b,x); Add(i<<1|1,mid+1,r,a,b,x);
dat[i]=(dat[i<<1]+dat[i<<1|1])%P;
} int Qur(int i,int l,int r,int a,int b){
if(r<a||b<l) return 0;
if(a<=l&&r<=b) return dat[i];
push_down(i,r-l+1);
int mid=l+r>>1;
return (Qur(i<<1,l,mid,a,b)+Qur(i<<1|1,mid+1,r,a,b))%P;
} inline void Add(int x,int y,int z){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
Add(1,1,n,dfn[top[x]],dfn[x],z);
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
Add(1,1,n,dfn[x],dfn[y],z);
} inline int Qur(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ans=(ans+Qur(1,1,n,dfn[top[x]],dfn[x]))%P;
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return (ans+Qur(1,1,n,dfn[x],dfn[y]))%P;
} int main(){
scanf("%d%d%d%d",&n,&q,&R,&P);
F(i,1,n) scanf("%d",a+i);
int x,y; F2(i,1,n) scanf("%d%d",&x,&y), ins(x,y), ins(y,x);
dep[R]=1; DFS(R,0); Ex(R);
DFS2(R,0);
build(1,1,n);
F(i,1,q){
int opt,x,y,z;
scanf("%d",&opt);
if(opt==1) scanf("%d%d%d",&x,&y,&z), Add(x,y,z);
if(opt==2) scanf("%d%d",&x,&y), printf("%d\n",Qur(x,y));
if(opt==3) scanf("%d%d",&x,&z), Add(1,1,n,dfn[x],dfn[x]+siz[x]-1,z);
if(opt==4) scanf("%d",&x), printf("%d\n",Qur(1,1,n,dfn[x],dfn[x]+siz[x]-1));
}
return 0;
}
NOI2015的题目,要在树上进行清零和置一的操作。有链上和子树上的操作。
#include<cstdio>
#include<algorithm>
#define F(i,a,b) for(int i=a;i<=(b);++i)
#define F2(i,a,b) for(int i=a;i<(b);++i)
#define eF(i,u) for(int i=h[u];i;i=nxt[i])
using namespace std; int n,q,a[100005],b[100005],tmp;
int h[100005],to[100005],nxt[100005],tot;
inline void ins(int x,int y){nxt[++tot]=h[x];to[tot]=y;h[x]=tot;}
int top[100005],son[100005],siz[100005],dep[100005],faz[100005],dfn[100005],cnt;
int dat[262145],laz[262145]; inline void Ex(int u){
for(int i=u;i;i=son[i]) top[i]=u;
} void DFS(int u){
siz[u]=1; dep[u]=dep[faz[u]]+1;
int Heavy=0;
eF(i,u){
DFS(to[i]);
siz[u]+=siz[to[i]];
if(siz[Heavy]<siz[to[i]]) Heavy=to[i];
}
son[u]=Heavy;
eF(i,u) if(to[i]!=Heavy) Ex(to[i]);
} void DFS2(int u){
dfn[u]=++cnt; b[cnt]=a[u];
if(son[u]) DFS2(son[u]);
eF(i,u) if(to[i]!=son[u]) DFS2(to[i]);
} inline void push_down(int i,int len){
if(!laz[i]) return;
int len2=len>>1;
laz[i<<1]=laz[i<<1|1]=laz[i];
if(laz[i]==1) dat[i<<1]=dat[i<<1|1]=0;
else dat[i<<1]=len-len2, dat[i<<1|1]=len2;
laz[i]=0;
} void Mdf(int i,int l,int r,int a,int b,int x){
if(r<a||b<l) return;
if(a<=l&&r<=b) {dat[i]=x?r-l+1:0; laz[i]=x+1; return;}
push_down(i,r-l+1);
int mid=l+r>>1;
Mdf(i<<1,l,mid,a,b,x); Mdf(i<<1|1,mid+1,r,a,b,x);
dat[i]=dat[i<<1]+dat[i<<1|1];
} int Qur(int i,int l,int r,int a,int b){
if(r<a||b<l) return 0;
if(a<=l&&r<=b) return dat[i];
push_down(i,r-l+1);
int mid=l+r>>1;
return Qur(i<<1,l,mid,a,b)+Qur(i<<1|1,mid+1,r,a,b);
} inline void Mdf(int y){
int x=1;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
Mdf(1,1,n,dfn[top[x]],dfn[x],1);
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
Mdf(1,1,n,dfn[x],dfn[y],1);
} inline int Qur(int y){
int x=1,ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ans=ans+Qur(1,1,n,dfn[top[x]],dfn[x]);
x=faz[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return ans+Qur(1,1,n,dfn[x],dfn[y]);
} int main(){
scanf("%d",&n);
int x; F2(i,1,n) scanf("%d",&x), ins(x+1,i+1), faz[i+1]=x+1;
++n;
dep[1]=1; DFS(1); Ex(1);
DFS2(1);
scanf("%d",&q);
F(i,1,q){
char s[10];
int x,y;
scanf("%s%d",s,&x); ++x;
if(s[0]=='i') y=Qur(x), Mdf(x), printf("%d\n",Qur(x)-y);
else printf("%d\n",Qur(1,1,n,dfn[x],dfn[x]+siz[x]-1)), Mdf(1,1,n,dfn[x],dfn[x]+siz[x]-1,0);
}
return 0;
}
【算法学习】【洛谷】树链剖分 & P3384 【模板】树链剖分 P2146 软件包管理器的更多相关文章
- 【Luogu】P2146软件包管理器(树链剖分)
题目链接 上午跟rqy学了一道超难的概率题,准备颓一会,于是水了这么一道水题. 话说这题真的是模板啊.数据范围正好,描述特别贴近(都不给你绕弯子的),连图都给你画出来,就差题目描述加一句“树链剖分模板 ...
- Luogu P2146 软件包管理器(树链剖分+线段树)
题意 给定\(n\)个软件包,每个软件包都有一个依赖软件包,安装一个软件包必须安装他的依赖软件包,卸载一个软件包必须先卸载所有依赖于它的软件包.给定\(m\)此操作,每次一个操作\(install/u ...
- 洛谷 - P2146 - 软件包管理器 - 重链剖分
https://www.luogu.org/problem/P2146 继续重链剖分. 这里好像很好懂,每次安装软件就区间改值赋值整个路径是1,然后比较前后的sum值变化就可以了.事实上后一次的sum ...
- 洛谷 [P2146] 软件包管理器
树剖 将一个软件是否安装,看作是sum数组的0或1,对于每个操作前后sum[1]的变化,就是所求 #include <iostream> #include <cstdio> # ...
- NOI2015 软件包管理器(树链剖分+线段树)
P2146 软件包管理器 题目描述 Linux用户和OSX用户一定对软件包管理器不会陌生.通过软件包管理器,你可以通过一行命令安装某一个软件包,然后软件包管理器会帮助你从软件源下载软件包,同时自动解决 ...
- 洛谷 P2146 [NOI2015]软件包管理器 (树链剖分模板题)
题目描述 Linux用户和OSX用户一定对软件包管理器不会陌生.通过软件包管理器,你可以通过一行命令安装某一个软件包,然后软件包管理器会帮助你从软件源下载软件包,同时自动解决所有的依赖(即下载安装这个 ...
- 洛谷 P2146 [NOI2015]软件包管理器 树链剖分
目录 题面 题目链接 题目描述 输入输出格式 输入格式: 输出格式: 输入输出样例 输入样例#1: 输出样例#1: 输入样例#2: 输出样例#2: 说明 说明 思路 AC代码 总结 题面 题目链接 P ...
- [luogu P3384] [模板]树链剖分
[luogu P3384] [模板]树链剖分 题目描述 如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作1: 格式: 1 x y z 表示将树从x到y结点 ...
- BZOJ_4196_[NOI2015]_软件包管理器_(树链剖分)
描述 http://www.lydsy.com/JudgeOnline/problem.php?id=4196 给出一棵树,树上点权为0或1.u权值为1的条件是从根节点到u路径上的所有点权值都为1.u ...
随机推荐
- 数据结构之二叉树java实现
二叉树是一种非线性数据结构,属于树结构,最大的特点就是度为2,也就是每个节点只有一个左子树和一个右子树.二叉树的操作主要为创建,先序遍历,中序遍历,后序遍历.还有层次遍历.遍历有两种方式,一是采用递归 ...
- JVM中各种变量保存位置
Java中变量分为静态变量,实例变量,临时变量.那么各种变量具体保存在JVM中的何处呢? 1 静态变量:位于方法区. 2 实例变量:作为对象的一部分,保存在堆中. 3 临时变量:保存于栈中,栈随线程的 ...
- Python - 静态函数(staticmethod), 类函数(classmethod), 成员函数 区别(完全解析)
原文地址:http://blog.csdn.net/caroline_wendy/article/details/23383995 还有一篇:http://blog.csdn.net/carolzha ...
- 【洛谷4005】小Y和地铁(搜索)
[洛谷4005]小Y和地铁(搜索) 题面 洛谷 有点长. 题解 首先对于需要被链接的两个点,样例中间基本上把所有的情况都给出来了. 但是还缺了一种从下面绕道左边在从整个上面跨过去在从右边绕到下面来的情 ...
- 洛谷 P3235 [HNOI2014]江南乐 解题报告
P3235 [HNOI2014]江南乐 Description 两人进行 T 轮游戏,给定参数 F ,每轮给出 N 堆石子,先手和后手轮流选择石子数大于等于 F 的一堆,将其分成任意(大于1)堆,使得 ...
- MyBatis.2剖析
上次给大家介绍了一下properties 和 environments 的配置, 接下来就正式开始看源码了: 上次例子中,我们以 SqlSessionFactoryBuilder 去创建 SqlSes ...
- Android中全屏 取消标题栏,TabHost中设置NoTitleBar的三种方法(转)
Android中全屏 取消标题栏,TabHost中设置NoTitleBar的三种方法http://www.cnblogs.com/zdz8207/archive/2013/02/27/android- ...
- 解题:洛谷2093 JZPFAR
题面 初见K-D Tree 其实这样的题(欧几里得距离第$x$近点对)不应该用K-D Tree做,因为会被构造数据卡成$O(n^2)$,随机的另说. 但是并没有找到合适的K-D Tree的题(区域统计 ...
- 团体程序设计天梯赛 L3-012. 水果忍者
/*对于一条满足条件的直线,向下移,直到触碰一条线段的下端点,仍然经过其它线段,该直线仍然满足条件 即以一条线段的下(上)端点作为直线上的一点,求为了经过一条线段的最小.最大斜率值(mink,maxk ...
- POI的XWPFParagraph.getRuns分段问题 多余逗号
POI的XWPFParagraph.getRuns分段问题 2018年08月28日 09:49:32 银爪地海贼 阅读数:376 这是我的模板 后台打印出来是分段的 造成这样的原因是${name} ...