線段樹是一種高效的資料結構,可以在\(o(nlog_n)\)的時間內查詢區間最值或區間和,解決動態的rmq
問題,並且可以為一些演算法進行優化,如dijkstra
最短路、掃瞄線等。
線段樹是一種基於分治演算法的二叉樹。每個結點維護乙個區間,以及在該區間內的資料資訊。在當前結點$x$,它的區間是$[left,right]$,則它的兩個子結點的區間分別為$[left,mid],[mid+1,right]$,$mid=\frac$。由於採用了分治的思想,在進行操作時每一層至多訪問兩個結點,極大優化了效率。根據定義,我們可以得出線段樹結點的結構:
template class segmenttree
tree[lim];
int size;
// ...
};
這裡使用了類體和模板,可以適用於更多型別。
node
結構體就是線段樹的結點,其中的sum
是該結點維護的區間和。
線段樹的基本操作就是單點修改。根據分治思想,我們訪問結點時,如果已經到了要修改的結點(區間長度為1且已經到達目標位置),就將其值修改,否則,向它的子結點遞迴,在回溯時隨便更新自己結點的值。
首先看乙個pushup
函式,用於更新當前結點的值。
void pushup(int root)
這個函式把子結點的值累加到當前結點上。由於使用了陣列,子結點可以不用進行特判是否存在,直接累加即可。
遞迴過程中,區間會不斷縮小,最後長度會變為\(1\),即為乙個點,如果這個點和要修改的點重合,就可以修改了。時間複雜度為\(o(log_n)\)。
void modify(int root, int left, int right, int pos, t key)
int mid = left + right >> 1;
modify(root << 1, left, mid, pos, key);
modify(root << 1 | 1, mid + 1, right, pos, key);
pushup(root); // 更新區間和
}
現在有了單點修改,但對於乙個初始好的序列,單點修改顯然不太方便,並且時間複雜度會過大(然而有時還是可以過)。所以我們也有一種方法快速構建線段樹,並用乙個序列初始化。
void build(int root, int left, int right, t arr)
int mid = left + right >> 1;
build(root << 1, left, mid, arr);
build(root << 1 | 1, mid + 1, right, arr);
pushup(root); // 更新區間和
}
如果區間長度為$1$,則直接初始化,否則進行遞迴,回溯時`pushup`。時間複雜度為$o(n)$。只要在遞迴是進行區間的判斷,如果查詢的位置\(pos \leq mid\),則向左子樹遞迴,否則向右子樹遞迴。
t query(int root, int left, int right, int pos)
int mid = left + right >> 1;
if(pos <= mid) return query(root << 1, left, mid, pos); // 在左邊
else return query(root << 1 | 1, mid + 1, right, pos); // 在右邊
}
如果使用單點修改來實現區間修改,時間複雜度就會達到\(o((right-left+1)nlog_n)\)。所以,我們需要引入懶標記。在結點上加上懶標記就可以快速修改區間。
舉個例子:
我們有乙個維護了區間為$[1,8]$的線段樹,我們要給這個區間加上數$v$,有兩種方法:
很顯然,第二種效率更高。這就是懶標記。
懶標記可以是加的標記,也可以是乘的標記,只要滿足分配律,就可以使用懶標記。但是如果是開根號等等的操作就不可以,因為\(\sqrt \neq \sqrt + \sqrt\)。
對於目前的線段樹,我們需要修改結點的結構體:
template class segmenttree
tree[lim];
int size;
// ...
};
修改可以分為這幾個情況:
然後更新當前結點。
但這裡有乙個問題,如果我們修改時經過乙個已經打過懶標記的結點,就無法正確更新區間和。這時候就需要標記下傳了。在修改區間之前,首先標記下傳,使子結點的區間和也更新,這樣就能得到正確答案了。
void update(int root, int left, int right, t key)
void pushdown(int root, int left, int right)
再根據上面講解,我們可以得出以下區間修改**。
void modify(int root, int left, int right, int mleft, int mright, t key)
int mid = left + right >> 1;
pushdown(root, left, right);
if (mleft <= mid) // 如果修改區間被左邊包含,則往左邊區間遞迴
modify(root << 1, left, mid, mleft, mright, key);
if (mid < mright) // 如果修改區間被右邊包含,則往右邊區間遞迴
modify(root << 1 | 1, mid + 1, right, mleft, mright, key);
pushup(root); // 更新
}
與區間查詢類似,也需要進行標記下傳。
查詢可以也可以分為這三個情況:
最後累加左右子樹區間和。
t query(int root, int left, int right, int qleft, int qright)
```cpp
template class segmenttree
void build(t arr)
void modify(int left, int right, t key)
t query(int left, int right)
private:
struct node
tree[lim];
int size;
void pushup(int root)
void update(int root, int left, int right, t key)
void pushdown(int root, int left, int right)
void build(int root, int left, int right, t arr)
int mid = left + right >> 1;
build(root << 1, left, mid, arr);
build(root << 1 | 1, mid + 1, right, arr);
pushup(root);
}void modify(int root, int left, int right, int mleft, int mright, t key)
int mid = left + right >> 1;
pushdown(root, left, right);
if (mleft <= mid)
modify(root << 1, left, mid, mleft, mright, key);
if (mid > mright)
modify(root << 1 | 1, mid + 1, right, mleft, mright, key);
pushup(root);
}t query(int root, int left, int right, int qleft, int qright)
資料結構 線段樹
啦啦啦啦啦啦線段樹是個好東西 好吧並沒有什麼好的 但貌似還是很好啊 線段樹就是一棵樹!顧名思義 又是這個詞 就是求關於一段的某些什麼什麼東西。比如區間最大值啊什麼的。引用百科知識 線段樹是一種二叉搜尋樹,與區間樹相似,它將乙個區間劃分成一些單元區間,每個單元區間對應線段樹中的乙個葉結點。對於線段樹中...
資料結構 線段樹
一 目標 1.如何快速的查詢出下列陣列arr 2,5 的和 2。以及更新arr 4 為6。用普通的方法查詢的複雜度為o n 更新的複雜度為o 1 這時候我們可以用線段樹來快速完成這些操作,複雜度為logn。二 內容 如何建立,查詢,更新線段樹。public class qurqpd int tree...
資料結構 線段樹
線段樹是一顆平衡的二叉搜尋樹,他以空間換區時間,讓線性查詢加速log級別的查詢,用到的演算法主要是二分搜尋和遞迴。例如 有陣列data 我有乙個需求,我需要頻繁的查詢區間i j的sum和。這裡先給出兩個解決方案 如果使用最普通的演算法遍歷,那麼查詢和更新的複雜度為o n 當然你還可以使用動態規劃,定...