線段樹
線段樹,一種拿來解決 \(RMQ\) 與區間和問題的高效的資料結構,對於這兩種問題,線段樹均能在 \(O(mlog_{2}n)\) 的時間複雜度內解決。在這兩個基礎的應用場景的基礎上,線段樹還發展出了各種豐富的應用。
總的來說,線段樹是一個應用廣泛,功能強大的資料結構,本章將介紹其概念及基本操作。
本章將包括:
- 線段樹
- 一 概念
- (1.原理
- (2.分析
- 二 實現
- (1.建樹
- 關於區間問題:
- (2.區間修改
- (3.區間查詢
- 三 例題
- 一 概念
一 概念
概括的說,線段樹可以理解為“分治法+二叉樹結構+ \(Lazy-tag\) 技術”。
(1.原理
線段樹是分治法與樹的有機結合,可將線段樹上的每個節點視為“線段”(或“區間”),區間是由分治得到的,下圖為一棵表示區間 \([1,10]\) 的線段樹:
(2.分析
由圖可以看出其有幾點基本特徵:
1.用分治法自頂向下建立,每次分治左右子樹各一半。
2.每個節點都表示一個區間 \([L,R]\) 葉節點 \(L==R\) ,非葉結點 \(L<R\) 。
3.除最後一層其它層都是滿的。
對於每個區間 \([L,R]\) 區間都有:
若 \(L \ne R\) ,即 \([L,R]\) 不為葉子結點;則可將其分為 \([L,M]\) 與 \([M + 1,R]\) 兩部分,其中 \(M=(L+R)/2\) ;
線段樹節點(即線段)表示的值,可以為線段上的區間和,也可定義為區間最值或其他要求值,這也是線段樹為什麼靈活多變的原因
由上述條件可知,線段樹適合解決的問題的特徵是:大區間的解可以由小區間的解合併而來。
二 實現
線段樹的程式碼實現。
(1.建樹
分治建樹,順帶維護線段;
int ls(int p){return p<<1;} //左兒,2*n
int rs(int p){return p<<1|1;} //右兒,2*n+1
int a[maxn],tree[maxn]; //a陣列儲存數列元素,tree樹組表示一個線段區間的值
void push_up(int p){ //維護線段
return tree[p]=tree[ls(p)]+tree[rs(p)];
//若題目為求最小值,則此處為 tree[p]=min(tree[ls(p)],tree[rs(p)]);
}
void build_tree(ll p,ll pl,ll pr){//p為節點編號,其指向區間[pl,pr]
if(pl==pr){
tree[p]=a[pl]; //葉子結點,賦值
return;
}
ll mid=(pl+pr)>>1; //分治,折半
build_tree(ls(p),pl,mid); //左子樹
build_tree(rs(p),mid+1,pr); //右子樹
push_up(p); //從下往上維護線段樹陣列
}
關於區間問題:
對於區間問題,我們採用與分治一樣的手段,(即:大區間的解由小區間的解合併而來)。具體是怎樣操作呢?模版如下
//在遞迴函式中
void mo(int l,int r,int p,int pl,int pr){
//要進行__操作的區間[l,r],現在查詢到p線段[pl,pr];
if(l<=pl&&r>=pr){ //[pl,pr]包含於[l,r]或[pl,pr]等於[l,r]
//todo 即進行操作
return;
}
}
但如果你對分治掌握的並不好(像我一樣),理解起來可能有一點難度,用下圖來解釋一下(掌握的可以跳過)
上圖即為 l<=pl&&r>=pr
的情況。當然,情況不唯一,我們可以簡單的理解為區間 \([L,R]\) 由三個小區間 \([L,pl]\) , \([pl,pr]\) , \([pr,R]\) 組成,由是,對於區間 \([L,R]\) 的操作便可由三個小區間的解合併而來。
(2.區間修改
首先,我們需要引入 \(Lazy-tag\) 的概念:技如其名,懶標記,指在進行區間修改時,不一次執行到底,而是打下標記,當用到時,再進行修改。(不用就不修改,為我們貪心到了不少時間複雜度)
int tag[maxn]; //tag陣列用來儲存懶標記
void add_tag(int p,int pl,int pr,int d){ //新增標記d
tag[p]+=d;
tree[p]+=d*(pr-pl);
}
void push_tag(int p,int pl,int pr){ //維護懶陣列標記
if(tag[p]){ //之前留下的懶標記,為了不被覆蓋,向下傳給子樹
int mid=(pl+pr)>>1;
add_tag(ls(p),pl,mid);
add_tag(rs(p),mid+1,pr);
tag[p]=0; //標記被傳走
}
}
void up_date(int l,int r,int p,int pl,int pr,int d){
if(l<=pl&&r>=pr){
add_tag(p,pl,pr,d); //打上懶標記,畢竟我是懶人
return;
}
push_down(p,pl,pr); //如不能覆蓋,把tag傳給子樹
ll mid=(pl+pr)>>1;
if(l<=mid) updata(l,r,ls(p),pl,mid,d ); //遞迴左子樹
if(r>mid) updata(l,r,rs(p),mid+1,pr,d); //遞迴右子樹
push_up(p); //維護線段樹
}
程式碼如上所示,懶標記不止能記錄加法,如乘,除,乘方,開方,異或等。都可以以懶標記的形式儲存,但當存在多個不同的懶標記時,注意懶標記的處理順序,順序不當可能會導致精度出現差異。
當然,對於單點修改問題,區間修改也可以兼任,只需查詢時讓 \(l=r\) 即可。
(3.區間查詢
int query(int l,int r,int p,int pl,int pr){
if(l<=pl&&r>=pr){
return tree[p];
}
push_down(p,pl,pr);
int res=0,mid=(pl+pr)>>1;
if(l>pl) res+=query(l,r,ls(p),pl,mid);
if(p<pr) res+=query(l,r,rs(p),mid+1,pr);
return res;
}
此為區間求和的的程式碼,挨個加起來,沒什麼好說的。
三 例題
- 3372 【模板】線段樹 :
最經典的班子題,區間加+區間求和
- P3870 [TJOI2009] 開關
對於線段樹的另一種應用方式,好好利用異或,此題會簡單不少
- P2357守墓人
花哨了一點,仔細一看,還是區間加+區間求和的版子