c++ 树状数组
关于树状数组
树状数组,即 Binary Indexed Tree ,主要用于维护查询前缀和
属于 log 型数据结构
和线段树比较
都是 log 级别
树状数组常数、耗费的空间、代码量都比线段树小
树状数组无法完成复杂的区间操作,功能有限
树状数组介绍
二叉树大家一定不陌生
然而真实的树状数组省去了一些空间
其中黑色的是原数组,红色的是树状数组
根据图可以看出 S[] 的由来
S[1] = A[1]
S[2] = A[1] + A[2]
S[3] = A[3]
S[4] = A[1] + A[2] + A[3] + A[4]
按照上面的规律:
S[5] = A[5]
S[6] = A[5] + A[6]
······
可以发现:这颗树是有规律的
S[i] = A[i-2k+1] + A[i-2k+2] + ··· + A[i]
其中 k 是 i 在 2 进制下末尾连续 0 的个数
比如 i=4D=100B 则 k=2
那如何求和呢,如要求位置 6 的和,就应该是 S[6]+S[4]
根据上式可以算出每个位置的前缀和 V[i]=S[i]+S[i-2k1]+S[(i-2k1)-2k2]+ ···
新的问题来了: 2k 怎么求?
有两种方法: i&(i^(i-1)) 和 i&-i ,他们统一叫做 lowbit
lowbit 原理
lowbit 相当于求二进制从末尾到第一个 1 这一段
如 lowbit(1010B)=10B
方法 1
i-1 就是 i 在二进制中从末尾到末尾第一个 1 全部取反
如 20D=10100B 19D=10011B
把它们位异或一下,使得末尾有若干个 1 ,并去掉了前面相同的部分,如 10100B^10011B=00111B
再与原数位与一下,由于除了原数末尾第一个 1 以外都不同,所以其余都是 0
如 10100B&00111B=100B ,就是 lowbit 了
方法 2
然而现实中用的更多还是这个也许这个好记
-x 即为 x 的反码加一
而反码在加一时由于取反了,后面有一段都是 1 ,所以就会一直进位直到遇到 0 并使其变成 1
由于取反了,只有那一位 1 是相同的,这样只要位与一下,只留下那个 1 就行了
树状数组的操作
既然 get 到了精髓,后面的操作也简单了许多
约定
变量名 | 意义 |
---|---|
n | 原数组长度 |
t[] | 树状数组 |
单点修改
上面说了 S[i] = A[i-2k+1] + A[i-2k+2] + ··· + A[i]
那既然 A[i] 修改了, S[i+2k] 、 S[i+2k+2k] ··· 都被修改了
inline void add(int p,int v){
for(;p<=n;p+=p&-p)
t[p]+=v;
}
单查
前面也给出公式了,直接循环
inline int sum(int p){
register int ans=0;
for(;p;p-=p&-p)
ans+=t[p];
return ans;
}
区查
有了前缀和自然可以求区间和
直接返回sum(r)-sum(l-1)
例题
单修 + 区查
洛谷 P3374
前面已经讲过了
#include<bits/stdc++.h>
using namespace std;
inline char nc(){
static char buf[100000],*S=buf,*T=buf;
return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
}
inline int read(){
static char c=nc();register int f=1,x=0;
for(;c>'9'||c<'0';c=nc()) c==45?f=-1:1;
for(;c>'/'&&c<':';c=nc()) x=(x<<3)+(x<<1)+(c^48);
return x*f;
}
char fwt[100000],*ohed=fwt;
const char *otal=ohed+100000;
inline void pc(char ch){
if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
*ohed++=ch;
}
inline void write(int x){
if(x<0) pc('-'),x=-x;
if(x>9) write(x/10);
pc(x%10+'0');
}
int n,m,opt,x,y,t[500002];
inline void add(int p,int v){
for(;p<=n;p+=p&-p)
t[p]+=v;
}
inline int sum(int p){
register int ans=0;
for(;p;p-=p&-p)
ans+=t[p];
return ans;
}
int main(){
n=read(),m=read();
for(register int i=1;i<=n;i++){
x=read();
add(i,x);
}
while(m--){
opt=read(),x=read(),y=read();
if(opt==1) add(x,y);
else write(sum(y)-sum(x-1)),pc('\n');
}
fwrite(fwt,1,ohed-fwt,stdout);
}
区改 + 单查
虽然看上去没大变化,但是如果按照之前的思路,复杂度为 \(O(mn\ log\ n)\) ,比普通数组还差
所以需要运用差分的思想,设 d[i] 为 a[i] 的差分数组,且 d[i]=a[i]-a[i-1]
那么 \(a_i = \sum\limits_{j=1}^i d_j\)
因为是单点查询,所以我们考虑直接维护 d 这个数组的前缀和
怎么区间修改?运用差分思想,可以先从 l 开始加上那个值,再从 r 开始减去那个值,最后求和时就相当于区间修改了
洛谷 P3368
#include<bits/stdc++.h>
using namespace std;
inline char gc(){
static char buf[100000],*S=buf,*T=buf;
return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
}
inline int read(){
static char c=gc();register int f=1,x=0;
for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
return x*f;
}
char fwt[100000],*ohed=fwt;
const char *otal=ohed+100000;
inline void pc(char ch){
if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
*ohed++=ch;
}
inline void write(int x){
if(x<0) x=-x,pc('-');
if(x>9) write(x/10);
pc(x%10+'0');
}
int n,m,opt,x,y,k,lst,t[500002];
inline void add(int p,int v){
for(;p<=n;p+=p&-p)
t[p]+=v;
}
inline int sum(int p){
register int ans=0;
for(;p;p-=p&-p)
ans+=t[p];
return ans;
}
int main(){
n=read(),m=read();
for(register int i=1;i<=n;i++){
x=read();
add(i,x-lst);
lst=x;
}
while(m--){
opt=read(),x=read();
if(opt==1){
y=read(),k=read();
add(x,k),add(y+1,-k);
}
else write(sum(x)),pc('\n');
}
fwrite(fwt,1,ohed-fwt,stdout);
}
区改 + 区查
还是运用差分思想,但是如何在差分数组中求前缀和呢?
已知 \(sum_i = \sum\limits_{j=1}^i a_j\)
把 a[j] 换成差分数组,得到 \(sum_i = \sum\limits_{j=1}^i \sum\limits_{k=1}^j d_k\)
可以看出每个元素出现的次数是递减的,变换一下,得 \(i*(d_1+d_2+d_3+···)-(0*d_1+1*d_2+2*d_3+···)\)
写成求和公式: \((i*\sum\limits_{j=1}^i d_j)-(\sum\limits_{j=1}^i (j-1)*d_j)\)
这时我们发现:后面那一部分可以用树状数组存下来,快速求和
洛谷 P3372
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline char gc(){
static char buf[100000],*S=buf,*T=buf;
return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
}
inline ll read(){
static char c=gc();register ll f=1,x=0;
for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
return x*f;
}
char fwt[100000],*ohed=fwt;
const char *otal=ohed+100000;
inline void pc(char ch){
if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
*ohed++=ch;
}
inline void write(ll x){
if(x<0) x=-x,pc('-');
if(x>9) write(x/10);
pc(x%10+'0');
}
ll x,y,ls,rs,tmp,t1[100005],t2[100005];
int n,m,opt,lst,k;
inline void add(int p,int v,ll t[]){
for(;p<=n;p+=p&-p)
t[p]+=v;
}
inline ll sum(int p,ll t[]){
ll ans=0;
for(;p;p-=p&-p)
ans+=t[p];
return ans;
}
int main(){
n=read(),m=read();
for(register int i=1;i<=n;i++){
x=read(),tmp=x-lst;
add(i,tmp,t1);
add(i,tmp*(i-1),t2);
lst=x;
}
while(m--){
opt=read(),x=read(),y=read();
if(opt==1){
k=read();
add(x,k,t1);
add(x,k*(x-1),t2);
add(y+1,-k,t1);
add(y+1,-k*y,t2);
}
else{
rs=y*sum(y,t1)-sum(y,t2);
ls=(x-1)*sum(x-1,t1)-sum(x-1,t2);
write(rs-ls),pc('\n');
}
}
fwrite(fwt,1,ohed-fwt,stdout);
}
高级操作
代替平衡树
你没看错,树状数组可以代替平衡树
这时我们的 t[] 维护的就是数字的个数。
sum 便是 x 的排名
加入 x 直接add(x,1);
,删除 x 直接add(x,-1);
求出第 k 大
可以直接二分
int kth(int k){
int l=1,r=n,mid;
while(l<r){
mid=(l+r)/2;
if(sum(mid)<k) l=mid+1;
else r=mid;
}
return r;
}
或者通过倍增进行差分(比二分快许多)
int kth(int k){
int ans=0;
for(int i=30;i>=0;i--){
ans+=(1<<i);
if(ans>n||t[ans]>=k)ans-=(1<<i);
else k-=t[ans];
}
return ++ans;
}
前驱
就是排名为 x-1 (小一点点)的数
kth(sum(x-1))
后继
就是排名比 x 大一的数
kth(sum(x)+1)
例题
洛谷 P3369
要注意一点:输入有负数,每输入一个就把它加上 107 ,输出时减去 107 ,那么 n 就应该是 2*107
#include<bits/stdc++.h>
using namespace std;
inline char gc(){
static char buf[100000],*S=buf,*T=buf;
return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
}
inline int read(){
static char c=gc();register int f=1,x=0;
for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
return x*f;
}
char fwt[100000],*ohed=fwt;
const char *otal=ohed+100000;
inline void pc(char ch){
if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
*ohed++=ch;
}
inline void write(int x){
if(x<0) x=-x,pc('-');
if(x>9) write(x/10);
pc(x%10+'0');
}
int T,opt,x,t[20000001],sz,n;
void add(int p,int v){
for(;p<=n;p+=(p&-p))
t[p]+=v;
}
int sum(int p){
int ans=0;
for(;p;p-=(p&-p))
ans+=t[p];
return ans;
}
int kth(int k){
int ans=0;
for(int i=30;i>=0;i--){
ans+=(1<<i);
if(ans>n||t[ans]>=k) ans-=(1<<i);
else k-=t[ans];
}
return ++ans;
}
//int kth(int k){
// int l=1,r=n,mid;
// while(l<r){
// mid=(l+r)/2;
// if(sum(mid)<k) l=mid+1;
// else r=mid;
// }
// return r;
//}
int main(){
n=20000000;
T=read();
while(T--){
opt=read(),x=read();
if(opt!=4) x+=10000000;
if(opt==1) add(x,1);
else if(opt==2) add(x,-1);
else if(opt==3) write(sum(x-1)+1),pc('\n');
else if(opt==4) write(kth(x)-10000000),pc('\n');
else if(opt==5) write(kth(sum(x-1))-10000000),pc('\n');
else write(kth(sum(x)+1)-10000000),pc('\n');
}
fwrite(fwt,1,ohed-fwt,stdout);
}
求逆序对数
同样,我们用 t[] 维护数字的个数
那么,对于第 i 个数 x ,逆序对数就是 i-sum(x)
离散化
如果直接维护,输入的数据较大(如 109 ),空间无法开那么大
所以可以定义一个结构体, val 表示数, id 表示下标
按照 val 从小到大排序, id 作为第二关键字,排完序后发现: id 等效与 val
这样空间只需开元素数量个
例题
洛谷 P1908
由于 add 的值始终是 1 ,所以简化了 add 函数
#include<bits/stdc++.h>
#define N 500005
using namespace std;
typedef long long ll;
inline char gc(){
static char buf[100000],*S=buf,*T=buf;
return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
}
inline int read(){
static char c=gc();register int f=1,x=0;
for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
return x*f;
}
struct opt{
int val,id;
bool operator < (const opt &x) const
{
if(val==x.val) return id<x.id;
return val<x.val;
}
bool operator > (const opt &x) const
{
if(val==x.val) return id>x.id;
return val>x.val;
}
}x[N];
void qs(int l,int r){
int i=l,j=r;
opt mid=x[rand()%(r-l)+l];
while(i<=j){
while(x[i]<mid) i++;
while(x[j]>mid) j--;
if(i<=j){
swap(x[i],x[j]);
i++,j--;
}
}
if(i<r) qs(i,r);
if(j>l) qs(l,j);
}
int n,t[N];
ll s;
inline void add(int p){
for(;p<=n;p+=p&-p)
t[p]++;
}
inline ll sum(int p){
ll ans=0;
for(;p;p-=p&-p)
ans+=t[p];
return ans;
}
int main(){
n=read();
for(register int i=1;i<=n;i++){
x[i].val=read();
x[i].id=i;
}
if(n>1) qs(1,n);
for(register int i=1;i<=n;i++){
add(x[i].id);
s+=i-sum(x[i].id);
}
printf("%lld",s);
}
The End
c++ 树状数组的更多相关文章
- BZOJ 1103: [POI2007]大都市meg [DFS序 树状数组]
1103: [POI2007]大都市meg Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 2221 Solved: 1179[Submit][Sta ...
- bzoj1878--离线+树状数组
这题在线做很麻烦,所以我们选择离线. 首先预处理出数组next[i]表示i这个位置的颜色下一次出现的位置. 然后对与每种颜色第一次出现的位置x,将a[x]++. 将每个询问按左端点排序,再从左往右扫, ...
- codeforces 597C C. Subsequences(dp+树状数组)
题目链接: C. Subsequences time limit per test 1 second memory limit per test 256 megabytes input standar ...
- BZOJ 2434: [Noi2011]阿狸的打字机 [AC自动机 Fail树 树状数组 DFS序]
2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec Memory Limit: 256 MBSubmit: 2545 Solved: 1419[Submit][Sta ...
- BZOJ 3529: [Sdoi2014]数表 [莫比乌斯反演 树状数组]
3529: [Sdoi2014]数表 Time Limit: 10 Sec Memory Limit: 512 MBSubmit: 1399 Solved: 694[Submit][Status] ...
- BZOJ 3289: Mato的文件管理[莫队算法 树状数组]
3289: Mato的文件管理 Time Limit: 40 Sec Memory Limit: 128 MBSubmit: 2399 Solved: 988[Submit][Status][Di ...
- 【Codeforces163E】e-Government AC自动机fail树 + DFS序 + 树状数组
E. e-Government time limit per test:1 second memory limit per test:256 megabytes input:standard inpu ...
- 【BZOJ-3881】Divljak AC自动机fail树 + 树链剖分+ 树状数组 + DFS序
3881: [Coci2015]Divljak Time Limit: 20 Sec Memory Limit: 768 MBSubmit: 508 Solved: 158[Submit][Sta ...
- 树形DP+DFS序+树状数组 HDOJ 5293 Tree chain problem(树链问题)
题目链接 题意: 有n个点的一棵树.其中树上有m条已知的链,每条链有一个权值.从中选出任意个不相交的链使得链的权值和最大. 思路: 树形DP.设dp[i]表示i的子树下的最优权值和,sum[i]表示不 ...
- bzoj2743离线+树状数组
奇葩染色,对于每一个点关心的是前前个同颜色的位置,但是处理方法相同 离线比较神奇,按照右端点排序,然后每次用的是左端点,就不用建可持久化树状数组(什么鬼)了 区间修改+单点查询 果断差分以后用树状数组 ...
随机推荐
- Java报错:Unable to find setter method for attribute: [x]
在学习JavaWeb JSTL与自定义标签时遇到的坑,用的老师给的代码结果直接原地报错:javax.servlet.ServletException: org.apache.jasper.Jasper ...
- win11拖动窗口造成崩溃的问题
问题描述 拖动窗口,随机概率出现 屏幕闪烁 屏幕黑屏 屏幕瞬间分屏 解决方法 windowes11贴吧大神给的方案 1,按下 win键+R 输入 regedit 进入注册表,进入以下路径:计算机\HK ...
- Mybatis-typeAliases的作用
其他具体代码请访问->mybatis的快速入门 1.在mybatis核心配置文件SqlMapperConfig.xml中配置别名 <!-- 定义别名--> <typeAlias ...
- 入门学习SpringCloud
今天趁着空余时间,看了一丁点狂神SpringCloud的视频.学习微服务及架构相关知识,明天再学习系列视频的剩下部分,部署第一个SpringCloud练习. 同时趁着晚上课后大家有时间组织了小型会议, ...
- jquery的常用API
1, 增 $('body').append('<h1>大标题</h1>') $('body').append('<h2>二标题</h2>') $('&l ...
- DOM的事件传播机制
在dom传播的过程中,一个事件有触发到响应,经历了三个过程: 1,目标的挖洞过程,先有html标签触发事件,然后向子标签一层一层传播,但未执行,,直到找到事件目标为止,这个过程叫做挖洞过程, 2,目标 ...
- 数仓建设 | ODS、DWD、DWM等理论实战(好文收藏)
本文目录: 一.数据流向 二.应用示例 三.何为数仓DW 四.为何要分层 五.数据分层 六.数据集市 七.问题总结 导读 数仓在建设过程中,对数据的组织管理上,不仅要根据业务进行纵向的主题域划分,还需 ...
- 在 K8s 上运行 GraphScope
本文将详细介绍:1) 如何基于 Kubernetes 集群部署 GraphScope ; 2) 背后的工作细节; 3) 如何在分布式环境中使用自己构建的 GraphScope 开发镜像. 上篇文章介绍 ...
- XCTF练习题---MISC---神奇的Modbus
XCTF练习题---MISC---神奇的Modbus flag:sctf{Easy_Modbus} 解题步骤: 1.观察题目,下载附件 2.打开下载文件,发现可以用WireShark打开,打开看看是啥 ...
- JSP标签、JSTL标签、EL表达
JSP页面转发,附带数据 <jsp:forward page="/jsptag2.jsp"> <jsp:param name="name" v ...