[数据结构]树状数组的基本应用
#[数据结构]树状数组的基本应用
一、树状数组简介
这部分内容只是对树状数组的简单复习,如果您不熟悉树状数组,可以自行百度或参考其他关于树状数组的博客。
树状数组是一种使用数组来模拟"树"的数据结构。树状数组的核心是
lowbit(x)
,设p=二进制下最低位1到结尾的0的个数,那么lowbit(x)=\(2^p\)。
例:lowbit(6)=lowbit(\((110)_2\))=2;
lowbit(8)=lowbit(\((1000)_2\))=8;我们已经知道lowbit(x)所表达的含义,下面给出计算公式:
lowbit(x)=x&(-x);
关于该公式的推导已经超出了我们讨论的范围,因此这里不再证明。
特别要注意的是,我们在做题的时候是不能计算lowbit(0)的,即不能调用数组下标0。由于lowbit(0)=0,调用的话会陷入死循环。为什么使用
lowbit(x)
?为了避免查询与修改两种操作出现过高的复杂度(以前缀和为例,其区间查询的复杂度很低,而单点修改的复杂度却很高),树状数组得以在O(logn)的复杂度内完成单点修改以及区间查询。
下面给出树状数组操作时的模型图:(图片摘自百度百科)
下图为查询a[1]~a[7]
的权值和:
7-lowbit(7)=6;6-lowbit(6)=4;4-lowbit(4)=0;
二、树状数组的基本应用
1.区间查询、单点修改
最基础的用法。线段树可以实现树状数组的所有功能,但是树状数组的代码简短,可实施性强。因此在不必要的情况下不使用线段树。
例1: P3374【模板】树状数组1
题目描述:已知一个数列,你需要进行下面两种操作:1.将某一个数加上x; 2.求出某区间每一个数的和
思路:模板题。
代码:
#include<bits/stdc++.h>
#define N 500100
using namespace std;
int n,m,f[N];
inline int lowbit(int x){return x&(-x);}
void Update(int x,int s){
while(x<=n){f[x]+=s;x+=lowbit(x);}
}
int Add(int x){
int ans=0;
while(x>0){ans+=f[x];x-=lowbit(x);}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1,d;i<=n;i++){
scanf("%d",&d);
Update(i,d);
}
for(int i=1,a,x,y;i<=m;i++){
scanf("%d%d%d",&a,&x,&y);
if(a==1) Update(x,y);
else printf("%d\n",Add(y)-Add(x-1));
}
return 0;
}
2.单点查询、区间修改
我们发现,应用二与一所解决的问题颠倒了。这样的话怎么处理呢?
我们引用一种新的方法:差分
设原数组a={1,3,2,4,8,6,7},则对应的差分数组b={1,2,-1,2,4,-2,1}
得到规律:
\[\sum_{i=1}^k b_i=a_k\]
\[b_i=a_i-a_{i-1}\]
最后的问题在于如何在差分数组上表示区间修改。
如下图:
每次区间加一个值val
,只会对差分数组的\(b_x\)、\(b_{y+1}\)有影响。
所以我们只需要修改两个值即可:update(x,val);update(y+1,-val);
例2: P3368【模板】树状数组2
题目描述:已知一个数列,你需要进行下面两种操作:1.将某区间每一个数数加上x;2.求出某一个数的值
思路:典型的差分思想。
代码:
#include<bits/stdc++.h>
#define N 500100
using namespace std;
int n,m,f[N];
inline int lowbit(int x){return x&(-x);}
void Update(int x,int s){
while(x<=n){f[x]+=s;x+=lowbit(x);}
}
int Sum(int x){
int ans=0;
while(x>0){ans+=f[x];x-=lowbit(x);}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
int last=0;
for(int i=1,now;i<=n;i++){
scanf("%d",&now);
Update(i,now-last);//预处理
last=now;
}
for(int i=1,a,x,y,k;i<=m;i++){
scanf("%d",&a);
if(a==1){
scanf("%d%d%d",&x,&y,&k);
Update(x,k);//差分
Update(y+1,-k);
}
else{
scanf("%d",&x);
printf("%d\n",Sum(x));
}
}
return 0;
}
3.求逆序对
例3: P1908 逆序对
题目描述:求序列中逆序对的个数
思路:我们每次读取一个值,就把这个值对应树状数组的下标加1,之后询问小于这个值的和,此时是比这个值小的数的个数。用这个值减去这个值小的数的个数就是这个数的逆序对。
代码:
#include<bits/stdc++.h>
#define ll long long
#define N 500010
using namespace std;
ll n,t[N],a[N],p[N],b[N],q[N],ans;
inline ll lowbit(ll x){
return x&(-x);
}
inline void modify(ll x,ll k){
while(x<=n){
t[x]+=k;
x+=lowbit(x);
}
}
inline ll query(ll x){
ll res=0;
while(x>0){
res+=t[x];
x-=lowbit(x);
}
return res;
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
b[i]=a[i];
}
sort(b+1,b+n+1);
int m=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++){
int pos=lower_bound(b+1,b+m+1,a[i])-b;
q[i]=pos;
}//离散化处理
for(int i=1;i<=n;i++){
modify(q[i],1);//统计逆序对个数
ans+=i-query(q[i]);
}
printf("%lld",ans);
return 0;
}
例4: UVA10810 Ultra-QuickSort
题目描述:使一个序列有小到大排列的两个数最小交换次数
思路:只有\(a_i>a_j\)是才会交换,故转化为逆序对处理。
代码:
#include<bits/stdc++.h>
#define N 500010
using namespace std;
int a[N],tree[N],maxpos,n;
inline int lowbit(int x){return x&(-x);}
inline void modify(int x,int k){
while(x<=maxpos){
tree[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x){
int res=0;
while(x>0){
res+=tree[x];
x-=lowbit(x);
}
return res;
}
int main()
{
while(scanf("%d",&n)&&n){
maxpos=0;
memset(tree,0,sizeof(tree));
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i]++;
maxpos=max(maxpos,a[i]);
}
int ans=0;
for(int i=n;i>=1;i--){
ans+=query(a[i]-1);
modify(a[i],1);
}
printf("%d\n",ans);
}
return 0;
}
例5: P1774 最接近神的人_NOI导刊2010提高(02)
思路:一道双倍经验题。与例三完全一致。
原文地址:https://www.cnblogs.com/cyanigence-oi/p/11708420.html
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 精华 | SQL注入万能Bypass技巧
- 【STM32F429开发板用户手册】第31章 STM32F429的SPI总线基础知识和HAL库API
- Linux 查找当前目录下所有包含指定内容的文件
- 父子管道更有效地扩展应用及其存储库结构
- Go语言 | 你还在这样获取文件的大小吗?
- 如何在Gitlab流水线中对部署进行控制?
- 工具的使用 | Hydra暴力破解工具的用法
- “灯下黑”应用在windows隐身后门中的实践
- 渗透测试-信息收集命令总结
- 什么?一个核同时执行两个线程?
- 如何下载网页上的视频?
- c++ int,unsigned int混合表达式类型转换
- MySQL5.7+查看Waiting for table metadata lock 锁情况
- input如何快速进行规则校验
- 史上最详细的sqlServer手工注入详解