树状数组:
当我们要对一个数组进行求前缀和和修改一个数时;
1.利用数组来存,利用前缀和来求(O(n)),修改一个数(O(1));
2.利用前缀和数组来维护, 求出前缀和(O(1)),修改一个数(O(n));
所以想到是否可以利用一个方法使得两种操作的时间复杂度都降低;
树状数组可以实现(O(logn));
树状数组的基本操作:
1.修改(在第x个数上加上c)
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)){
tr[i]+=c;
}
}
2.查询(查询1~n的和)
int sum(int x)
{
int res=0;
for(int i=x;i>0;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
一个数x看成2进制表示是 x=2^i(k)+2^i(k-1)+2^i(k-2)+......+2^i(1); (不超过logx);
所以可以看成区间
(x-2^i(1), x] 长度: 2^i(1);
(x-2^i(1)-2^i(2), x-2^i(1)] 长度: 2^i(2);
(x-2^i(1)-2^i(2)-2^i(3), x-2^i(1)-2^i(2)]; 长度: 2^i(3);
.
.
(0,x-2^i(1)-2^i(2)-2^i(3)---2^i(k-1)] 长度: 2^i(k);
发现长度刚好是x的二进制表示的最后一个1对应的2的次幂;
2的i(2)次方刚好是x-2的i(1)次方的最后一个1;
(L,R]的长度一定是R的二进制表示最后一个1所对应的次幂; len=R-lowbit(R);
(L,R]--->(R-lowbit(R)+1,R];
我们用C[R]来表示(L,R]这段区间的总和; 通过枚举右端点来看总和;
接下来找到C[]数组之间的关系;
让我们来看y总的图; 来自Acwing;
观察图我们来利用最少的块来实现;
C[16]=C[8]+C[12]+C[4]+C[15]+a[16];
C[8]=C[4]+C[6]+C[7]+a[8];
C[12]=C[10]+C[11]+a[12];
每次先加入a[x]; for(int i=1;i<=n;i++) add(i,a[i]);
x>0 说明必须有一个1,找到最后一个1; x=....10000;
因为加入了a[i],所以原来区间长度减1; C[x]=(x-lowbit(x)+1,x-1];
x-1=.....01111;
所以从子节点出发每次找到父节点累加即可;
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
学习完基本知识来做题目练习:
795. 前缀和 - AcWing题库(让读者先熟悉树状数组的基本用法);
AC代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int tr[N];
int n,m;
int a[N];
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)){
tr[i]+=c;
}
}
int sum(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
signed main()
{
cin.tie(0); cout.tie(0);
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
add(i,a[i]);// 树状数组初始化把a[x]先加入;
}
while(m--)
{
int l,r;
cin>>l>>r;
int ans=sum(r)-sum(l-1);// 利用前缀和思想;
cout<<ans<<"\n";
}
return 0;
}
242. 一个简单的整数问题 - AcWing题库(利用差分数组来实现树状数组);
AC代码:
// 看到题目中是将[l,r]这一段区间内加上一个数,想到差分;
// 将对一段数组的操作变成对两个数的修改操作;
// 在差分数组中找到一个数可以利用前缀和;
// 这样可以利用树状数组来实现;
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int a[N],b[N];
int tr[N];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)){
tr[i]+=c;
}
}
int sum(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
signed main()
{
cin.tie(0); cout.tie(0);
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i]-a[i-1];
add(i,b[i]);
}
while(m--)
{
char op;
cin>>op;
if(op=='Q')
{
int x; cin>>x;
int ans=sum(x);
cout<<ans<<"\n";
}
else
{
int l,r,c; cin>>l>>r>>c;
//b[l]+=c; b[r+1]-=c; // 想到如果用差分应该怎么做;
add(l,c);
add(r+1,-c);
}
}
return 0;
}
243. 一个简单的整数问题2 - AcWing题库(区间修改和区间查询);
AC代码:
// 分析知这题要求区间修改和区间查询;
// 利用差分数组来实现第一个操作; b数组作为差分数组; a数组是原数组;
// 考虑第二个操作:求出l~r这个区间内的和;
/* a1=b1;
a2=b1+b2;
a3=b1+b2+b3;
.
.
an=b1+b2+b3+...+bn;
令s[n]=a1+a2+a3+..+an; 所以l~r这个区间内的和为s[r]-s[l-1];
即我们求出s[n];
考虑将矩阵补全; n*(b1+b2+b3+...+bn)-(1*b2+2*b3+3*b4+...+(n-1)*bn);
=(n+1)*(b1+b2+..+bn)-(1*b1+2*b2+3*b3+..+n*bn);
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,m;
int tr1[N],tr2[N];
int a[N],b[N];
int lowbit(int x)
{
return x&(-x);
}
void add1(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)){
tr1[i]+=c;
}
}
int sum1(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr1[i];
}
return res;
}
void add2(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)){
tr2[i]+=c;
}
}
int sum2(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr2[i];
}
return res;
}
int pre(int x)
{
int ans=(x+1)*(sum1(x))-sum2(x);
return ans;
}
signed main()
{
cin.tie(0); cout.tie(0);
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i]-a[i-1];
add1(i,b[i]);
add2(i,i*b[i]);// 推导公式中的后部分提前建立树状数组;
}
while(m--)
{
char op;
cin>>op;
if(op=='Q')
{
int l,r;
cin>>l>>r;
int ans=pre(r)-pre(l-1);
cout<<ans<<"\n";
}
else
{
int l,r,c;
cin>>l>>r>>c;
add1(l,c);
add1(r+1,-c);
add2(l,l*c);
add2(r+1,(r+1)*(-c));
}
}
}
树状数组的一大作用是求出1~x的和,而利用前缀和思想可以统计区间[l,r]的和;
所以可以利用树状数组来实现统计前面有多少比自己大或者小的元素个数:
例题:活动 - AcWing
AcWing 241. 楼兰图腾:
AC代码:
//将所有情况分成n类: 以1为最低,以2为最低,以3为最低......以n为最低; 最后全部加起来;
//对于第k类而言,最低点的纵坐标是y; 找到左边比y大的以及右边比y大的,利用乘法原理进行相乘;
//求出V 的个数: 就是左边(y,mmax]==[y+1,mmax]中点的个数; 右边(y,mmax]==[y+1,mmax]中点的个数;
//求出反v的个数: 就是左边[1,y-1]中点的个数; 右边[1,y-1]中点的个数;
//ps:不能直接建立add(a[i],1); 因为该题是求出左右分别大于和小于;直接建立后会把右边的也算上;
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int a[N];
int mmax;// 记录最大的是哪个;
int tr[N];
int n;
int res1=0; int res2=0;
int Greater1[N],Greater2[N];
int low1[N],low2[N];
int lowbit(int x){ return x&(-x); }
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i)){
tr[i]+=c;
}
}
int sum(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
signed main()
{
cin.tie(0); cout.tie(0);
ios::sync_with_stdio;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
mmax=max(mmax,a[i]);
//add(a[i],1);
}
// 从左边开始扫描求出左边比i大的和左边比i小的;
for(int i=1;i<=n;i++)
{
int y=a[i];
add(y,1);// 边扫描边累计;
Greater1[i]=sum(mmax)-sum(y);
low1[i]=sum(y-1);
}
memset(tr,0,sizeof tr);
// 从右边开始扫描求出右边比i大的和右边比i小的;
for(int i=n;i;i--)
{
int y=a[i];
add(y,1);// 边扫描边累计;
Greater2[i]=sum(mmax)-sum(y);
low2[i]=sum(y-1);
}
for(int i=1;i<=n;i++) res1+=Greater1[i]*Greater2[i];
for(int i=1;i<=n;i++) res2+=low1[i]*low2[i];
cout<<res1<<" "<<res2;
return 0;
}