據說樹狀數組就是線段樹。線段樹參考鏈接
有一天,小明給了我三個問題(其實是我自己出的啦~)
(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<size
* sum(i) 返回原數組下標從0到i的和 O(logn) 0<=i<size
* sum(l,r) 返回原數組下標從l到r的和 O(logn) 0<=l<=r<size
* a[i] 返回原數組下標從0到i的和 O(logn) 0<=i<size
* a[i]+=v 將原數組第i位增加v O(logn) 0<=i<size
* a[i]-=v 將原數組第i位減去v O(logn) 0<=i<size
* 警告:超出操作支持的範圍,產生的結果無法預料。
* 版權所有,非商業用途可以無限制使用,複製。禁止作爲商業用途使用。
**/
#include<vector>
namespace OrangeOI
{
template<typename Type>
class BinaryIndexTree
{
private:
size_t mSize;
std::vector<Type> 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<mSize; index += lowbit(index + 1))
mArray[index] += value;
}
Type sum(int index)
{
Type answer = Type();
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);
}
};
}
如果大家覺得哪裏講的不是很清楚,歡迎在評論裏提出,我抽空修改,謝謝啦
以上。