線段樹的原理及實現
前言
有時我們需要對陣列中[i, j]區間中的所有值進行操作,這樣的操作對於普通的樹來說是十分麻煩的,所以我們引入了新的一種樹——線段樹。
線段樹
線段樹(segment tree),顧名思義, 是用來存放給定區間(segment, or interval)內對應資訊的一種資料結構。即線段樹的每一個節點代表一段區間,而節點的值代表區間的一個性質,他可以是區間的和、區間的最大公約數。。。
使用線段樹時要求其具有區間可加性,即一個大區間的值能夠由它分開的兩個小區間的值得到。
舉個栗子:總區間數字和 = 左半區間數字和 + 右半區間數字和
舉個反慄:由左半區間的眾數和右半區間的眾數求不出 總區間的眾數
來張圖便於大家理解
基本實現
儲存實現:
線段樹是以二叉樹的形式來表示的,所以我們只需要一個數組來表示,i節點的左右節點分別是i * 2 和i * 2 + 1。由上圖我們也可以看出,線段樹不一定是完全二叉樹,考慮到極端情況,若區間的長度為N,則線段樹的陣列長度開到4 * N絕對是夠用的。
程式碼以區間和的線段樹為例進行講解。
pushUp操作:
pushUp表示由下往上跟新節點的值,當我們改變了區間中某一個值或某些值時,我們需要沿著線段樹向上更新改變值的節點與根節點間的所有值,可以看出這部操作與線段樹的高度有關,複雜度為O(logn)。
void pushUp(int id){ sum[id] = sum[id * 2] + sum[id * 2 + 1]; }
構造:
線段樹的構造採用遞迴方式,先找到葉節點在樹中的位置,然後一步步遞迴構建起整個樹。
const int N = 1000000; long long int sum[N], lazyT[N]; long long int a[N]; void buildTree(int l, int r, int id){ if(l == r){ sum[id] = a[l]; return; } int mid = (l + r) / 2; buildTree(l, mid, id * 2); buildTree(mid + 1, r, id * 2 + 1); pushUp(id); }
點修改:
改變區件中的某一個值,即在樹中找到對應的葉節點,更新其值,然後遞迴改變該節點到根節點路徑上所有節點的值。
//change a node(a[idChange] += num)
void update(int idChange, int num, int l, int r, int id){
if(l == r){
sum[id] += num;
return;
}
int mid = (l + r) / 2;
if(idChange <= mid){
update(idChange, num, l, mid, id * 2);
}
else{
update(idChange, num, mid + 1, r, id * 2 + 1);
}
pushUp(id);
}
區間修改:
在區間修改的時候我們引入一個新的概念——懶惰標記
懶惰標記:表示本節點的統計資訊已經根據標記更新過了,但是本節點的子節點還沒有更新。
為什麼叫懶惰標記呢,哈哈,因為程式猿實在太懶了,構建個樹都要偷工減料。且待我慢慢給你講解這懶惰標記的作用
比如還是這棵樹,我們要把1-13區間中的所有值都+1,按照正常思維,你肯定會想到從根節點開始一路向下找找到每一個葉節點+1,然後遞迴改變所有節點的值,最終遞歸回去,這樣查詢時就會得到正確的結果。
但程式猿的思維是不一樣的(懶的要命),說我就想得到1-13的值,你給我把整個樹都改了,太麻煩了,我太懶了,懶的去改整個樹。不就是+1嘛,我給根節點打個+1標記,就像套個BUFF一樣,這樣根節點1-13的值num = num + 13 * 1,這樣不是省了很多事嗎。
這時有人問了,你這是查詢1-13,你可以這樣偷懶,那如果我要查詢3-10呢?程式猿嘿嘿一笑,那我就下推懶惰標記唄,BUFF從1-13轉移到1-7和8-13上,這樣1-7 num = num + 7 * 1, 8-13num = num + 6 * 1, 再看好像還不是要找的曲間,繼續下推,直到找的找到3-4,5-7,8-10三個區間,給這三個套上BUFF,將這三個區間的值改變後加起來便是要查詢的值了,這樣還是省了好多操作有沒有。
當然如果你要找某一個節點的值,那懶惰標記必然是推到底了,也就查詢操作的時間最多還是O(logn)的。
懶惰標記分為相對標記和絕對標記
相對標記:與懶惰標記的標記順序無關,比如說+1,這樣的懶惰標記可以疊加,比如說你先添加了標記+1,後又新增一個+2標記,那麼這個標記可以直接變為+3
絕對標記:與懶惰標記的標記順序有關,比如說將節點的值變為a,這樣的標記是不可以疊加的,而且與順序有很大的關係。比如先添加個標記“變a”,後添加個標記“變b”,在標記的時候就要注意變a還是變b,哪個先變,哪個後變,這些都很重要。
來看看操作的c++實現:
pushDown:懶惰標記下推
update:區間修改
void pushDown(int id, int leftTreeNum, int rightTreeNum){
if(lazyT[id]){
lazyT[id * 2] += lazyT[id];
lazyT[id * 2 + 1] += lazyT[id];
sum[id * 2] += lazyT[id] * leftTreeNum;
sum[id * 2 + 1] += lazyT[id] * rightTreeNum;
lazyT[id] = 0;
}
}
//change a [](a[idChangeLeft, a[idChangeRight]] += num)
void update(int idChangeLeft, int idChangeRight, int num, int l, int r, int id){
if(idChangeLeft <= l && r <= idChangeRight){
sum[id] += num * (r - l + 1);
lazyT[id] += num;
return;
}
int mid = (l + r) / 2;
pushDown(id, mid - l + 1, r - mid);
if(idChangeLeft <= mid){
update(idChangeLeft, idChangeRight, num, l, mid, id * 2);
}
if(idChangeRight > mid){
update(idChangeLeft, idChangeRight, num, mid + 1, r, id * 2 + 1);
}
pushUp(id);
}
查詢操作:
在查詢過程中有一個很重要的操作便是懶惰標記的下推,理解了懶惰標記的下推,查詢操作就很好理解了。
long long int query(int queryLeft, int queryRight, int l, int r, int id){
if(queryLeft <= l && r <= queryRight){
return sum[id];
}
int mid = (l + r) / 2;
pushDown(id, mid - l + 1, r - mid);
long long int ans = 0;
if(queryLeft <= mid){
ans += query(queryLeft, queryRight, l, mid, id * 2);
}
if(queryRight > mid){
ans += query(queryLeft, queryRight, mid + 1, r, id * 2 + 1);
}
return ans;
}
完整程式碼:
#include <iostream>
using namespace std;
const int N = 1000000;
long long int sum[N], lazyT[N];
long long int a[N];
void pushUp(int id){
sum[id] = sum[id * 2] + sum[id * 2 + 1];
}
void pushDown(int id, int leftTreeNum, int rightTreeNum){
if(lazyT[id]){
lazyT[id * 2] += lazyT[id];
lazyT[id * 2 + 1] += lazyT[id];
sum[id * 2] += lazyT[id] * leftTreeNum;
sum[id * 2 + 1] += lazyT[id] * rightTreeNum;
lazyT[id] = 0;
}
}
void buildTree(int l, int r, int id){
if(l == r){
sum[id] = a[l];
return;
}
int mid = (l + r) / 2;
buildTree(l, mid, id * 2);
buildTree(mid + 1, r, id * 2 + 1);
pushUp(id);
}
//change a node(a[idChange] += num)
void update(int idChange, int num, int l, int r, int id){
if(l == r){
sum[id] += num;
return;
}
int mid = (l + r) / 2;
if(idChange <= mid){
update(idChange, num, l, mid, id * 2);
}
else{
update(idChange, num, mid + 1, r, id * 2 + 1);
}
pushUp(id);
}
//change a [](a[idChangeLeft, a[idChangeRight]] += num)
void update(int idChangeLeft, int idChangeRight, int num, int l, int r, int id){
if(idChangeLeft <= l && r <= idChangeRight){
sum[id] += num * (r - l + 1);
lazyT[id] += num;
return;
}
int mid = (l + r) / 2;
pushDown(id, mid - l + 1, r - mid);
if(idChangeLeft <= mid){
update(idChangeLeft, idChangeRight, num, l, mid, id * 2);
}
if(idChangeRight > mid){
update(idChangeLeft, idChangeRight, num, mid + 1, r, id * 2 + 1);
}
pushUp(id);
}
long long int query(int queryLeft, int queryRight, int l, int r, int id){
if(queryLeft <= l && r <= queryRight){
return sum[id];
}
int mid = (l + r) / 2;
pushDown(id, mid - l + 1, r - mid);
long long int ans = 0;
if(queryLeft <= mid){
ans += query(queryLeft, queryRight, l, mid, id * 2);
}
if(queryRight > mid){
ans += query(queryLeft, queryRight, mid + 1, r, id * 2 + 1);
}
return ans;
}
總結
線段樹作為一種由點資訊擴充套件到線資訊的資料結構,在多方面都有應用,它的查詢和修改操作的時間複雜度都為O(n),有著很好的效能。