树状数组 算法分析
一个神奇的稀奇古怪的算法
算法优劣:
树状数组是用来维护区间的,应该是做区间问题时最常用的方法了(除了暴力)。树状数组的优点很明显:与线段树相比
码量小,空间小,容易调试
,与分块相比易理解,更快捷
。然而缺点也是很致命的:它能做的事不多,一般都只是用来查找区间和、区间极值等问题。更复杂的区间维护就用不了树状数组了QAQ。
所谓“树状数组”:
——树状数组最良心的地方就在于它是线性的!只要用一个一维数组存储就行了。
——线段树不也是吗 -_-
——啊这,线段树查询很麻烦嘛。。。
如何用一个线性的数组当作树来用呢?这是线段树、堆、树状数组这一类算法所要研究的问题。在线段树和堆中,我们所用的方法就是
左节点p<<1
,右节点p<<1|1
。而在树状数组中,有一个非常玄学奇妙的算法——\(lowbit\),为了方便解释,下面上图:
这应该很容易发现规律,但是。。。如何把规律表示成式子就是个很恶心的问题,那么免去推理过程,有:
或者说的形象一点,比如\(n=6\)时,\(6\) 所能够整除的最大的的 \(2\) 的次方数为 \(2\) ,\(2=2^1\), 因此:
- 注:求\(2^k\)的操作名为lowbit
\(\large\text{那么以上就是树状数组的存储规则}\)
算法实现:
上面已经明确
k为n在2进制下末尾1的个数
,这个\(1\)的个数我们可以用n&(-n)
去查找,证明下面给出,至于怎么想到的。。。。。。因为我们的前辈是有大智慧的人
众所周知, -n即为n取反加\(1\)。如果不加\(1\),那么很容易发现
n的反码 & n的原码 = 0000000.....0
。那么n&(-n)
的大小就决定在这个加的\(1\)上。那么因为取反以后,前面所有的\(0\)都变成了\(1\),加上 \(1\) 就会发生进位
,那么进位一直持续到遇见第一个 \(0\),即原来的第一个 \(1\) 的位置上,并且在进位的途中所遇到的所有 \(1\) 全部都变成 \(0\)。那么也就是说,除了最后停下来的那个被变成 \(1\) 的 \(0\) 的位置上是 \(2\) 个 \(1\),其他的所有位置上都有 \(0\),那么进行&
操作的时候就会全部变成0,那么最后得出的结果就是1后面跟着k个0
,那也就是\(2^k\)了
树状数组一般只能是修改单点(这就体现出与线段树的差距了)。那么既然是修改单点(假设要修改 \(n\)),我们就要找到所有存有\(C_n\)的 \(T_i\),并将所有的 \(T_i\) 更新。那么我们就要找到 \(n=i-2^k+b(1\leq b \leq 2^k)\)。一个T节点的上一级肯定是\(2^{k+1}\),因为它的上级必然会管到\(j-2^{k_1}+1\),也就是管了\(2^k\)个点,而它所直接管辖的\(T\)节点的管辖个数必然是\(2^{k_1-1}\),那么所以应该很容易看出,只要依次向上加一个\(2^k\small\text{(这个k随节点变化而变化)}\),一直加到数组上限就能找到所有管着\(C_n\)的点了。那么代码实现就很简单了:
inline void addNum(int p,int v)
//给p节点加上v
{
while(p<=n)
{
t[p]+=v;
p+=(p&(-p));
}
return;
}
因为树状数组的存储信息不多。所以只能退而求次。比如要求\([l,r]\)的和,那么我们就可以去用\([1,r]-[1,l-1]\)这样求前缀和的形式来把\([l,r]\)求出来。那么此时,我们就需要找到能够拼凑成类似\([1,n]\)的和的节点并加和。去观察一下图像,很容易发现\([1,8]\)即为\(T_8\)的值,\([1,7]\)也很好找,是\(T_7+T_6+T_4\)。进一步发现它的规律,我们发现,当我们寻找\([1,n]\)时,首先\(T_n\)必然是会被包括的(如果加了\(T_i(i>n)\),必然会有多余的值被包含,而如果只加了\(T_i(i<n)\),则\(n\)也不可能被包括)。然后我们就可以堂而皇之的将\(T_n\)加进去了。当我们把\(T_n\)加进去以后,那么\(T_n\)所包含的最小的\(C_i\)的\(i\)值就是\(\small{i-2^k+1}\)(上面讲的规律),那么现在的问题就转换成了求\([1,n-2^k]\)了.所以我们要求的就肯定含有\(T_{n-2^k}\)了,通过这样,我们就可以进行操作直到\(\small{n-2^k=0}\)。
inline int getAns(int p)
//寻找 [1,p] 的和
{
int ans=0;
while(p)
{
ans+=t[p];
p-=(p&(-p));
}
return ans;
}
代码汇总
-
- 方便您调试的代码(显示一个树状数组结构)
#include<bits/stdc++.h>
#define lb(a) (a&(-a))
using namespace std;
int main()
{
cout<<"输入树状数组范围:";
int n;cin>>n;
for(int i=1;i<=n;++i)
{
cout<<"对于节点 "<<i<<" 有:"<<endl<<"T"<<i<<" = ";
for(int j=i-(lb(i))+1;j<i;++j)
{
cout<<"C"<<j<<" + ";
}
cout<<"C"<<i<<endl;
cout<<"(2^k)lowbit("<<i<<") = "<<lb(i)<<endl<<endl<<endl;
}
return 0;
}
-
- 树状数组代码(模板第1题)
#include<bits/stdc++.h>
#define lb(w) (w&(-w))
using namespace std;
int t[500001],n;
inline long long int read()
{
long long int res=0;bool flag=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')flag=0;ch=getchar();}
while(ch>='0'&&ch<='9'){res=(res<<1)+(res<<3)+(ch^48);ch=getchar();}
if(flag)return res;return -res;
}
inline void addNum(int p,int v)
{
while(p<=n)t[p]+=v,p+=lb(p);
return;
}
inline int getAns(int p)
{
int ans=0;
while(p)ans+=t[p],p-=lb(p);
return ans;
}
int main()
{
n=read();int m=read();
for(int i=1;i<=n;++i)
{
int r=read();
addNum(i,r);
}
for(int i=1;i<=m;++i)
{
int o=read(),x=read(),y=read();
if(o==1)addNum(x,y);
else
{
int ans=getAns(y)-getAns(x-1);
printf("%d\n",ans);
}
}
return 0;
}
配套算法 线段树 ------>