今天我们来讨论一种我自认为十分实用的数据结构——树状数组。在我看来树状数组十分易懂,一定程度上与线段树相比有极大的优势。接下来我们来深究树状数组。
前提
你不需要会线段树,但你需要知道线段树的存贮结构(因为我将通过比较线段树来讲解)。
正文
我们先来见识一下线段树的储存方式:
应该很好懂,接下来就是主角登场了:
当当当当……是不是觉得很优美?什么?你居然否认!哼哼,等我讲完你就会觉得优美了。
下面是树状数组的存储方法(敲黑板):以上方的树为例,我们令八个子节点的权值各为a[1],a[2],a[3]....a[8];然后定义c[i]代表子树的叶子结点的权值之和:
有图有真相,相信大家都已经看懂了。
接下来就是树状数组的构建,请大家先拿出草稿纸。我们可以发现,每一个c[i]都在一个节点的左儿子上,所以对照式子可以发现c[i]=a[i-2^k+1]+a[i-2^k+2]+……+a[i](k为i的二进制中从最低位到高位连续零的长度),所以我们需要求出k,至于怎么求,这就是为什么要你们拿草稿纸的原因,大家可以自行推算,这里我先把答案亮出来吧:我们引入lowbit(x)求k,具体操作就是t&(-t),为了方便,不建议大家写一个函数,可以直接定义:#define lowbit(t) t&(-t) (如果你是P党,那么恕我救不了你)。现在树状数组的核心已经讲述完成了,接下来的就十分简单了,大家可以打开网易云音乐,搜索用户落九思,进入他的歌单,一边听歌一边学树状数组。对于树状数组的构建,我们可以把c全部看成权值为0,那么,构建操作就成了单点修改,add(i,a[i])即可,而单点修改,当c[i]被更改时,我们发现c[i]的祖先上的c也需要更改,然而有一个好消息:下一个需要修改的祖先的c数组的编号为i+lowbit[i](大家可以拿出草稿纸证一下),于是,我们就顺理成章的能写出add了:
void add(int x,int y){for(int i=x;i<=n;i+=lowbit(i))c[i]+=y;}
至于剩下的区间查询,不过是add的反向操作,我此处就不细讲了,留给大家思考。但此处还是给出代码:
int getsum(int x){int ans=0;for(int i=x;i>0;i-=lowbit(i))ans+=c[i];return ans;}于是乎,树状数组就讲完了,它的时间复杂度为nlogn,它在一些情况下可以代替线段树,取得更好的效果,但它不支持区间修改,所以慎用,慎用。