【算法学习】【洛谷】树链剖分 & 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 ...
随机推荐
- P1896 [SCOI2005]互不侵犯
题目描述 在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案.国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子. 注:数据有加强(2018/4/25) ...
- jingchi.ai 2017.11.25-26 Onsite面试
时间:2017.11.25 - 11.26 地点:安徽安庆 来回路费报销,住宿报销. day1: 大哥哥问了我一个实际中他们遇到的问题.有n个点,将点进行分块输出,输出各个块的均值点.具体就是100* ...
- LEP所需环境
一.LEP所需环境 Python 3.6 Flask Docker 二.Python安装 LEP必须在Python3.6环境下运行,如果是在Python2.7下运行会报以下错误! Python3.6的 ...
- 使用IDEA远程部署tomcat和调试
环境: CentOS 7 Tomcat 9.0.1 jdk-9.0.1 IntelliJ IDEA 2017.3 Tomcat中的配置 在catalina.sh文件中加入以下的配置 CATALINA_ ...
- 【BZOJ2655】Calc(拉格朗日插值,动态规划)
[BZOJ2655]Calc(多项式插值,动态规划) 题面 BZOJ 题解 考虑如何\(dp\) 设\(f[i][j]\)表示选择了\(i\)个数并且值域在\([1,j]\)的答案. \(f[i][j ...
- ASP.NET MVC API 接口验证
项目中有一个留言消息接口,接收其他系统的留言和展示留言,参考了网上的一些API验证方法,发现使用通用权限管理系统提供的验证方法最完美(http://www.cnblogs.com/jirigala/p ...
- Python 字符串前面加u,r,b的含义
1.字符串前加 u 例:u"我是含有中文字符组成的字符串." 作用: 后面字符串以 Unicode 格式 进行编码,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时 ...
- android中service启动后台程序
Service是Android中一个类,它是Android四大组件之一,使用Service可以在后台执行长时间的操作( perform long-running operations in the b ...
- Shell记录-Shell命令(其他)
top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器. .命令格式 top [参数] Shell 2.命令功能 显示当前系统正在执行的 ...
- negativeView 的使用
参考链接:http://blog.csdn.net/u012702547/article/details/51253222 1.一般来讲,是配合drawerLayout使用的,在xml文件中声明,其中 ...