树状数组
作用:
动态地维护前缀和查询
Time complexity
修改一个数:$$o(lgn)$$
查询一段区间和:$$o(lgn)$$
实现过程
1 lowbit
返回一个数的二进制下末尾第一个1和后面的0构成的数
如11011000100 返回100=4
int lowbit(int x)
{
return x&(-x);
}
2 建立树状数组
定义$$t[x]$$保存以下标$$x$$的子树中叶节点的值
有如下几点性质(我不会证明)
1.$$t[x]$$覆盖的长度为$$lowbit(x)$$
2.$$x$$的父节点下标为$$x+lowbit(x)$$
3.$$x$$的左上角的结点下标为$$x-lowbit(x)$$(求前缀和)
4.建成的数的深度为$$log(n)+1$$,因此修改和查询的时间复杂度最差为$$o(logn)$$
那么我们根据这些性质
首先是把数组$$ind$$位置的数加上$$k$$
void add(int x,int k)
{
for(;x<=n;x+=lowbit(x))
{
t[x]+=k;
}
}
查询下标为$$ind$$的前缀和
long long ask(int x)
{
long long ans=0;
for(;x;x-=lowbit(x))
{
ans+=t[x];
}
return ans;
}
例题:E - Mod Sigma Problem
题目要求
首先要注意内层的求和要模$$M$$,但外层不要
先定义一个$$s[i]=(a[1]+...+a[i])mod M$$
所以在模$$M$$的意义下的区间和可以表示为
所以原式可以转化为
随后我们可以再把$$r$$固定
式子就变成了
第二个求和可以直接利用前缀和来处理
此处的关键在于处理这个$$x_r$$,含义为对于每一个$$r$$,第二个求和累加的过程中$$s[r]<s[l-1]$$的个数
为此我们可以用一个树状数组来维护
把$$s[r]$$的值映射为树状数组的下标,$$t[x]$$表示$$s[r]=x$$的个数
当我们遍历一个新的$$r$$时,此时$$t$$数组里储存的说上一此遍历的结果
要求得此时的$$x_r$$只需要求出$$[s[r]+1,m]$$的区间和,即为大于$$s[r]$$的个数---$$x_r$$
注意当$$s[r]$$为$$0$$时不要去加
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,m;
long long ans;
long long a[N],t[N],s[N],sl[N];
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int k)
{
for(;x<=m;x+=lowbit(x))
{
t[x]+=k;
}
}
long long ask(int x)
{
long long ans=0;
for(;x;x-=lowbit(x))
{
ans+=t[x];
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
long long sum=0;
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
a[i]%=m;
s[i]=(s[i-1]+a[i])%m;
sl[i]=sl[i-1]+s[i];
}
for(int r=1;r<=n;r++)
{
ans+=s[r]*r-sl[r-1]+(ask(m)-ask(s[r]))*m;
if(s[r])
add(s[r],1);
}
cout<<ans<<endl;
}