carnation13的树状数组学习笔记
树状数组算是挺早的一个坑吧,最近终于想起来把它给填补掉了。
概念
树状数组是一种比较厉害且常用的数据结构,用于区间的查询修改等操作,实现起来比线段树要简单,码量要短。
常用操作
1、单点修改+区间查询
最简单的树状数组就是这样的:
void add(int p, int x){ //给位置p增加x
while(p <= n) sum[p] += x, p += p & -p;
}
int ask(int p){ //求位置p的前缀和
int res = 0;
while(p) res += sum[p], p -= p & -p;
return res;
}
int range_ask(int l, int r){ //区间求和
return ask(r) - ask(l - 1);
}
2、区间修改+单点查询
通过“差分”(就是记录数组中每个元素与前一个元素的差),可以把这个问题转化为问题1。
void add(int p, int x){ //这个函数用来在树状数组中直接修改
while(p <= n) sum[p] += x, p += p & -p;
}
void range_add(int l, int r, int x){ //给区间[l, r]加上x
add(l, x), add(r + 1, -x);
}
int ask(int p){ //单点查询
int res = 0;
while(p) res += sum[p], p -= p & -p;
return res;
}
3、区间修改+区间查询
这是最常用的部分,也是用线段树写着最麻烦的部分——但是现在我们有了树状数组!
怎么求呢?我们基于问题2的“差分”思路,考虑一下如何在问题2构建的树状数组中求前缀和:
位置p的前缀和 =
在等式最右侧的式子\(\sum\limits_{i=1}^p\sum\limits_{j=1}^id[j]\)中,\(d[1]\)被用了\(p\)次,\(d[2]\)被用了\(p-1\)次,以此类推...我们可以写出
位置p的前缀和 =
那么我们可以维护两个数组的前缀和:
一个数组是\(sum1[i]=d[i]\)
另一个数组是 \(sum2[i]=d[i]*i\)
void add(ll p, ll x){
for(int i = p; i <= n; i += i & -i)
sum1[i] += x, sum2[i] += x * p;
}
void range_add(ll l, ll r, ll x){
add(l, x), add(r + 1, -x);
}
ll ask(ll p){
ll res = 0;
for(int i = p; i; i -= i & -i)
res += (p + 1) * sum1[i] - sum2[i];
return res;
}
ll range_ask(ll l, ll r){
return ask(r) - ask(l - 1);
}
用这个做区间修改区间求和的题,无论是时间上还是空间上都比带lazy标记的线段树要优。
例题
1.洛谷P3374 【模板】树状数组 1
思路:树状数组板子题,单点修改+区间查询即可
2.洛谷P3368 【模板】树状数组 2
思路:树状数组板子题,区间修改+单点查询即可
3.洛谷P6225 [eJOI2019]异或橙子
思路:单点修改+区间查询,将树状数组中的维护区间和改为维护区间xor即可。
要两个树状数组,分别维护奇数位置和偶数位置异或和。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i,a,b) for(ll i=a;i<=b;++i)
#define per(i,a,b) for(ll i=a;i>=b;--i)
ll n,q,a[200005],b,x,y,u[200005],v[200005];
void add1(ll p,ll x)
{
for(ll i=p;i<=n;i+=i&-i)u[i]^=x;
//u[p]=x;
}
void add2(ll p,ll x)
{
for(ll i=p;i<=n;i+=i&-i)v[i]^=x;
//v[p]=x;
}
ll xor1(ll p)
{
ll res=0;
for(ll i=p;i;i-=i&-i)res^=u[i];
return res;
}
ll xor2(ll p)
{
ll res=0;
for(ll i=p;i;i-=i&-i)res^=v[i];
return res;
}
ll range_xor1(ll l,ll r)
{
return xor1(r)^xor1(l-1);
}
ll range_xor2(ll l,ll r)
{
return xor2(r)^xor2(l-1);
}
signed main()
{
ios::sync_with_stdio(false);
cin>>n>>q;
for(ll i=1;i<=n;++i)cin>>a[i];
for(ll i=1;i<=n;++i)
{
if(i&1)add1(i/2+1,a[i]);
else add2(i/2,a[i]);
}
while(q--)
{
cin>>b>>x>>y;
if(b==1)
{
if(x&1)add1(x/2+1,y^a[x]),a[x]=y;
else add2(x/2,y^a[x]),a[x]=y;
}
else
{
if((y-x)&1)cout<<0<<endl;
else
{
if(x&1)cout<<range_xor1(x/2+1,y/2+1)<<endl;
else cout<<range_xor2(x/2,y/2)<<endl;
}
}
}
return 0;
}
4.洛谷P1908 逆序对
思路:以下只介绍树状数组做法(因为要练树状数组qwq)
先对数组进行离散化,然后用树状数组维护离散化的序列,单点修改+区间查询即可
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i,a,b) for(ll i=a;i<=b;++i)
#define per(i,a,b) for(ll i=a;i>=b;--i)
ll n,a[500005],b[500005],sum[500005];
void add(ll p,ll x)
{
for(ll i=p;i<=n;i+=i&-i)sum[i]+=x;
}
ll ask(ll p)
{
ll res=0;
for(ll i=p;i;i-=i&-i)res+=sum[i];
return res;
}
ll range_ask(ll l,ll r)
{
return ask(r)-ask(l-1);
}
signed main()
{
ios::sync_with_stdio(false);
cin>>n;
for(ll i=1;i<=n;++i)cin>>a[i],b[i]=a[i];
sort(b+1,b+1+n);
for(ll i=1;i<=n;++i)a[i]=lower_bound(b+1,b+1+n,a[i])-b;
ll ans=0;
for(ll i=1;i<=n;++i)
{
ans+=range_ask(a[i]+1,n);
add(a[i],1);
}
cout<<ans<<endl;
return 0;
}
5.洛谷P5094 [USACO04OPEN] MooFest G 加强版
思路:先对v排序,然后用两个树状数组,一个维护数量,另一个维护坐标和,对小于当前位置的坐标和大于当前位置的坐标分别求贡献
6.洛谷P2161 [SHOI2009]会场预约
思路:二分找到最后可能交叉的区间,树状数组维护单点值,固定左端点,二分右端点。
7.洛谷P3605 [USACO17JAN]Promotion Counting P
思路:树状数组+离散化+dfs
代码(关键部分dfs)
{
ans[u]-=ask(100000)-ask(p[u]);
for(ll i=head[u];i;i=e[i].next)
{
ll v=e[i].v;
dfs(v);
}
ans[u]+=ask(100000)-ask(p[u]);
add2(p[u],1);
}
8.洛谷P1972 [SDOI2009] HH的项链
思路:思维题,将右端点排序后,再维护一个树状数组,重复出现则将前一个删除,离线求出ans即可
9.洛谷P3531 [POI2012]LIT-Letters
思路:设第一个串为a,第二个串为b
我们可以找出a串中第x次出现的字符c1与其在b中第x次出现的相同的字符c2的位置,计入p数组
此时只需求出p数组的逆序对即可
为什么?
因为我们要通过交换使得a串对应的p数组与b串的p数组相同
但a串与a串自身处理出来的p数组就是{1,2,3,4,...,n}
所以交换的次数等同于求逆序对
10.洛谷P1966 [NOIP2013 提高组] 火柴排队
思路:将a、b数组离散化后,剩下做法步骤同上题,别忘了统计ans时取模。

浙公网安备 33010602011771号