树状数组适用范围:单点修改,区间查询、区间修改,单点查询、区间修改,区间查询。
对不变的数组进行区间查询,前缀和无疑是不错的选择;然而对于需要动态修改的数组,又有该效率查询需求的,可以使用树状数组优化(对数阶的查询与修改)。
·单点修改,区间查询:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=5e5+10;
int tree[N],n;//树状数组、原始数组长度
int lowbit(int x)//计算tree[x]管辖长度
{
return x&-x;
}
void add(int x,int k)//给原数组第x位上加k
{
while(x<=n)
{
tree[x]+=k;
x+=lowbit(x);
}
}
int sum(int x)//求原数组1~x元素的和,相当前缀和
{
int ans=0;
while(x>0)
{
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int op,x,y,k;
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>y;
add(i,y);
}
while(k--)//单点修改,区间查询
{
cin>>op>>x>>y;
if(op==1){
add(x,y);
}else{
cout<<sum(y)-sum(x-1)<<endl;
}
}
}
·区间修改,区间查询:
区间的高效的率修改,必然要用到差分;高效查询区间,必然要用到前缀和。
为了高效区间修改与查询,须先了解前缀和与查分的关系:
求前缀要用到d[i]、d[i]*i的前缀和,二者均可以用树状数组进行维护。这样区间查询就不成问题了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
//区间修改,区间查询
//记s[i]为a[i]的前缀和
//用两个数组维护a[n]的差分数组d[i],d[i]*i,其前缀和分别记作sum1[i],sum2[i],则s[i]=(i+1)*sum1(i)-sum2(i)
//sum1,sum2用一个函数sum实现即可
const int N=1e6+10;
int n,a[N],d[N],tr1[N],tr2[N],m;
void add(int x,int k)
{
int val=x*k;
while(x<=n)
{
tr1[x]+=k;
tr2[x]+=val;//不可写成x*k,在迭代的过程中,x的值会发生变化!
x+=x&-x;
}
}
int sum(int *t,int x)
{
int ans=0;
while(x>0){
ans+=t[x];
x-=x&-x;
}
return ans;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
d[i]=a[i]-a[i-1];
add(i,d[i]);
}
int op;
while(m--)
{
cin>>op; int L,R,v;
if(op==1)//区间修改
{
cin>>L>>R>>v;
add(L,v); add(R+1,-v);
}
else//区间查询
{
cin>>L>>R;
cout<<sum(tr1,R)*(R+1)-sum(tr2,R)-(sum(tr1,L-1)*L-sum(tr2,L-1))<<endl;//sum[R]-sum[L-1];
}
}
}
对于区间修改,单点查询,在此基础上直接求查分数组的前缀和即可。
·应用:
·求逆序数:
牛客·逆序数
将数组里的元素逐一插入哈希表,哈希表中h[i]位置上的前缀和,即小于i的数量,h[n]-h[a[i]即大于a[i]的元素数量。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10;
int tree[N],n;
void add(int x)
{
while(x<=n)
{
tree[x]++;
x+=x&-x;
}
}
int sum(int x)
{
int ans=0;
while(x)
{
ans+=tree[x];
x-=x&-x;
}
return ans;
}
signed main()
{
cin>>n;
int ans=0;
for(int i=1;i<=n;i++)
{
int x; cin>>x;
ans+=sum(n)-sum(x);
add(x);
}
cout<<ans;
return 0;
}
此处,树状数组tree维护的是哈希表,优化了哈希表前缀和的查询时间,tree进行的是单点修改,区间查询。复杂度为O(nlogn)。
洛谷·Above the Median G
把>=m的元素替换成1,<m的替换成-1进行等效处理,求前缀和s,任选i<j,s[j]-s[i]>=0可以判定在区间[i+1,j]上的子串是满足条件的,在j前找小于等于s[j]的即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int p=1e9+7;
const int mod=998244353;
const int N=2e5+10;
int n,m;
int t[N],a[N];
void add(int x)
{
while(x<=n*2+1)
{
t[x]++;
x+=x&-x;
}
}
int sum(int x)
{
int ans=0;
while(x>0)
{
ans+=t[x];
x-=x&-x;
}
return ans;
}
void solve()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
if(x>=m) a[i]=a[i-1]+1;
else a[i]=a[i-1]-1;
}
int ans=0;
add(0+n);
for(int i=1;i<=n;i++)
{
ans+=sum(a[i]+n);
add(a[i]+n);
}
cout<<ans;
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t;
t=1;
// cin>>t;
while(t--)
{
solve();
}
return 0;
}
同逆序数题相似,维护哈希表,注意这里的前缀和值可能为负,需要树状数组内存需开大点,将所有值+n就解决了。
离散化+二分查找+树状数组
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N],b[N],t[N],n;
void add(int x)
{
while(x<=n)
{
t[x]++;
x+=x&-x;
}
}
int sum(int x)
{
int ans=0;
while(x)
{
ans+=t[x];
x-=x&-x;
}
return ans;
}
int find(int x)//二分答案
{
int l=1,h=n,mid;
while(l<h)
{
mid=l+h>>1;//第二种写法,ans满足sum(ans)>=x,ans+1有可能也满足sum(ans+1)>=x,因为数据是离散化的,而ans-1一定不满足sum(ans-1)>=x
//故用第二种写法。
if(sum(mid)<x) l=mid+1;
else h=mid;
}
return b[l];
}
pair<int,int>p[N];
int main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>p[i].first; p[i].second=i;
}
//离散化
sort(p+1,p+1+n);
for(int i=1;i<=n;i++)
{
a[p[i].second]=i;
b[i]=p[i].first;
}
cout<<b[a[1]]<<endl;
add(a[1]);
for(int i=2;i<=n;i++)
{
add(a[i]);
if(i&1){
cout<<find(i+1>>1)<<endl;
}
}
}
24/8/10