据说树状数组就是线段树。线段树参考链接
有一天,小明给了我三个问题(其实是我自己出的啦~)
(1)有一个机器,支持两种操作,在区间[1,10000]上进行。
操作A:把位置x的值+k
操作B:询问区间[l,r]所有数字之和
区间的初始值全部为0
现在你要充当这个机器,操作A和操作B会被穿插着安排给你,
要求对于所有操作B,给出正确的答案。
怎样做才能最节省精力?
(2)有一个机器,支持两种操作,在区间[1,10000]上进行。
操作A:把区间[l,r]的值全都+x
操作B:询问x位置的值。
区间的初始值全部为0
现在你要充当这个机器,操作A和操作B会被穿插着安排给你,
要求对于所有操作B,给出正确的答案。
怎样做才能最节省精力?
(3)有一个机器,支持两种操作,在区间[1,10000]上进行。
操作A:把区间[l,r]的值全都+x
操作B:询问区间[l,r]所有数字之和
区间的初始值全部为0
现在你要充当这个机器,操作A和操作B会被穿插着安排给你,
要求对于所有操作B,给出正确的答案。
怎样做才能最节省精力?
三个问题中操作的数量都可以认为是10000这个数量级
你可以动用的工具有:无限墨水的笔,一张足够大的纸,你的大脑(没多大内存的~)。
注意:
1.举个例子,进行这种类似的操作:
从一行任意打乱的数字中找一个数字
不能认为一瞬间就可以找到,在这里所花费的精力和数字的总数具有线性关系。
2.我们认为将数据转换为二进制不需要任何时间。
对于问题1,如果我们每种操作都暴力进行,
那么显然总的时间复杂度为O(mA+n*mB),n表示区间长度,
mA表示操作A执行的次数,mB表示操作B执行的次数。
那么有没有一种更加轻松的办法呢?
我们将引入一种数据结构,叫做<树状数组>。
在介绍树状数组之前,需要先介绍一下二进制的按位运算,这里只需要用到两种:
按位与运算,符号&: 两个数字都为1时,得1,否则得0.
那么1&1=1,0&1=0,1&0=0,0&0=0
3&11的值是多少呢?我们把它化成二进制
两个数字分别为0011和1011,然后对应的,每位之间进行与运算
0011
& 1011
——————
0011
所以答案是0011,即十进制的3。
接下来再介绍一下按位非运算,符号~,运算方法是0变1,1变0
比如~9的值,就是~1001(二进制),得到0110。
那么按位运算就说完了。
最后为了方便理解后面的内容,还得介绍一个计算机的特点。
在计算机中,我们操作的变量通常都有一个固定的位数,
比如c++中 int32_t 类型的变量,它用32位的二进制数来存储一个整数。
在这个范围下,
~1=~00000000000000000000000000000001=11111111111111111111111111111110
另外,n位的二进制数进行运算,一旦向第n+1位有进位,会直接舍去这个进位,
如四位二进制数1111+0001,答案是0000而不是10000。
有了这么多铺垫,要开始正题啦~
现在就要介绍一个非常神奇的函数,它叫做lowbit。
lowbit(x)=x&((~x)+1) (为了少引入补码的概念,我们这里稍微麻烦了一下,其实x&-x就行)
它的作用是什么呢?
它只保留"从低位向高位数,第一个数字1"作为运算结果
比如二进制数00011100,结果就是00000100,也就是4。
11111001,结果就是00000001,也就是1
不信的话可以验证一下。
那么这种运算对我们的算法有什么帮助呢?
首先我们来解决一下问题1。
先列举出从1~32的lowbit,
1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 16 1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 32
我们让第i个位置管理[i-lowbit(i)+1,i]这一段区间,示意图如下:
怎么看每个数字管理多少?
只要顺着数字往下画竖线,碰到的第一根横线所覆盖的范围就是它能管理的范围。
我们每次执行操作A(把位置x的值+k),只需要把"能管理到x的所有位置"都+k就行
那么怎样快速找到哪些位置能管理到x呢?
答案还是lowbit
我们先更新x,然后把x赋给一个新值,x+lowbit(x),那么新值依然可以管理到x,这样依次类推直到
x>10000即可。
比如x=2,那么首先把2的值+k,这不用说。
然后x的新值=x+lowbit(x)=2+lowbit(2)=4,对着上面的示意图看看,会发现4确实能管理到2,那么把4的位置+k
然后再来一遍,x=4+lowbit(4)=8,发现8还是能管理到2,继续给8这个位置+k,就这样依次类推下去
直到x=16384时,超过10000了,操作完成。
这样操作之后,树状数组里每一位当前存的值可能并不是该位置的实际值,为了方便区分,在下文中我们把实际值叫做"原数组的值",当前值就叫做"树状数组的值"。
可以证明,对于任意一个x属于[1,10000]我们最多进行log(2,10000)次操作,就可以完成操作A
那么把操作A变复杂(从O(1)变到O(logn))能换来什么好处?
答案就是,可以把操作B的时间复杂度降低成log级别的
询问区间[L,R]的和sum(L,R)。我们只需要求出sum(1,R)和sum(1,L-1),
然后sum(1,R)-sum(1,L-1)就是sum(L,R)了
那么对于任意的x,sum(1,x)怎么求呢?
我们把最终得到的答案存在ans变量中,执行下面的操作:
(1)ans初始化为0
(2)ans加上x位置的值
(3)给x赋予新值 x-lowbit(x)
(4)如果x>0则跳回操作(2),否则结束算法。
举个例子介绍一下:
一开始我们还是停留在树状数组第x位置上(比如x=6吧),答案一开始为0。
还记得吗,我们在进行"给原数组第x位置的数增加k"这个操作时,把"能管理到x的所有位置"都增加了k。
那么,对于任意一个位置,树状数组里的值就是"它能管理到的所有位置上,原数组的值之和"。
因此我们给答案加上树状数组第x位置的值,这里就得到了sum(5,6),因为6能管理[5,6]
然后给x减去lowbit(x),得到4。再加上x位置的值,也就是sum(1,4),因为4能管理[1,4]
再让x=x-lowbit(x),得到0,由于不再大于0,算法终止,得到答案。
这时答案恰好是sum(1,6),哈哈~
依然可以证明,最多只需要进行log级别次数的查询。
这样我们进行操作B的时间复杂度也是log级别了。
至此,树状数组就说完了,问题1也成功得到解决,时间复杂度O((mA+mB)*logn)。
在10000这个数量级下明显比之前的O(mA+(mB*n))小得多。
而且,位运算的常数非常小,因此整个算法执行速度会很快。
问题2怎么办?用差分的方法,区间[l,r]所有值+k改成"位置l加上k,位置r+1减去k"
查询的时候直接查询sum(1,x)就行,不理解的话可以自己构造一组数据尝试一下。
问题3怎么办?稍微复杂一点
用两个树状数组,分别叫做d和s
进行A操作时,d维护差分,s维护x*d[x]。
update(d,l,x);update(d,r+1,-x);
update(s,l,x*l);update(s,r+1,-x*(r+1));
进行B操作时
sum(L,R)=sum(1,R)-sum(1,L-1)
sum(1,L-1)=L*query(d,L-1)-query(s,L-1)
sum(1,R)=(R+1)*query(d,R)-query(s,R)
此方法是从博客看到的,感谢作者,并附上链接:
【小结】树状数组的区间修改与区间查询 - 每天心塞一点点 - 博客频道 - CSDN.NET
最后附上我自己封装的c++树状数组模板~
首先是简洁版,适合比赛现场手写,非常简短
int tree[100010],n=100000;
void add(int x,int num)
{
for(;x<=n;x+=x&-x)
tree[x]+=num;
}
int sum(int x)
{
int answer =0;
for(;x>0;x-=x&-x)
answer+=tree[x];
return answer;
}
然后是一个功能较全的模板类,可以在项目里使用(好像并没有项目用得到2333)
(模板当前版本号为V1.2)
/**
* 树状数组模板使用说明
* 以下将树状数组维护的区间称为原数组
* 操作 说明 时间复杂度 支持范围
* size() 返回树状数组的大小 O(1) ~
* resize(x) 重新指定树状数组的大小为x O(1) x>=0
* add(i,v) 将原数组第i位增加v O(logn) 0<=i
namespace OrangeOI
{
template
class BinaryIndexTree
{
private:
size_t mSize;
std::vector mArray;
struct BinaryIndexTree_Node
{
BinaryIndexTree_Node(BinaryIndexTree& bit, size_t pos) :
mBIT(bit), mPos(pos) {}
const BinaryIndexTree_Node operator +=(Type value)
{
mBIT.add(mPos, value);
return *this;
}
const BinaryIndexTree_Node operator -=(Type value)
{
mBIT.add(mPos, -value);
return *this;
}
operator Type()
{
return mBIT.sum(mPos);
}
private:
BinaryIndexTree& mBIT;
size_t mPos;
};
int lowbit(int num)
{
return num&(~num + 1);
}
public:
BinaryIndexTree() {}
BinaryIndexTree(size_t size) :
mSize(size)
{
mArray.resize(mSize);
}
virtual ~BinaryIndexTree() {}
const size_t size()
{
return mSize;
}
void resize(size_t size)
{
mSize = size;
mArray.resize(size);
}
void add(int index, Type value)
{
for (; index= 0; index -= lowbit(index + 1))
answer += mArray[index];
return answer;
}
Type sum(int left, int right)
{
return sum(right) - sum(left - 1);
}
BinaryIndexTree_Node operator[](size_t pos)
{
return BinaryIndexTree_Node(*this, pos);
}
};
}
如果大家觉得哪里讲的不是很清楚,欢迎在评论里提出,我抽空修改,谢谢啦
以上。
来自