2025dsfz集训Day9:树状数组、LCA、RMQ
Day8 I:树状数组
\]
\]
后记:关于本文的所有题目,请吸氧(\(O2\))+开 \(long\ long\)
关于树状数组
顾名思义就是一个结构为树形结构的数组,于二叉树的结构类似但又不同,它是在二叉树的结构上删除了一些中间节点.
树状数组可以解决大部分区间上面的修改以及查询的问题,例如:
- 1.单点修改,单点查询,
- 2.区间修改,单点查询,
- 3.区间查询,区间修改,
换言之,线段树能解决的问题,树状数组大部分也可以,但是并不一定都能解决,因为线段树的扩展性比树状数组要强.
有人会问了既然线段树的问题能够用树状数组解决而且线段树还比树状数组扩展性强,那为什么不直接用线段树呢?
- 树状数组的作用就是为了简化线段树.基于某些简单的问题,我们没必要用到功能性强但实现复杂的线段树.
优点:修改和查询操作复杂度于线段树一样都是logN,但是常数比线段树小,并且实现比线段树简单
缺点:扩展性弱,线段树能解决的问题,树状数组不一定能解决.
树状数组结构
设黑色框内数组为 \(A[1]\to A[8]A[1]→A[8]\)
那么可以得到以下式子:
\]
\]
\]
\]
\]
\]
\]
\]
我们便可以得到 \(C[i] = A[i - 2^k+1] + A[i - 2^k+2] + ... + A[i]\)
在这里,\(k\) 为 \(i\) 的二进制中从最低位到高位连续零的长度
那么,如何求出二进制中从最低位到高位连续零的长度呢?
我们需要找最低位的 \(1\)!!!
如何找最低位的 \(1\) 呢?
我们需要引入 \(lowbit\)
点击查看 $lowbit$ 模版
inline int lowbit(int x){
return x&(-x);
}
\(\&\) 运算,即与运算,即按位比较都是 \(1\) 则为 \(1\),否则为 \(0\)。
\(lowbit\) 的原理简单说一下:
在计算机中二进制是以补码存储的。对于 \(x(x>0)\),他的补码就是他的本身. 而 \([−x]\) 补为 \([x]\) 补连同符号位取反加一之后的结果 所以 \([-x]\) 补 \(\&[x]\) 补刚好就是最低位 \(1\) 的结果
总结一下规律:\(x\&(-x)\),当 \(x\) 为 \(0\) 时结果为 \(0\) ; \(x\) 为奇数时,结果为 \(1\) ; \(x\) 为偶数时,结果为 \(x\) 中 \(2\) 的最大次方的因子。用处呢就是找最低位的 \(1\) 的位置。
(其实树状数组就是个特殊的前缀和数组)
树状数组操作
加点
点击查看代码
void update(int pos,int num){
while(pos<=n){
tree[pos]+=num;
pos+=pos&-pos;
}
}
查询
点击查看代码
long long query(int pos){
long long res = 0;
while(pos){
res += tree[pos];
pos -= pos&-pos;
}
return res;
}
例题
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n,m,a[100015],tree[100015];
void update(int pos,int num){
while(pos<=n){
tree[pos]+=num;
pos+=pos&-pos;
}
}
long long query(int pos){
long long res = 0;
while(pos){
res += tree[pos];
pos -= pos&-pos;
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i = 1;i <= n;i++){
cin>>a[i];
update(i,a[i]);
}
for(int i = 1;i <= m;i++){
int op;
cin>>op;
if(op == 0){
int l,r;
cin>>l>>r;
cout<<query(r)-query(l-1)<<"\n";
}else{int pos,x;
cin>>pos>>x;
update(pos,x);
}
}
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;
long long n,m,x[100015],y[1010101],tree[100015];
void update(int pos,int num){
pos++;
while(pos<100005){
tree[pos]+=num;
pos+=pos&-pos;
}
}
long long query(int pos){
long long res = 0;
pos++;
while(pos){
res += tree[pos];
pos -= pos&-pos;
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(long long i = 1;i <= n;i++){
cin>>x[i]>>y[i];
cout<<query(x[i])<<"\n";
update(x[i],1);
}
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;
long long cnt,n,m,x[100015],y[1010101];
struct tree{
int c[101010];
void update(int pos,int num){
while(pos<=n){
c[pos]+=num;
pos+=pos&-pos;
}
}
long long query(int pos){
long long res = 0;
while(pos){
res += c[pos];
pos -= pos&-pos;
}
return res;
}
}t1,t2;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
memset(t1.c,0,sizeof t1.c);
memset(t2.c,0,sizeof t2.c);
cin>>n>>m;
int k;
for(int i = 1;i<= m;i++){
cin>>k;
int l,r;
if(k == 1){
cin>>l>>r;
t1.update(l,1);
t2.update(r,1);
cnt++;
}else{
cin>>l>>r;
cout<<cnt-t1.query(n)+t1.query(r)-t2.query(l-1)<<"\n";
}
}
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int tree[1000005],k;
int n,m;
void update(int pos,int num){
while(pos<=n){
tree[pos]+=num;
pos+=pos&-pos;
}
}
long long query(int pos){
long long res = 0;
while(pos){
res += tree[pos];
pos -= pos&-pos;
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i = 1;i<= m;i++){
char s;
cin>>s;
int l,r;
if(s == 'A'){
cin>>l;
cout<<query(l)<<"\n";
}else if(s == 'B'){
cin>>l>>r;
update(l,r);
}else{
cin>>l>>r;
update(l,-r);
}
}
}
\(LCA\) 与 \(RMQ\)
\(RMQ\) 全称 \(Range Maximum/Minimum Query\)
做法很多,比较优秀的是 \(ST\) 表。\(Sparse Table\),稀疏表
\(ST\) 表
- \(st\) 表是用来解决可重复贡献问题的一种数据结构。
- 考虑设 \(st[i][j]\) 表示 \(i\) 到 \(i+(2^j)-1\) 这段区间的最大值。
- 那么可以转移: \(st[i][j]=max(st[i][j-1],st[i+2^(j-1)][j-1])\)
- 区间查询:设 \(k=log(r-l+1)\) 向下取整。那么区间最大值即为
\(max(st[l][k],st[r-(2^k)+1][k])\) - 时间复杂度:预处理 \(O(nlogn)\),查询 \(O(1)\)。
- 空间复杂度 \(O(nlogn)\)。
- 其他可重复贡献问题也可解决。
优化
一个小但复杂的优化:\(Four\ Russian\)(四毛子)
考虑将序列每 \(B\) 个分成一块,总块数 \(\frac{n}{B}\)。
对于每块求出最小值,得到一个长度为 n/B 的数组,对这个数组做 st 表。
再对每个块内分别做 st 表。
查询:可分成两个左右的散块和中间一段整块,三次 st 表查询。
分析复杂度:预处理为 \(O(\frac{n}{B}log\frac{n}{B}+\frac{n}{B}×BlogB)\),在 \(B\) 取 \(log\ n\) 时可得到理论最优复杂度为预处理 \(O(nloglogn)\),查询 \(O(1)\),空间也为 \(O(n loglogn)\)。
然而虽然预处理很优秀,但是每次查询需要三个 \(st\) 表,常数很大。且由于代码不好写,并且一般的算法竞赛并没有很高的复杂度要求,因此很不常用。(嘻嘻)
基于状压的线性 \(RMQ\) 算法
对于一个特殊情况的进一步优化:\(+1-1RMQ。\)
也即:序列相邻元素差值为 \(1\) 或 \(-1\) ,求 $RMQ $。
我们注意到 \(B=logn\) 时瓶颈在于块内 \(st\) 表处理。
而由于差为 \(1\) 或 \(-1\),且每次对散块的询问都是一个长度不超过 \(B=logn\) 的序列,在序列头元素固定的情况下,这样的序列只有 \(∑_{i=1}^{logn}2^i\) 种,这个值为 \(O(n)\) 。我们可以对每种情况预处理此时最小值-头元素的结果,这个可以直接递推。于是我们就做到了 \(O(n)\) 预处理,\(O(1)\) 查询的 \(RMQ\) 算法,仅限于 \(+1-1\) 序列。
重新回到平凡情况,我们审视一下四毛子算法。
对于每个块真的需要 都做 \(st\) 表吗?其实如果询问的 \(l,r\) 不在同一个块内,我们可以直接预处理每个块的前缀、后缀最小值来做到 \(O(n)\) 预处理 \(O(1)\) 查询。
关键就在于如果询问的 \(l,r\) 在同一个块内如何去做。
考虑使用单调栈:预处理的时候从 \(1\) 到 \(n\) 枚举 \(r\),处理每个 \(r\) 的单调递增栈。那么 \(l\) 到 \(r\) 的最小值就是栈中第一个在 \(l\) 后的位置。
- 我们可以用一个二进制整数去存储以 \(r\) 为右端点,长度为 \(logn\) 的一个后缀里面每个数是否在单调栈内。那么查询就只要询问一个二进制数的最低的1。使用 \(\_\_builtin\_ctz\) 即可 \(O(1)\) 查询(或者 \(log.lowbit\))。
此方法即基于状压的线性 \(RMQ\) 算法。
其实讲了这么多实际上用的还是平凡的 \(RMQ\)…
LCA
- \(LCA\):最近公共祖先(\(Lowest\ Common\ Ancestor\))
求LCA
如何求两点的最近公共祖先?
- 朴素求法:每次选择深度较大的那个点向上跳到父亲,不断重复直到两个点重合,即为LCA。
复杂度:预处理 \(VBN MN M O(n)\),单次询问 \(O(n)\)。
倍增
倍增法:最经典的求 \(LCA\) 的方法。
和 \(ST\) 表中的倍增类似,我们设 \(f[u][i]\) 表示节点 \(u\) 的 \(2^i\) 级祖先。\(1\) 级祖先即为父亲。
- 转移:\(f[u][i]=f[f[u][i-1]][i-1]\)。
有了这个就可以 \(O(log n)\) 求某个点的 \(k\) 级祖先:将 \(k\) 二进制分解。
求 \(LCA\):首先将两个点跳到深度相等。然后从大到小枚举 \(i\),如果 \(f[u][i]\) 和 \(f[v][i]\) 不相等就将两个点都向上跳成 \(f[u][i]\) 和 \(f[v][i]\)。这样最终 \(f[u][0]\) 就是答案。
时间复杂度:预处理 \(O(nlogn)\),单次询问 \(O(logn)\)
空间复杂度:\(O(nlogn)\)。
DFS
用 \(dfs\) 序求 \(lca\):假设询问的点为 \(x,y\)。
不妨设 \(x\) 的 \(dfs\) 序在 \(y\) 前面。\(dfn[x]<dfn[y]\)
求出 \(dfs\) 序在 \([ dfn[x]+1, dfn[y] ]\) 内的结点中深度最小的那个,设为 u,则答案为
\(fa[u]\)。
要特判 $x==y $。
求深度最小值可用 \(RMQ\) 来求,复杂度为 \(O(nlogn) – O(1)\)。
单次 \(O(1)\),非常优秀。
- 当然也可以用 \(+1-1RMQ\) 做到 \(O(n)-O(1)\)
离线 tarjan LCA
使用并查集。
首先离线,\(dfs\) 整棵树,在回溯某个结点的时候将他合并到他父亲的并查集上。
在回溯点 \(u\) 时枚举其中一个结点为 \(u\) 的所有询问 \((u,v)\),如果 \(v\) 已经被回溯过,那么 \(v\) 所在的并查集即为答案。
时间复杂度:\(O(mα(m+n)+n)\)。优点是复杂度优秀,缺点是需要离线。
2025dsfz集训Day9:树状数组、LCA、RMQ的更多相关文章
- HDU 5293 Tree chain problem 树形dp+dfs序+树状数组+LCA
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=5293 题意: 给你一些链,每条链都有自己的价值,求不相交不重合的链能够组成的最大价值. 题解: 树形 ...
- HDU4836 The Query on the Tree(树状数组&&LCA)
由于智力的问题,百度之星完全lu不动..开场看第一题根据题目给的条件我觉得一定是可以构造出来的,题目给的意思颇有鸽巢原理的感觉,于是觉得开场第一题应该就是智力构造题了,想了半个小时,发现完全想不动,于 ...
- BZOJ3881[Coci2015]Divljak——AC自动机+树状数组+LCA+dfs序+树链的并
题目描述 Alice有n个字符串S_1,S_2...S_n,Bob有一个字符串集合T,一开始集合是空的. 接下来会发生q个操作,操作有两种形式: “1 P”,Bob往自己的集合里添加了一个字符串P. ...
- HDU 5293 Annoying problem 树形dp dfs序 树状数组 lca
Annoying problem 题目连接: http://acm.hdu.edu.cn/showproblem.php?pid=5293 Description Coco has a tree, w ...
- 【BZOJ2819】Nim 树状数组+LCA
[BZOJ2819]Nim Description 著名游戏设计师vfleaking,最近迷上了Nim.普通的Nim游戏为:两个人进行游戏,N堆石子,每回合可以取其中某一堆的任意多个,可以取完,但不可 ...
- 【BZOJ4999】This Problem Is Too Simple! 离线+树状数组+LCA
[BZOJ4999]This Problem Is Too Simple! Description 给您一颗树,每个节点有个初始值. 现在支持以下两种操作: 1. C i x(0<=x<2 ...
- 求解区间问题的三种做法的区别 线段树、树状数组、RMQ
树状数组主要用于计算区间的和,在区间元素修改值的时候能够快速修改而不是以O(n)的复杂度进行修改: 线段树是把区间以树的形式分拆为若干个小区间,每个小区间存的都有一个值(树状数组的元素存的是区间值), ...
- [CSP-S模拟测试]:异或(树状数组+LCA)
题目传送门(内部题21) 输入格式 第一行一个字符串$str$,表示数据类型.第二行一个正整数$k$,表示集合$K$的大小,保证$k>1$.接下来$k$行每行$k$个数,第$i$行第$j$个数表 ...
- POJ 2763"Housewife Wind"(DFS序+树状数组+LCA)
传送门 •题意 一对夫妇居住在 xx村庄,给村庄有 $n$ 个小屋: 这 $n$ 个小屋之间有双向可达的道路,不会出现环,即所构成的图是个树: 从 $a_i$ 小屋到 $b_i$ 小屋需要花费 $w_ ...
- LCA + 树状数组 + 树上RMQ
题目链接:http://poj.org/problem?id=2763 思路:首先求出树上dfs序列,并且标记树上每个节点开始遍历以及最后回溯遍历到的时间戳,由于需要修改树上的某两个节点之间的权值,如 ...
随机推荐
- 腾讯云HAI与DeepSeek携手打造私有化高效代码助手
今天,我们依然以DeepSeek-R1大模型为核心,继续探索其在实际场景中的可用性.今天的重点将放在基于DeepSeek-R1大模型,结合JetBrains IDEA 插件代码助手(CodeGPT)进 ...
- 通过Ollama本地部署DeepSeek R1以及简单使用
本文介绍了在Windows环境下,通过Ollama来本地部署DeepSeek R1.该问包含了Ollama的下载.安装.安装目录迁移.大模型存储位置修改.下载DeepSeek以及通过Web UI来对话 ...
- yum repo和rpm,添加阿里repos
RPMRPM(Red-hat Package Manager),是一个由红帽最早开发出来的包管理器,目前已经是大多数Linux发行的默认包管理器.RPM管理的包都是以.rpm结尾,其中存储了该软件的安 ...
- 【java】对list集合进行去重 传统方式 VS Lambda
说道集合的去重,我们就肯定能想到唯一元素集合set,还有map中的key.那么我们传统的去重的方式肯定是要选择set或者是map来实现了,另外在对实体对象类型进行去重的时候,我们可能会指定实体中的一个 ...
- QT5笔记:2.可视化UI设计
2.可视化UI设计 参考视频:https://www.bilibili.com/video/BV1AX4y1w7Nt 窗口的三种类型,每种类型窗体拥有的方法不同: MainWindow:指的是正常窗体 ...
- wikidata介绍和查询
Wikidata是一个大型结构化开源知识图,为维基百科等项目提供支持.我们可使用SPARQL(Wikidata官方Tutorial)对其进行查询.SPARQL是一种专为 RDF(Resource ...
- pnpm:无法加载文件 C:\Users\Five\AppData\Roaming\npm\pnpm.ps1 ,因为在此系统上禁止运行脚本
前言 重装完了电脑系统,运行pnpm 无法加载文件,pnpm -V也不行 解决方案 用管理员方式启动power shell 输入命令:set-ExecutionPolicy RemoteSigned ...
- Selenium KPI接口- 鼠标案例
鼠标操作 实现功能:百度页面->移动到'设置'按钮->右键点击/鼠标双击/鼠标拖拽到元素松开. 首先导入ActionChains方法 使用格式:ActionChains(driver).操 ...
- manim边学边做--线性变换的场景类
在数学可视化领域,Manim的LinearTransformationScene类为线性代数教学提供了强大的工具. 这个专门设计的场景类继承自VectorScene,通过内置的变换动画和坐标系管理,能 ...
- Web前端入门第 12 问:HTML 常用属性一览
HELLO,这里是大熊学习前端开发的入门笔记. 本系列笔记基于 windows 系统. HTML 常用属性大约 70 个,是否又头大了?脸上笑嘻嘻,心里嘛...嘿嘿... 温馨提示:属性不用死记硬背, ...