读者要求:知道什么是树状数组,不知道可以先看一下百度百科,然后再来阅读本文,传送门:
https://baike.baidu.com/item/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/313739?fr=aladdin
首先谈一下个人对于一维树状数组的理解:
首先树状数组是为了解决频繁的读取前k项的数组和而且数组内容动态改变的问题而提出的(个人认为),这里强调一下数组内容动态改变。假如数组的内容是静态的,那么只需要一个数组D,第K个元素的内容就是数组A前K个元素的和就可以了。但是考虑到数组内容的动态改变,假设改变数组A第K个元素,那么对应的上面的D数组K后面的元素便都要更新,比较费时。可以考虑改变D数组的内容编码,这样就可以减少当A数组元素更新时需要更新的D数组的元素个数。比如按照上面的树状数组编码方式,C数组,当改变A6时,只需要更新C6和C8两个元素,而D数组要更新D6,D7,D8,D9四个元素。虽然求和时需要更多的C数组的元素表示,比如求前6项的和,按照D数组,只需要D6就可以,但是按照C树状数组,需要C6+C4。但是对于频繁改变内容的数组还是树状数组比较好
下面讲一下对于s[x]=a[1]+a[2]+a[3]+...+a[k];为什么可以使用如下代码计算:
s[x]=0;
for(int =x;i>0;i-=lowbit(i))s[x]+=c[i];
上面c[k]=a[k-lowbit(k)+1]+...+a[k],lowbit(k)=k&(-k)就是整数k的二进制表示中右边第一个1所代表的数字(不知道请百度)
首先从目的出发,我们是要求前x项的和,为了方便理解,我就求前6项的和:s[6]。我们可以把6写成二进制来观察,即110,
110有什么特殊的含义呢?对!他可以分成1*2^4+1*2^1(哈哈),现在求和就变成了两个部分,之所以是两个部分是因为二进制里面有2个1(可以这样理解):(A1+A2+A3+A4)+(A5+A6),我们称前一部为X,后一部分为Y,现在的问题是如何理解这两个部分,这两个部分和C数组有什么关系。对于X部分或者Y部分,我们描述他们需要2个参数,一个是最右边的元素的下标,就是X部分里面的A4和Y部分里面的A6,然后就是每一个部分的长度,就是X的4个长度和Y的2个长度,机智的读者已经发现其实Y部分的和就是C[6],X部分的和即使C[4],换句话说假如我们求和s[x],他可以分成若干C的和,具体的C的元素确定可以通过对x写成二进制,然后观察里面的1,1的个数代表了和被分成几部分,也就是C的个数,然后我们可以从最后一个元素即Ax开始推,就举上面的例子,首先从A6开始,第一部分和是C6,他的长度是0010,就是2,这个就是6二进制右边第一位1的数值,可以通过lowbit计算,之所以需要知道C6跨越的长度是为了知道下一个C是多少,6-2=4,所以下一个开始的起点是A4,和是C4,这一个步进就是i-=lowbit(i)了,现在上面的代码应该很清楚了吧(我认为是)。
下面讲一下具体的一个元素,他的父节点下标的计算或者遍历吧,下面是代码:
for(int i=k;i<n;i+=lowbit(i))
首先通过上面的介绍,对于C[k]可以理解为一个区间,就是(A[k-lowbit(k)+1,A[k]],比如C[6]=(A[5],A[6]](注意左端点取不到)现在的问题就是包含Ax的区间有哪些,也即是C有哪些,或者讲父亲元素有哪些。考虑x的二进制举个例子进行讲解吧,求A6的父元素的下标,6的二进制0110,显然比6小的C是不可能包含A6的,那么现在观察大于6的二进制数,我们这样考虑,分成2部分,一部分是6第一个1的右边部分,即0110和左边0110。我们先保证左边部分不变,对右边部分的部分0变成1,显然只要右边部分有一个1,对应的那个C的下界是0110,而且取不到,故在右边部分是不可以有1的,就是说0111是不行的,那么下一个数字就是1000了,显然C8是OK的,从图中可以看出,是第一个父元素,上面的结论规范下就是对于一个x,写成二进制,比如1000010000,对于第一个右边的1右边不能出现1,就是1000010001到1000011111都是不行的,至少加10000,得到1000100000,这个C是可以的,下面推下一个C,对于1000100001到1000111111也是不包含1000010000的,至少要加100000,才可以,就这样递推下去就可以求完所有包含Ax的C了,这就是为什么每次步进i+=lowbit(i)的原因了。
现在讲一下POJ 2155的题目了,楼教主出的题目,传送门:
http://poj.org/problem?id=2155
代码如下:
int N;
int tree[1005][1005]//01矩阵
void Getsum(int x,int y)//对子矩阵(1,1)-(x,y)进行置反
{
for(int i=x;i>0;i-=(i&-i))
{
for(int j=y;j>0;j-=(j&-j))
{
tree[i][j]^=1;
}
}
}
int Update(int x,int y)//计算和返回(x,y)的值
{
int s=0;
for(int i=x;i<=N;i+=(i&-i))
{
for(int j=y;j<=N;j+=(j&0j))
{
s+=tree[i][j];
}
}
if(s%2==0)return 0;
else return 1;
}
这里我只写了关键的两个函数,分别是进行置换的函数Getsum()和求(x,y)值的函数Update(),由于这种解法是使用二维树状矩阵的,我就解释一下上面两个函数,这里的tree矩阵不是原来的01矩阵,也就是说不是题目上的01矩阵,而是题目上的树状矩阵,比如上面文中的原来数组A,C数组是A上的树状数组一样,里面的元素是对应A数组里面的元素的和,这里的tree矩阵就是二维树状矩阵,他同样管理着一块区域,这里管理可以这样理解。比如一维树状数组里面的C6是A6和A5的和,可以理解为C6管理A6到A5这一块区域,同样tree[x][y]也是管理以前区域,其中x为[x-lowbit(x)+1,x],y为[y-lowbit(y)+1,y],下面举个例子,考虑3*3的矩阵
sum[3][3]=tree[3][3]+tree[3][2]+tree[2][3]+tree[2][2],注意这里其实不是加,加号只是表示一下tree[3][3]的区域由右边的4块区域组成,蓝色是tree[3][3],黄色是tree[3][2],红色是tree[2][3],紫色是tree[2][2],所以反置3*3内的矩阵就是分别反置上面四块区域的矩阵,即更新tree[3][3],tree[2][3],tree[3][2],和tree[2][2]即可,这就是上面Getsum函数的意思。现在求(x,y)出的值,只需要考虑包含(x,y),的区域反置次数的总和是奇还是偶就可以了,这句话可以这样理解,首先(x,y)有可能位于多个tree包含的面积下,而反置操作只记录在tree里面,1表示反置了奇数次,0相当于没有反置,假设包含(x,y)的tree有n1,n2,n3,n1是1,n2是1,n3是0,就是说一共对(x,y)反了2次所以(x,y)还是0,奇数就是1。这个就是Update()函数的工作,他就是求(x,y)的父节点,原理其实和上面的一维树状数组一样,而Getsum反置和一维的求前k个元素的和是一样的,当然这里的tree里面的值不再是和,而是反置的次数,奇数次是1,偶数次是0。讲完了!!!!